├── .github ├── actions-rs │ └── grcov.yml ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── filename_parts.rs ├── main.rs ├── mem_fs.rs └── opts.rs └── tests └── integration_test.rs /.github/actions-rs/grcov.yml: -------------------------------------------------------------------------------- 1 | branch: true 2 | ignore-not-existing: true 3 | llvm: true 4 | output-type: lcov 5 | prefix-dir: /home/runner/work/unf/unf 6 | ignore: 7 | - "/*" 8 | - "../*" 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | allow: 8 | - dependency-type: direct 9 | - dependency-type: indirect 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions-rs/example/blob/master/.github/workflows 2 | 3 | on: [push, pull_request] 4 | 5 | name: tests 6 | 7 | jobs: 8 | check: 9 | name: Check 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout sources 13 | uses: actions/checkout@v3 14 | 15 | - name: Install stable toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | override: true 21 | 22 | - name: Run cargo check 23 | uses: actions-rs/cargo@v1 24 | with: 25 | command: check 26 | 27 | test: 28 | name: Test Suite 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v3 33 | 34 | - name: Install stable toolchain 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | profile: minimal 38 | toolchain: stable 39 | override: true 40 | 41 | - name: Run cargo test 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | 46 | lints: 47 | name: Lints 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout sources 51 | uses: actions/checkout@v3 52 | 53 | - name: Install stable toolchain 54 | uses: actions-rs/toolchain@v1 55 | with: 56 | profile: minimal 57 | toolchain: stable 58 | override: true 59 | components: rustfmt, clippy 60 | 61 | - name: Run cargo fmt 62 | uses: actions-rs/cargo@v1 63 | with: 64 | command: fmt 65 | args: --all -- --check 66 | 67 | - name: Run cargo clippy 68 | uses: actions-rs/cargo@v1 69 | with: 70 | command: clippy 71 | args: -- -D warnings 72 | 73 | grcov: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - name: Checkout sources 77 | uses: actions/checkout@v3 78 | 79 | - name: Install toolchain 80 | uses: actions-rs/toolchain@v1 81 | with: 82 | toolchain: nightly 83 | override: true 84 | profile: minimal 85 | 86 | - name: Execute tests 87 | uses: actions-rs/cargo@v1 88 | with: 89 | command: test 90 | env: 91 | CARGO_INCREMENTAL: 0 92 | RUSTFLAGS: -Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests 93 | RUSTDOCFLAGS: -Cpanic=abort 94 | 95 | # Note that `actions-rs/grcov` Action can install `grcov` too, 96 | # but can't use faster installation methods yet. 97 | # As a temporary experiment `actions-rs/install` Action plugged in here. 98 | # Consider **NOT** to copy that into your workflow, 99 | # but use `actions-rs/grcov` only 100 | - name: Pre-installing grcov 101 | uses: actions-rs/install@v0.1 102 | with: 103 | crate: grcov 104 | use-tool-cache: true 105 | 106 | - name: Gather coverage data 107 | id: coverage 108 | uses: actions-rs/grcov@v0.1 109 | with: 110 | config: .github/actions-rs/grcov.yml 111 | 112 | - name: Coveralls upload 113 | uses: coverallsapp/github-action@master 114 | with: 115 | github-token: ${{ secrets.GITHUB_TOKEN }} 116 | path-to-lcov: ${{ steps.coverage.outputs.report }} 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | TAGS 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.19" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "assert_cmd" 16 | version = "2.0.4" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" 19 | dependencies = [ 20 | "bstr", 21 | "doc-comment", 22 | "predicates", 23 | "predicates-core", 24 | "predicates-tree", 25 | "wait-timeout", 26 | ] 27 | 28 | [[package]] 29 | name = "atty" 30 | version = "0.2.14" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 33 | dependencies = [ 34 | "hermit-abi", 35 | "libc", 36 | "winapi", 37 | ] 38 | 39 | [[package]] 40 | name = "autocfg" 41 | version = "1.1.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 44 | 45 | [[package]] 46 | name = "bitflags" 47 | version = "1.3.2" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 50 | 51 | [[package]] 52 | name = "bstr" 53 | version = "0.2.17" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" 56 | dependencies = [ 57 | "lazy_static", 58 | "memchr", 59 | "regex-automata", 60 | ] 61 | 62 | [[package]] 63 | name = "cc" 64 | version = "1.0.73" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 67 | 68 | [[package]] 69 | name = "cfg-if" 70 | version = "1.0.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 73 | 74 | [[package]] 75 | name = "clap" 76 | version = "3.2.20" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" 79 | dependencies = [ 80 | "atty", 81 | "bitflags", 82 | "clap_derive", 83 | "clap_lex", 84 | "indexmap", 85 | "once_cell", 86 | "strsim", 87 | "termcolor", 88 | "textwrap", 89 | ] 90 | 91 | [[package]] 92 | name = "clap_derive" 93 | version = "3.2.18" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" 96 | dependencies = [ 97 | "heck", 98 | "proc-macro-error", 99 | "proc-macro2", 100 | "quote", 101 | "syn", 102 | ] 103 | 104 | [[package]] 105 | name = "clap_lex" 106 | version = "0.2.4" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 109 | dependencies = [ 110 | "os_str_bytes", 111 | ] 112 | 113 | [[package]] 114 | name = "clipboard-win" 115 | version = "4.4.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" 118 | dependencies = [ 119 | "error-code", 120 | "str-buf", 121 | "winapi", 122 | ] 123 | 124 | [[package]] 125 | name = "deunicode" 126 | version = "1.3.2" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "08ff6a4480d42625e59bc4e8b5dc3723279fd24d83afe8aa20df217276261cd6" 129 | 130 | [[package]] 131 | name = "difflib" 132 | version = "0.4.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 135 | 136 | [[package]] 137 | name = "dirs-next" 138 | version = "2.0.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 141 | dependencies = [ 142 | "cfg-if", 143 | "dirs-sys-next", 144 | ] 145 | 146 | [[package]] 147 | name = "dirs-sys-next" 148 | version = "0.1.2" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 151 | dependencies = [ 152 | "libc", 153 | "redox_users", 154 | "winapi", 155 | ] 156 | 157 | [[package]] 158 | name = "doc-comment" 159 | version = "0.3.3" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 162 | 163 | [[package]] 164 | name = "either" 165 | version = "1.8.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" 168 | 169 | [[package]] 170 | name = "endian-type" 171 | version = "0.1.2" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 174 | 175 | [[package]] 176 | name = "errno" 177 | version = "0.2.8" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 180 | dependencies = [ 181 | "errno-dragonfly", 182 | "libc", 183 | "winapi", 184 | ] 185 | 186 | [[package]] 187 | name = "errno-dragonfly" 188 | version = "0.1.2" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 191 | dependencies = [ 192 | "cc", 193 | "libc", 194 | ] 195 | 196 | [[package]] 197 | name = "error-code" 198 | version = "2.3.1" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" 201 | dependencies = [ 202 | "libc", 203 | "str-buf", 204 | ] 205 | 206 | [[package]] 207 | name = "fastrand" 208 | version = "1.8.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 211 | dependencies = [ 212 | "instant", 213 | ] 214 | 215 | [[package]] 216 | name = "fd-lock" 217 | version = "3.0.6" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "e11dcc7e4d79a8c89b9ab4c6f5c30b1fc4a83c420792da3542fd31179ed5f517" 220 | dependencies = [ 221 | "cfg-if", 222 | "rustix", 223 | "windows-sys", 224 | ] 225 | 226 | [[package]] 227 | name = "fuchsia-cprng" 228 | version = "0.1.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 231 | 232 | [[package]] 233 | name = "getrandom" 234 | version = "0.2.7" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 237 | dependencies = [ 238 | "cfg-if", 239 | "libc", 240 | "wasi", 241 | ] 242 | 243 | [[package]] 244 | name = "hashbrown" 245 | version = "0.12.3" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 248 | 249 | [[package]] 250 | name = "heck" 251 | version = "0.4.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 254 | 255 | [[package]] 256 | name = "hermit-abi" 257 | version = "0.1.19" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 260 | dependencies = [ 261 | "libc", 262 | ] 263 | 264 | [[package]] 265 | name = "indexmap" 266 | version = "1.9.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 269 | dependencies = [ 270 | "autocfg", 271 | "hashbrown", 272 | ] 273 | 274 | [[package]] 275 | name = "instant" 276 | version = "0.1.12" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 279 | dependencies = [ 280 | "cfg-if", 281 | ] 282 | 283 | [[package]] 284 | name = "io-lifetimes" 285 | version = "0.7.3" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "1ea37f355c05dde75b84bba2d767906ad522e97cd9e2eef2be7a4ab7fb442c06" 288 | 289 | [[package]] 290 | name = "itertools" 291 | version = "0.10.5" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 294 | dependencies = [ 295 | "either", 296 | ] 297 | 298 | [[package]] 299 | name = "lazy_static" 300 | version = "1.4.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 303 | 304 | [[package]] 305 | name = "libc" 306 | version = "0.2.139" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 309 | 310 | [[package]] 311 | name = "linux-raw-sys" 312 | version = "0.0.46" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" 315 | 316 | [[package]] 317 | name = "log" 318 | version = "0.4.17" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 321 | dependencies = [ 322 | "cfg-if", 323 | ] 324 | 325 | [[package]] 326 | name = "maybe-uninit" 327 | version = "2.0.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 330 | 331 | [[package]] 332 | name = "memchr" 333 | version = "2.5.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 336 | 337 | [[package]] 338 | name = "memoffset" 339 | version = "0.6.5" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 342 | dependencies = [ 343 | "autocfg", 344 | ] 345 | 346 | [[package]] 347 | name = "nibble_vec" 348 | version = "0.1.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 351 | dependencies = [ 352 | "smallvec 1.8.0", 353 | ] 354 | 355 | [[package]] 356 | name = "nix" 357 | version = "0.23.1" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" 360 | dependencies = [ 361 | "bitflags", 362 | "cc", 363 | "cfg-if", 364 | "libc", 365 | "memoffset", 366 | ] 367 | 368 | [[package]] 369 | name = "once_cell" 370 | version = "1.15.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" 373 | 374 | [[package]] 375 | name = "os_str_bytes" 376 | version = "6.3.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" 379 | 380 | [[package]] 381 | name = "owning_ref" 382 | version = "0.3.3" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37" 385 | dependencies = [ 386 | "stable_deref_trait", 387 | ] 388 | 389 | [[package]] 390 | name = "parking_lot" 391 | version = "0.4.8" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "149d8f5b97f3c1133e3cfcd8886449959e856b557ff281e292b733d7c69e005e" 394 | dependencies = [ 395 | "owning_ref", 396 | "parking_lot_core", 397 | ] 398 | 399 | [[package]] 400 | name = "parking_lot_core" 401 | version = "0.2.14" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "4db1a8ccf734a7bce794cc19b3df06ed87ab2f3907036b693c68f56b4d4537fa" 404 | dependencies = [ 405 | "libc", 406 | "rand", 407 | "smallvec 0.6.14", 408 | "winapi", 409 | ] 410 | 411 | [[package]] 412 | name = "predicates" 413 | version = "2.1.1" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" 416 | dependencies = [ 417 | "difflib", 418 | "itertools", 419 | "predicates-core", 420 | ] 421 | 422 | [[package]] 423 | name = "predicates-core" 424 | version = "1.0.3" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" 427 | 428 | [[package]] 429 | name = "predicates-tree" 430 | version = "1.0.5" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" 433 | dependencies = [ 434 | "predicates-core", 435 | "termtree", 436 | ] 437 | 438 | [[package]] 439 | name = "proc-macro-error" 440 | version = "1.0.4" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 443 | dependencies = [ 444 | "proc-macro-error-attr", 445 | "proc-macro2", 446 | "quote", 447 | "syn", 448 | "version_check", 449 | ] 450 | 451 | [[package]] 452 | name = "proc-macro-error-attr" 453 | version = "1.0.4" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 456 | dependencies = [ 457 | "proc-macro2", 458 | "quote", 459 | "version_check", 460 | ] 461 | 462 | [[package]] 463 | name = "proc-macro2" 464 | version = "1.0.49" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" 467 | dependencies = [ 468 | "unicode-ident", 469 | ] 470 | 471 | [[package]] 472 | name = "promptly" 473 | version = "0.3.1" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "9acbc6c5a5b029fe58342f58445acb00ccfe24624e538894bc2f04ce112980ba" 476 | dependencies = [ 477 | "rustyline", 478 | ] 479 | 480 | [[package]] 481 | name = "quote" 482 | version = "1.0.21" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 485 | dependencies = [ 486 | "proc-macro2", 487 | ] 488 | 489 | [[package]] 490 | name = "radix_trie" 491 | version = "0.2.1" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 494 | dependencies = [ 495 | "endian-type", 496 | "nibble_vec", 497 | ] 498 | 499 | [[package]] 500 | name = "rand" 501 | version = "0.4.6" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 504 | dependencies = [ 505 | "fuchsia-cprng", 506 | "libc", 507 | "rand_core 0.3.1", 508 | "rdrand", 509 | "winapi", 510 | ] 511 | 512 | [[package]] 513 | name = "rand_core" 514 | version = "0.3.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 517 | dependencies = [ 518 | "rand_core 0.4.2", 519 | ] 520 | 521 | [[package]] 522 | name = "rand_core" 523 | version = "0.4.2" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 526 | 527 | [[package]] 528 | name = "rdrand" 529 | version = "0.4.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 532 | dependencies = [ 533 | "rand_core 0.3.1", 534 | ] 535 | 536 | [[package]] 537 | name = "redox_syscall" 538 | version = "0.2.16" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 541 | dependencies = [ 542 | "bitflags", 543 | ] 544 | 545 | [[package]] 546 | name = "redox_users" 547 | version = "0.4.3" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 550 | dependencies = [ 551 | "getrandom", 552 | "redox_syscall", 553 | "thiserror", 554 | ] 555 | 556 | [[package]] 557 | name = "regex" 558 | version = "1.6.0" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 561 | dependencies = [ 562 | "aho-corasick", 563 | "memchr", 564 | "regex-syntax", 565 | ] 566 | 567 | [[package]] 568 | name = "regex-automata" 569 | version = "0.1.10" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 572 | 573 | [[package]] 574 | name = "regex-syntax" 575 | version = "0.6.27" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 578 | 579 | [[package]] 580 | name = "remove_dir_all" 581 | version = "0.5.3" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 584 | dependencies = [ 585 | "winapi", 586 | ] 587 | 588 | [[package]] 589 | name = "rsfs" 590 | version = "0.4.1" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "101969b02a54a072eaa40591765fa61d54e30bed4c7dfa7f3006380600546be6" 593 | dependencies = [ 594 | "parking_lot", 595 | ] 596 | 597 | [[package]] 598 | name = "rustix" 599 | version = "0.35.11" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "fbb2fda4666def1433b1b05431ab402e42a1084285477222b72d6c564c417cef" 602 | dependencies = [ 603 | "bitflags", 604 | "errno", 605 | "io-lifetimes", 606 | "libc", 607 | "linux-raw-sys", 608 | "windows-sys", 609 | ] 610 | 611 | [[package]] 612 | name = "rustyline" 613 | version = "9.1.2" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "db7826789c0e25614b03e5a54a0717a86f9ff6e6e5247f92b369472869320039" 616 | dependencies = [ 617 | "bitflags", 618 | "cfg-if", 619 | "clipboard-win", 620 | "dirs-next", 621 | "fd-lock", 622 | "libc", 623 | "log", 624 | "memchr", 625 | "nix", 626 | "radix_trie", 627 | "scopeguard", 628 | "smallvec 1.8.0", 629 | "unicode-segmentation", 630 | "unicode-width", 631 | "utf8parse", 632 | "winapi", 633 | ] 634 | 635 | [[package]] 636 | name = "same-file" 637 | version = "1.0.6" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 640 | dependencies = [ 641 | "winapi-util", 642 | ] 643 | 644 | [[package]] 645 | name = "scopeguard" 646 | version = "1.1.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 649 | 650 | [[package]] 651 | name = "smallvec" 652 | version = "0.6.14" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" 655 | dependencies = [ 656 | "maybe-uninit", 657 | ] 658 | 659 | [[package]] 660 | name = "smallvec" 661 | version = "1.8.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 664 | 665 | [[package]] 666 | name = "stable_deref_trait" 667 | version = "1.2.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 670 | 671 | [[package]] 672 | name = "str-buf" 673 | version = "1.0.6" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" 676 | 677 | [[package]] 678 | name = "strsim" 679 | version = "0.10.0" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 682 | 683 | [[package]] 684 | name = "syn" 685 | version = "1.0.101" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" 688 | dependencies = [ 689 | "proc-macro2", 690 | "quote", 691 | "unicode-ident", 692 | ] 693 | 694 | [[package]] 695 | name = "tempfile" 696 | version = "3.3.0" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 699 | dependencies = [ 700 | "cfg-if", 701 | "fastrand", 702 | "libc", 703 | "redox_syscall", 704 | "remove_dir_all", 705 | "winapi", 706 | ] 707 | 708 | [[package]] 709 | name = "termcolor" 710 | version = "1.1.3" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 713 | dependencies = [ 714 | "winapi-util", 715 | ] 716 | 717 | [[package]] 718 | name = "termtree" 719 | version = "0.2.4" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" 722 | 723 | [[package]] 724 | name = "textwrap" 725 | version = "0.15.1" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" 728 | 729 | [[package]] 730 | name = "thiserror" 731 | version = "1.0.37" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 734 | dependencies = [ 735 | "thiserror-impl", 736 | ] 737 | 738 | [[package]] 739 | name = "thiserror-impl" 740 | version = "1.0.37" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 743 | dependencies = [ 744 | "proc-macro2", 745 | "quote", 746 | "syn", 747 | ] 748 | 749 | [[package]] 750 | name = "unf" 751 | version = "2.1.4" 752 | dependencies = [ 753 | "assert_cmd", 754 | "clap", 755 | "deunicode", 756 | "lazy_static", 757 | "promptly", 758 | "regex", 759 | "rsfs", 760 | "tempfile", 761 | "walkdir", 762 | ] 763 | 764 | [[package]] 765 | name = "unicode-ident" 766 | version = "1.0.6" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 769 | 770 | [[package]] 771 | name = "unicode-segmentation" 772 | version = "1.10.0" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" 775 | 776 | [[package]] 777 | name = "unicode-width" 778 | version = "0.1.10" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 781 | 782 | [[package]] 783 | name = "utf8parse" 784 | version = "0.2.0" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" 787 | 788 | [[package]] 789 | name = "version_check" 790 | version = "0.9.4" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 793 | 794 | [[package]] 795 | name = "wait-timeout" 796 | version = "0.2.0" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 799 | dependencies = [ 800 | "libc", 801 | ] 802 | 803 | [[package]] 804 | name = "walkdir" 805 | version = "2.3.2" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 808 | dependencies = [ 809 | "same-file", 810 | "winapi", 811 | "winapi-util", 812 | ] 813 | 814 | [[package]] 815 | name = "wasi" 816 | version = "0.11.0+wasi-snapshot-preview1" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 819 | 820 | [[package]] 821 | name = "winapi" 822 | version = "0.3.9" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 825 | dependencies = [ 826 | "winapi-i686-pc-windows-gnu", 827 | "winapi-x86_64-pc-windows-gnu", 828 | ] 829 | 830 | [[package]] 831 | name = "winapi-i686-pc-windows-gnu" 832 | version = "0.4.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 835 | 836 | [[package]] 837 | name = "winapi-util" 838 | version = "0.1.5" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 841 | dependencies = [ 842 | "winapi", 843 | ] 844 | 845 | [[package]] 846 | name = "winapi-x86_64-pc-windows-gnu" 847 | version = "0.4.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 850 | 851 | [[package]] 852 | name = "windows-sys" 853 | version = "0.36.1" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 856 | dependencies = [ 857 | "windows_aarch64_msvc", 858 | "windows_i686_gnu", 859 | "windows_i686_msvc", 860 | "windows_x86_64_gnu", 861 | "windows_x86_64_msvc", 862 | ] 863 | 864 | [[package]] 865 | name = "windows_aarch64_msvc" 866 | version = "0.36.1" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 869 | 870 | [[package]] 871 | name = "windows_i686_gnu" 872 | version = "0.36.1" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 875 | 876 | [[package]] 877 | name = "windows_i686_msvc" 878 | version = "0.36.1" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 881 | 882 | [[package]] 883 | name = "windows_x86_64_gnu" 884 | version = "0.36.1" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 887 | 888 | [[package]] 889 | name = "windows_x86_64_msvc" 890 | version = "0.36.1" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 893 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unf" 3 | version = "2.1.4" 4 | authors = ["Benjamin Levy "] 5 | edition = "2021" 6 | description = """ 7 | UNixize Filename -- replace annoying anti-unix characters in filenames""" 8 | readme = "README.md" 9 | license = "MIT" 10 | repository = "https://github.com/io12/unf" 11 | 12 | [dependencies] 13 | lazy_static = "1.4.0" 14 | regex = "1.6.0" 15 | promptly = "0.3.1" 16 | deunicode = "1.3.2" 17 | clap = { version = "3.2.20", features = ["derive"] } 18 | rsfs = "0.4.1" 19 | 20 | [dev-dependencies] 21 | tempfile = "3.3.0" 22 | walkdir = "2.3.2" 23 | assert_cmd = "2.0.4" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Benjamin Levy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![crates.io](https://img.shields.io/crates/v/unf)](https://crates.io/crates/unf) 2 | [![tests](https://github.com/io12/unf/workflows/tests/badge.svg)](https://github.com/io12/unf/actions?query=workflow%3Atests) 3 | [![Coverage Status](https://coveralls.io/repos/github/io12/unf/badge.svg?branch=master)](https://coveralls.io/github/io12/unf?branch=master) 4 | 5 | # `unf` 6 | 7 | UNixize Filename -- replace annoying anti-unix characters in filenames 8 | 9 | ## About 10 | 11 | Certain characters in filenames are problematic for command-line users. For example, spaces and parentheses are treated specially by the shell. `unf` renames these files, so you no longer have to be annoyed when your Windows-using friend sends you an irritatingly-named zip file. 12 | 13 | ## Installing 14 | 15 | ### Using `cargo` 16 | 17 | ``` sh 18 | cargo install unf 19 | ``` 20 | 21 | This installs to `~/.cargo/bin`, so make sure that's in your `PATH`. 22 | 23 | ### Arch Linux 24 | 25 | Install `unf` from the AUR. 26 | 27 | ## Usage 28 | 29 | ``` 30 | unf [FLAGS] ... 31 | ``` 32 | 33 | `...`: The paths of filenames to unixize 34 | 35 | `-r` `--recursive`: Recursively unixize filenames in directories. If some of the specified paths are directories, unf will operate recursively on their contents 36 | 37 | `-f` `--force` Do not interactively prompt to rename each file 38 | 39 | ## Examples 40 | 41 | ``` sh 42 | $ unf 🤔😀😃😄😁😆😅emojis.txt 43 | rename '🤔😀😃😄😁😆😅emojis.txt' -> 'thinking_grinning_smiley_smile_grin_laughing_sweat_smile_emojis.txt'? (y/N): y 44 | ``` 45 | 46 | ``` sh 47 | $ unf -f 'Game (Not Pirated 😉).rar' 48 | rename 'Game (Not Pirated 😉).rar' -> 'Game_Not_Pirated_wink.rar' 49 | ``` 50 | 51 | ### Recursion 52 | 53 | ``` sh 54 | $ unf -rf My\ Files/ My\ Folder 55 | rename 'My Files/Passwords :) .txt' -> 'My Files/Passwords.txt' 56 | rename 'My Files/Another Cool Photo.JPG' -> 'My Files/Another_Cool_Photo.JPG' 57 | rename 'My Files/Wow Cool Photo.JPG' -> 'My Files/Wow_Cool_Photo.JPG' 58 | rename 'My Files/Cool Photo.JPG' -> 'My Files/Cool_Photo.JPG' 59 | rename 'My Files/' -> 'My_Files' 60 | rename 'My Folder' -> 'My_Folder' 61 | ``` 62 | 63 | ### Collisions 64 | 65 | ``` sh 66 | $ unf -f -- --fake-flag.txt fake-flag.txt ------fake-flag.txt ' fake-flag.txt' $'\tfake-flag.txt' 67 | rename '--fake-flag.txt' -> 'fake-flag_000.txt' 68 | rename '------fake-flag.txt' -> 'fake-flag_001.txt' 69 | rename ' fake-flag.txt' -> 'fake-flag_002.txt' 70 | rename ' fake-flag.txt' -> 'fake-flag_003.txt' 71 | ``` 72 | 73 | ## FAQ 74 | 75 | ### Is this useful? 76 | 77 | Hopefully for some people. There are certain situations in which I believe this tool is useful. 78 | 79 | - Downloading files uploaded by non-CLI users, especially large archives with poorly-named files 80 | - The ` (1)` that gets appended to web browser download duplicates 81 | - Unix tools which take advantage of the loose Unix filename restrictions (like `youtube-dl`, which creates filenames from the video title) 82 | 83 | ### How does this handle collisions? 84 | 85 | Since `unf` is an automatic batch rename tool, there may be cases where the path to the unixized filename already exists. `unf` resolves this crisis by appending and incrementing a zero-padded number to the end of the file stem. An example of this is displayed [here](#collisions). 86 | 87 | ### Why is the collision-resolving number zero-padded? 88 | 89 | It has the nice property of being ordered when using tools that sort filenames by ASCII values, such as `ls` and shell completion. 90 | 91 | ### Why not just use shell completion to access problematic filenames? 92 | 93 | Shell completion can automatically insert backslash escapes, but this is sub-optimal. The backslash escapes make the filenames substantially less readable. However, shell completion is great for invoking `unf`. 94 | -------------------------------------------------------------------------------- /src/filename_parts.rs: -------------------------------------------------------------------------------- 1 | const FILENAME_NUM_DIGITS: usize = 3; 2 | 3 | /// Struct representing a filename that can be split, modified, and 4 | /// merged back into a filename string 5 | #[derive(PartialEq, Eq, Debug)] 6 | pub struct FilenameParts { 7 | /// From the beginning of the filename to the final dot before the extension 8 | pub stem: String, 9 | 10 | /// The zero-padded collision-resolving number 11 | pub num: Option, 12 | 13 | /// The file extension, not including the dot 14 | pub ext: Option, 15 | } 16 | 17 | impl FilenameParts { 18 | pub fn merge(&self) -> String { 19 | format!( 20 | "{}{}{}", 21 | self.stem, 22 | match self.num { 23 | // Format the collision-resolving number of a filename to a 24 | // zero-padded string 25 | Some(num) => format!("_{:0width$}", num, width = FILENAME_NUM_DIGITS), 26 | None => "".to_string(), 27 | }, 28 | match &self.ext { 29 | Some(ext) => format!(".{}", ext), 30 | None => "".to_string(), 31 | } 32 | ) 33 | } 34 | 35 | pub fn from_filename(filename: &str) -> Self { 36 | let mut it = filename.rsplitn(2, '.'); 37 | let ext = it.next().expect("tried to split empty filename"); 38 | let maybe_stem_num = it.next(); 39 | 40 | // Set the stem-num combination to the extension if the iterator 41 | // said it was `None`. This is such that only the content after 42 | // the final dot is considered the extension, but extension-less 43 | // files are properly handled. 44 | let (stem_num, ext) = match maybe_stem_num { 45 | Some(stem_num) => (stem_num, Some(ext.to_string())), 46 | None => (ext, None), 47 | }; 48 | 49 | // Hack to get an iterator over the last `FILENAME_NUM_DIGITS + 1` 50 | // characters of the stem-num combination. For files that have the 51 | // collision-resolving number, this is that prefixed with an 52 | // underscore. 53 | let num_it = stem_num 54 | .chars() 55 | .rev() 56 | .take(FILENAME_NUM_DIGITS + 1) 57 | .collect::>(); 58 | let mut num_it = num_it.iter().rev(); 59 | 60 | // Determine if the filename has a collision-resolving number and 61 | // parse it 62 | let num = if num_it.next() == Some(&'_') && num_it.len() == FILENAME_NUM_DIGITS { 63 | num_it.collect::().parse::().ok() 64 | } else { 65 | None 66 | }; 67 | 68 | // Split the stem from the stem-num combination 69 | let stem = if num.is_some() { 70 | stem_num 71 | .chars() 72 | .take(stem_num.len() - FILENAME_NUM_DIGITS - 1) 73 | .collect() 74 | } else { 75 | stem_num.to_string() 76 | }; 77 | 78 | Self { stem, num, ext } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | 86 | #[test] 87 | fn from_filename() { 88 | assert_eq!( 89 | FilenameParts::from_filename("a"), 90 | FilenameParts { 91 | stem: "a".to_string(), 92 | num: None, 93 | ext: None, 94 | } 95 | ); 96 | assert_eq!( 97 | FilenameParts::from_filename("a."), 98 | FilenameParts { 99 | stem: "a".to_string(), 100 | num: None, 101 | ext: Some("".to_string()), 102 | } 103 | ); 104 | assert_eq!( 105 | FilenameParts::from_filename(".a"), 106 | FilenameParts { 107 | stem: "".to_string(), 108 | num: None, 109 | ext: Some("a".to_string()), 110 | } 111 | ); 112 | assert_eq!( 113 | FilenameParts::from_filename("a_0000"), 114 | FilenameParts { 115 | stem: "a_0000".to_string(), 116 | num: None, 117 | ext: None, 118 | } 119 | ); 120 | assert_eq!( 121 | FilenameParts::from_filename("a_137"), 122 | FilenameParts { 123 | stem: "a".to_string(), 124 | num: Some(137), 125 | ext: None, 126 | } 127 | ); 128 | assert_eq!( 129 | FilenameParts::from_filename("a_000.txt"), 130 | FilenameParts { 131 | stem: "a".to_string(), 132 | num: Some(0), 133 | ext: Some("txt".to_string()), 134 | } 135 | ); 136 | assert_eq!( 137 | FilenameParts::from_filename("a____000.txt"), 138 | FilenameParts { 139 | stem: "a___".to_string(), 140 | num: Some(0), 141 | ext: Some("txt".to_string()), 142 | } 143 | ); 144 | assert_eq!( 145 | FilenameParts::from_filename(".x._._._222.txt"), 146 | FilenameParts { 147 | stem: ".x._._.".to_string(), 148 | num: Some(222), 149 | ext: Some("txt".to_string()), 150 | } 151 | ); 152 | } 153 | 154 | #[test] 155 | fn merge() { 156 | assert_eq!( 157 | "a", 158 | FilenameParts { 159 | stem: "a".to_string(), 160 | num: None, 161 | ext: None, 162 | } 163 | .merge() 164 | ); 165 | assert_eq!( 166 | "a.", 167 | FilenameParts { 168 | stem: "a".to_string(), 169 | num: None, 170 | ext: Some("".to_string()), 171 | } 172 | .merge() 173 | ); 174 | assert_eq!( 175 | ".a", 176 | FilenameParts { 177 | stem: "".to_string(), 178 | num: None, 179 | ext: Some("a".to_string()), 180 | } 181 | .merge() 182 | ); 183 | assert_eq!( 184 | "a_0000", 185 | FilenameParts { 186 | stem: "a_0000".to_string(), 187 | num: None, 188 | ext: None, 189 | } 190 | .merge() 191 | ); 192 | assert_eq!( 193 | "a_137", 194 | FilenameParts { 195 | stem: "a".to_string(), 196 | num: Some(137), 197 | ext: None, 198 | } 199 | .merge() 200 | ); 201 | assert_eq!( 202 | "a_000.txt", 203 | FilenameParts { 204 | stem: "a".to_string(), 205 | num: Some(0), 206 | ext: Some("txt".to_string()), 207 | } 208 | .merge() 209 | ); 210 | assert_eq!( 211 | "a____000.txt", 212 | FilenameParts { 213 | stem: "a___".to_string(), 214 | num: Some(0), 215 | ext: Some("txt".to_string()), 216 | } 217 | .merge() 218 | ); 219 | assert_eq!( 220 | ".x._._._222.txt", 221 | FilenameParts { 222 | stem: ".x._._.".to_string(), 223 | num: Some(222), 224 | ext: Some("txt".to_string()), 225 | } 226 | .merge() 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | mod filename_parts; 5 | mod mem_fs; 6 | mod opts; 7 | 8 | use filename_parts::FilenameParts; 9 | use opts::Flags; 10 | use opts::Opts; 11 | 12 | use std::collections::BTreeSet; 13 | use std::ffi::OsStr; 14 | use std::ffi::OsString; 15 | use std::path::Path; 16 | use std::path::PathBuf; 17 | 18 | use clap::Parser; 19 | use deunicode::deunicode; 20 | use promptly::prompt_default; 21 | use regex::Regex; 22 | use rsfs::DirEntry; 23 | use rsfs::GenFS; 24 | use rsfs::Metadata; 25 | 26 | type Result = std::result::Result>; 27 | 28 | #[cfg(test)] 29 | mod tests { 30 | use super::*; 31 | 32 | use tempfile::TempDir; 33 | 34 | #[test] 35 | fn test_unixize_filename_str() { 36 | let f = unixize_filename_str; 37 | assert_eq!(f("verbatim"), "verbatim"); 38 | assert_eq!(f("__trim____"), "trim"); 39 | assert_eq!(f("__a___b___c__"), "a_b_c"); 40 | assert_eq!(f(" a b c "), "a_b_c"); 41 | assert_eq!(f("a-b-c"), "a-b-c"); 42 | assert_eq!( 43 | f("🤔😀😃😄😁😆😅emojis.txt"), 44 | "thinking_grinning_smiley_smile_grin_laughing_sweat_smile_emojis.txt" 45 | ); 46 | assert_eq!(f("Æneid"), "AEneid"); 47 | assert_eq!(f("étude"), "etude"); 48 | assert_eq!(f("北亰"), "Bei_Jing"); 49 | assert_eq!(f("げんまい茶"), "genmaiCha"); 50 | assert_eq!(f("🦄☣"), "unicorn_biohazard"); 51 | assert_eq!(f("Game (Not Pirated 😉).rar"), "Game_Not_Pirated_wink.rar"); 52 | assert_eq!(f("--fake-flag"), "fake-flag"); 53 | assert_eq!(f("Évidemment"), "Evidemment"); 54 | assert_eq!(f("àà_y_ü"), "aa_y_u"); 55 | } 56 | 57 | #[test] 58 | fn test_resolve_collision() { 59 | let fs = rsfs::disk::FS; 60 | let root = TempDir::new().unwrap(); 61 | let root = root.path(); 62 | test_resolve_collision_fs(&fs, root); 63 | 64 | let fs = rsfs::mem::FS::new(); 65 | let root = Path::new("/"); 66 | test_resolve_collision_fs(&fs, root); 67 | } 68 | 69 | fn test_resolve_collision_fs(fs: &FS, root: &Path) { 70 | // Helper function taking a collider filename returning a 71 | // string representing the resolved collision 72 | let f = |filename: &str| -> String { 73 | let path = root.join(filename); 74 | fs.create_file(&path).unwrap(); 75 | 76 | resolve_collision(fs, root, &path) 77 | .file_name() 78 | .unwrap() 79 | .to_str() 80 | .unwrap() 81 | .to_string() 82 | }; 83 | 84 | assert_eq!(f("a"), "a_000"); 85 | assert_eq!(f("b_000"), "b_001"); 86 | assert_eq!(f("c.txt"), "c_000.txt"); 87 | assert_eq!(f("d_333.txt"), "d_334.txt"); 88 | assert_eq!(f("e_999.txt"), "e_1000.txt"); 89 | assert_eq!(f("e_1000.txt"), "e_1000_000.txt"); 90 | assert_eq!(f("z___222.txt"), "z___223.txt"); 91 | assert_eq!(f(".x._._._222.txt"), ".x._._._223.txt"); 92 | } 93 | } 94 | 95 | /// Clean up a string representing a filename, replacing 96 | /// unix-unfriendly characters (like spaces, parentheses, etc.) See the 97 | /// unit tests for examples. 98 | fn unixize_filename_str(fname: &str) -> String { 99 | lazy_static! { 100 | static ref RE_INVAL_CHR: Regex = Regex::new("[^a-zA-Z0-9._-]").unwrap(); 101 | static ref RE_UND_DUP: Regex = Regex::new("_+").unwrap(); 102 | static ref RE_UND_DOT: Regex = Regex::new("_+\\.").unwrap(); 103 | } 104 | 105 | // Replace all UNICODE characters with their ASCII counterparts 106 | let s = deunicode(fname); 107 | // Replace all remaining invalid characters with underscores 108 | let s = RE_INVAL_CHR.replace_all(&s, "_"); 109 | // Remove duplicate underscores 110 | let s = RE_UND_DUP.replace_all(&s, "_"); 111 | // Remove underscores before dot ('.') 112 | let s = RE_UND_DOT.replace_all(&s, "."); 113 | // Remove leading and trailing underscores and hyphens 114 | let s = s.trim_matches(|c| c == '_' || c == '-'); 115 | s.to_string() 116 | } 117 | 118 | fn read_children_names(fs: &FS, cwd: &Path, dir: &Path) -> Result> { 119 | let children_names = fs 120 | .read_dir(cwd.join(dir))? 121 | .map(|result_ent| result_ent.map(|ent| ent.file_name())) 122 | .collect::>>()?; 123 | Ok(children_names) 124 | } 125 | 126 | /// Like `unixize_path()`, but only operate on children of `dir` 127 | fn unixize_children(fs: &FS, cwd: &Path, dir: &Path, flags: Flags) -> Result<()> { 128 | for file_name in read_children_names(fs, cwd, dir)? { 129 | let path = dir.join(file_name); 130 | unixize_path(fs, cwd, &path, flags)?; 131 | } 132 | Ok(()) 133 | } 134 | 135 | /// Unixize the filename(s) specified by a path, according to the 136 | /// supplied arguments 137 | fn unixize_path(fs: &FS, cwd: &Path, path: &Path, flags: Flags) -> Result<()> { 138 | let parent = path.parent().unwrap_or(cwd); 139 | let basename = &path.file_name().map(OsStr::to_string_lossy); 140 | let basename = match basename { 141 | Some(s) => s, 142 | // If the path has no basename (for example, if it's `.` or `..`), only 143 | // unixize children 144 | None => return unixize_children(fs, cwd, path, flags), 145 | }; 146 | let new_basename = unixize_filename_str(basename); 147 | 148 | let stat = fs.metadata(cwd.join(path))?; 149 | let is_dir = stat.is_dir(); 150 | let should_prompt = !flags.force && !flags.dry_run; 151 | 152 | // Determine whether to recurse, possibly by prompting the user 153 | let recurse = flags.recursive 154 | && is_dir 155 | && (!should_prompt || { 156 | let msg = format!("descend into directory '{}'?", path.display()); 157 | prompt_default(msg, false)? 158 | }); 159 | 160 | if recurse { 161 | unixize_children(fs, cwd, path, flags)?; 162 | } 163 | 164 | // Skip files that already have unix-friendly names; this is done 165 | // after recursive handling because unix-friendly directory names 166 | // might have non-unix-friendly filenames inside 167 | if basename == &new_basename { 168 | return Ok(()); 169 | } 170 | 171 | let new_path = parent.join(new_basename); 172 | let new_path = resolve_collision(fs, cwd, &new_path); 173 | let rename_prefix = if flags.dry_run { 174 | "would rename" 175 | } else { 176 | "rename" 177 | }; 178 | let msg = format!( 179 | "{} '{}' -> '{}'", 180 | rename_prefix, 181 | path.display(), 182 | new_path.display() 183 | ); 184 | if should_prompt { 185 | // Interactively prompt whether to rename the file, skipping 186 | // if the user says no 187 | let msg = format!("{}?", msg); 188 | if !prompt_default(msg, false)? { 189 | return Ok(()); 190 | } 191 | } else { 192 | // Log rename non-interactively 193 | println!("{}", msg); 194 | } 195 | 196 | fs.rename(cwd.join(path), cwd.join(new_path))?; 197 | Ok(()) 198 | } 199 | 200 | /// Split, modify, and re-merge filename to increment the 201 | /// collision-resolving number, or create it if non-existent 202 | fn inc_filename_num(filename: &str) -> String { 203 | let FilenameParts { stem, num, ext } = FilenameParts::from_filename(filename); 204 | let num = match num { 205 | Some(val) => Some(val + 1), 206 | None => Some(0), 207 | }; 208 | FilenameParts { stem, num, ext }.merge() 209 | } 210 | 211 | /// Check if the target path can be written to without clobbering an 212 | /// existing file. If it can't, change it to a unique name. Note that 213 | /// this function requires that the filename is non-empty and valid 214 | /// UTF-8. 215 | fn resolve_collision(fs: &FS, cwd: &Path, path: &Path) -> PathBuf { 216 | if path_exists(fs, cwd, path) { 217 | let filename = path 218 | .file_name() 219 | .expect("filename is empty") 220 | .to_str() 221 | .expect("filename is not valid UTF-8"); 222 | let filename = inc_filename_num(filename); 223 | let path = path.with_file_name(filename); 224 | 225 | // Recursively resolve the new filename. This is how the 226 | // collision-resolving number is incremented. 227 | resolve_collision(fs, cwd, &path) 228 | } else { 229 | // File does not exist; we're done! 230 | path.to_path_buf() 231 | } 232 | } 233 | 234 | /// Returns `true` if the path points at an existing entity. 235 | fn path_exists(fs: &FS, cwd: P1, path: P2) -> bool 236 | where 237 | FS: GenFS, 238 | P1: AsRef, 239 | P2: AsRef, 240 | { 241 | let cwd = cwd.as_ref(); 242 | let path = path.as_ref(); 243 | fs.metadata(cwd.join(path)).is_ok() 244 | } 245 | 246 | fn unixize_paths(fs: &FS, cwd: &Path, paths: &[PathBuf], flags: Flags) -> Result<()> { 247 | for path in paths { 248 | unixize_path(fs, cwd, path, flags)?; 249 | } 250 | Ok(()) 251 | } 252 | 253 | /// Run `unf` with parsed command-line arguments in `opts`, returning any error 254 | fn main_opts(opts: Opts) -> Result<()> { 255 | let cwd = std::env::current_dir()?; 256 | 257 | if opts.flags.dry_run { 258 | // If using `--dry-run`, load the file tree into an in-memory filesystem 259 | // and use that instead of the real filesystem. This is required for the 260 | // collision handling to work. 261 | let fs = mem_fs::load(&opts.paths)?; 262 | unixize_paths(&fs, &cwd, &opts.paths, opts.flags) 263 | } else { 264 | let fs = rsfs::disk::FS; 265 | unixize_paths(&fs, &cwd, &opts.paths, opts.flags) 266 | } 267 | } 268 | 269 | /// Run `unf` with passed program arguments, returning any error 270 | fn try_main() -> Result<()> { 271 | main_opts(Opts::from_args()) 272 | } 273 | 274 | /// Run `unf` with passed program arguments, printing any error 275 | fn main() { 276 | if let Err(err) = try_main() { 277 | eprintln!("unf: error: {}", err); 278 | std::process::exit(1); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/mem_fs.rs: -------------------------------------------------------------------------------- 1 | //! Load a file tree into an in-memory filesystem 2 | 3 | use crate::Result; 4 | 5 | use std::path::Path; 6 | 7 | use rsfs::GenFS; 8 | 9 | /// Load file or directory from `path` into `fs`, including all children. 10 | fn load_insert(fs: &rsfs::mem::FS, path: &Path) -> Result<()> { 11 | if path.is_dir() { 12 | fs.create_dir(path)?; 13 | 14 | // Load children 15 | for ent in path.read_dir()? { 16 | let path = ent?.path(); 17 | load_insert(fs, &path)?; 18 | } 19 | } else { 20 | fs.create_file(path)?; 21 | } 22 | Ok(()) 23 | } 24 | 25 | /// Load the parts of the physical filesystem referenced by `paths` into a new 26 | /// in-memory filesystem. After the `paths` are canonicalized, all leading 27 | /// components and children will be created in the memory filesystem. 28 | /// 29 | /// ## Example 30 | /// 31 | /// A file structure 32 | /// ```text 33 | /// /tmp 34 | /// ├── a 35 | /// │   └── b 36 | /// ├── c 37 | /// └── foo 38 | /// ├── bar 39 | /// └── baz 40 | /// └── a 41 | /// ``` 42 | /// with `paths` = `["a", "foo/baz"]` and a working directory of `/tmp` would 43 | /// return an in-memory filesystem: 44 | /// ```text 45 | /// /tmp 46 | /// ├── a 47 | /// │   └── b 48 | /// └── foo 49 | /// └── baz 50 | /// └── a 51 | /// ``` 52 | pub fn load>(paths: &[P]) -> Result { 53 | let fs = rsfs::mem::FS::new(); 54 | 55 | for path in paths { 56 | let path = path.as_ref(); 57 | 58 | // Create all parents of canonicalized path 59 | let path = path.canonicalize()?; 60 | if let Some(parent) = path.parent() { 61 | fs.create_dir_all(parent)?; 62 | } 63 | 64 | // Recursively create path and children 65 | load_insert(&fs, &path)?; 66 | } 67 | 68 | Ok(fs) 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use super::*; 74 | 75 | use crate::path_exists; 76 | 77 | #[test] 78 | fn test_load() { 79 | let tmp = tempfile::TempDir::new().unwrap(); 80 | let tmp = tmp.path(); 81 | 82 | // Create file tree 83 | std::fs::create_dir_all(tmp.join("a")).unwrap(); 84 | std::fs::File::create(tmp.join("a/b")).unwrap(); 85 | std::fs::File::create(tmp.join("c")).unwrap(); 86 | std::fs::create_dir_all(tmp.join("foo/baz")).unwrap(); 87 | std::fs::File::create(tmp.join("foo/bar")).unwrap(); 88 | std::fs::File::create(tmp.join("foo/baz/a")).unwrap(); 89 | 90 | // Load file tree 91 | std::env::set_current_dir(tmp).unwrap(); 92 | let fs = load(&["a", "foo/baz"]).unwrap(); 93 | 94 | // Check in-memory filesystem 95 | 96 | assert!(!path_exists(&fs, tmp, "c")); 97 | assert!(!path_exists(&fs, tmp, "foo/bar")); 98 | 99 | assert!(path_exists(&fs, tmp, "a")); 100 | assert!(path_exists(&fs, tmp, "a/b")); 101 | assert!(path_exists(&fs, tmp, "foo/baz")); 102 | assert!(path_exists(&fs, tmp, "foo/baz/a")); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/opts.rs: -------------------------------------------------------------------------------- 1 | //! Command-line options 2 | 3 | use std::path::PathBuf; 4 | 5 | /// Parsed command-line arguments 6 | #[derive(clap::Parser, Debug)] 7 | #[structopt(about)] 8 | pub struct Opts { 9 | /// The paths of filenames to unixize 10 | #[structopt(required = true)] 11 | pub paths: Vec, 12 | 13 | /// Program flags 14 | #[structopt(flatten)] 15 | pub flags: Flags, 16 | } 17 | 18 | /// Parsed command-line flags 19 | #[derive(clap::Parser, Debug, Copy, Clone)] 20 | #[structopt(about)] 21 | pub struct Flags { 22 | /// Recursively unixize filenames in directories. If some of the specified 23 | /// paths are directories, unf will operate recursively on their contents. 24 | #[structopt(long, short)] 25 | pub recursive: bool, 26 | 27 | /// Do not interactively prompt to rename each file. 28 | #[structopt(long, short)] 29 | pub force: bool, 30 | 31 | /// Do not actually rename files. Only print the renames that would happen. 32 | #[structopt(long, short, conflicts_with = "force")] 33 | pub dry_run: bool, 34 | } 35 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::ffi::OsStr; 3 | use std::fs; 4 | use std::io; 5 | use std::os::unix::ffi::OsStrExt; 6 | use std::path::Path; 7 | use std::path::PathBuf; 8 | 9 | use tempfile::TempDir; 10 | use walkdir::WalkDir; 11 | 12 | fn run_unf( 13 | current_dir: P, 14 | args: &[S], 15 | stdin: B, 16 | expected_stdout: B, 17 | expected_stderr: B, 18 | before_paths: PS, 19 | expected_after_paths: PS, 20 | ) where 21 | B: AsRef<[u8]>, 22 | P: AsRef, 23 | PBUF: Into, 24 | S: AsRef, 25 | PS: IntoIterator, 26 | { 27 | let root = TempDir::new().unwrap(); 28 | let root = root.path(); 29 | 30 | for path in before_paths { 31 | let path = path.into(); 32 | let path = root.join(path); 33 | 34 | // Create all parents 35 | let result = fs::create_dir_all(path.parent().unwrap()); 36 | match result { 37 | Ok(()) => {} 38 | Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {} 39 | Err(err) => panic!("Failed creating parents: {}", err), 40 | } 41 | 42 | // Create path 43 | let is_file = path.file_name().unwrap().as_bytes().contains(&b'.'); 44 | if is_file { 45 | fs::File::create(path).unwrap(); 46 | } else { 47 | fs::create_dir(path).unwrap(); 48 | } 49 | } 50 | 51 | assert_cmd::Command::cargo_bin("unf") 52 | .unwrap() 53 | .current_dir(root.join(current_dir)) 54 | .args(args) 55 | .write_stdin(stdin.as_ref()) 56 | .assert() 57 | .success() 58 | .stdout(Box::leak(expected_stdout.as_ref().to_vec().into_boxed_slice()) as &[u8]) 59 | .stderr(Box::leak(expected_stderr.as_ref().to_vec().into_boxed_slice()) as &[u8]); 60 | 61 | let expected_after_paths = expected_after_paths 62 | .into_iter() 63 | .map(|path| path.into()) 64 | .collect::>(); 65 | let actual_after_paths = WalkDir::new(root) 66 | .into_iter() 67 | .map(|ent| { 68 | ent.unwrap() 69 | .into_path() 70 | .strip_prefix(root) 71 | .unwrap() 72 | .to_path_buf() 73 | }) 74 | .filter(|path| !path.as_os_str().is_empty()) 75 | .collect::>(); 76 | assert_eq!(expected_after_paths, actual_after_paths); 77 | } 78 | 79 | #[test] 80 | fn integration_test() { 81 | run_unf( 82 | ".", 83 | &["-f", "🤔😀😃😄😁😆😅emojis.txt"], 84 | "", 85 | "rename '🤔😀😃😄😁😆😅emojis.txt' -> 'thinking_grinning_smiley_smile_grin_laughing_sweat_smile_emojis.txt'\n", 86 | "", 87 | &["🤔😀😃😄😁😆😅emojis.txt"], 88 | &["thinking_grinning_smiley_smile_grin_laughing_sweat_smile_emojis.txt"], 89 | ); 90 | run_unf( 91 | ".", 92 | &["-f", "Game (Not Pirated 😉).rar"], 93 | "", 94 | "rename 'Game (Not Pirated 😉).rar' -> 'Game_Not_Pirated_wink.rar'\n", 95 | "", 96 | &["Game (Not Pirated 😉).rar"], 97 | &["Game_Not_Pirated_wink.rar"], 98 | ); 99 | run_unf( 100 | ".", 101 | &["-rf", "My Files/", "My Folder"], 102 | "", 103 | concat!( 104 | "rename 'My Files/Another Cool Photo.JPG' -> 'My Files/Another_Cool_Photo.JPG'\n", 105 | "rename 'My Files/Cool Photo.JPG' -> 'My Files/Cool_Photo.JPG'\n", 106 | "rename 'My Files/Passwords :) .txt' -> 'My Files/Passwords.txt'\n", 107 | "rename 'My Files/Wow Cool Photo.JPG' -> 'My Files/Wow_Cool_Photo.JPG'\n", 108 | "rename 'My Files/' -> 'My_Files'\n", 109 | "rename 'My Folder' -> 'My_Folder'\n", 110 | ), 111 | "", 112 | &[ 113 | "My Folder", 114 | "My Files", 115 | "My Files/Passwords :) .txt", 116 | "My Files/Another Cool Photo.JPG", 117 | "My Files/Wow Cool Photo.JPG", 118 | "My Files/Cool Photo.JPG", 119 | ], 120 | &[ 121 | "My_Folder", 122 | "My_Files", 123 | "My_Files/Passwords.txt", 124 | "My_Files/Another_Cool_Photo.JPG", 125 | "My_Files/Wow_Cool_Photo.JPG", 126 | "My_Files/Cool_Photo.JPG", 127 | ], 128 | ); 129 | run_unf( 130 | ".", 131 | &["-rd", "My Files/", "My Folder"], 132 | "", 133 | concat!( 134 | "would rename 'My Files/Another Cool Photo.JPG' -> 'My Files/Another_Cool_Photo.JPG'\n", 135 | "would rename 'My Files/Cool Photo.JPG' -> 'My Files/Cool_Photo.JPG'\n", 136 | "would rename 'My Files/Passwords :) .txt' -> 'My Files/Passwords.txt'\n", 137 | "would rename 'My Files/Wow Cool Photo.JPG' -> 'My Files/Wow_Cool_Photo.JPG'\n", 138 | "would rename 'My Files/' -> 'My_Files'\n", 139 | "would rename 'My Folder' -> 'My_Folder'\n", 140 | ), 141 | "", 142 | &[ 143 | "My Folder", 144 | "My Files", 145 | "My Files/Passwords :) .txt", 146 | "My Files/Another Cool Photo.JPG", 147 | "My Files/Wow Cool Photo.JPG", 148 | "My Files/Cool Photo.JPG", 149 | ], 150 | &[ 151 | "My Folder", 152 | "My Files", 153 | "My Files/Passwords :) .txt", 154 | "My Files/Another Cool Photo.JPG", 155 | "My Files/Wow Cool Photo.JPG", 156 | "My Files/Cool Photo.JPG", 157 | ], 158 | ); 159 | run_unf( 160 | ".", 161 | &[ 162 | "-f", 163 | "--", 164 | "--fake-flag.txt", 165 | "fake-flag.txt", 166 | "------fake-flag.txt", 167 | " fake-flag.txt", 168 | "\tfake-flag.txt", 169 | ], 170 | "", 171 | concat!( 172 | "rename '--fake-flag.txt' -> 'fake-flag_000.txt'\n", 173 | "rename '------fake-flag.txt' -> 'fake-flag_001.txt'\n", 174 | "rename ' fake-flag.txt' -> 'fake-flag_002.txt'\n", 175 | "rename '\tfake-flag.txt' -> 'fake-flag_003.txt'\n", 176 | ), 177 | "", 178 | &[ 179 | "--fake-flag.txt", 180 | "fake-flag.txt", 181 | "------fake-flag.txt", 182 | " fake-flag.txt", 183 | "\tfake-flag.txt", 184 | ], 185 | &[ 186 | "fake-flag.txt", 187 | "fake-flag_000.txt", 188 | "fake-flag_001.txt", 189 | "fake-flag_002.txt", 190 | "fake-flag_003.txt", 191 | ], 192 | ); 193 | } 194 | --------------------------------------------------------------------------------