├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── license.txt ├── readme.md └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /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 = "arguably" 7 | version = "2.2.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "57426e3d17f474fa14c859d6dc91caebce3d83682313d0920fd04d3018ee6e32" 10 | 11 | [[package]] 12 | name = "atty" 13 | version = "0.2.14" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 16 | dependencies = [ 17 | "hermit-abi", 18 | "libc", 19 | "winapi", 20 | ] 21 | 22 | [[package]] 23 | name = "autocfg" 24 | version = "1.1.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 27 | 28 | [[package]] 29 | name = "bitflags" 30 | version = "1.3.2" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 33 | 34 | [[package]] 35 | name = "cfg-if" 36 | version = "1.0.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 39 | 40 | [[package]] 41 | name = "chrono" 42 | version = "0.4.19" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 45 | dependencies = [ 46 | "libc", 47 | "num-integer", 48 | "num-traits", 49 | "time", 50 | "winapi", 51 | ] 52 | 53 | [[package]] 54 | name = "colored" 55 | version = "2.0.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 58 | dependencies = [ 59 | "atty", 60 | "lazy_static", 61 | "winapi", 62 | ] 63 | 64 | [[package]] 65 | name = "edit" 66 | version = "0.1.4" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "c562aa71f7bc691fde4c6bf5f93ae5a5298b617c2eb44c76c87832299a17fbb4" 69 | dependencies = [ 70 | "tempfile", 71 | "which", 72 | ] 73 | 74 | [[package]] 75 | name = "either" 76 | version = "1.7.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" 79 | 80 | [[package]] 81 | name = "fastrand" 82 | version = "1.7.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 85 | dependencies = [ 86 | "instant", 87 | ] 88 | 89 | [[package]] 90 | name = "form_urlencoded" 91 | version = "1.0.1" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 94 | dependencies = [ 95 | "matches", 96 | "percent-encoding", 97 | ] 98 | 99 | [[package]] 100 | name = "getrandom" 101 | version = "0.2.7" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 104 | dependencies = [ 105 | "cfg-if", 106 | "libc", 107 | "wasi 0.11.0+wasi-snapshot-preview1", 108 | ] 109 | 110 | [[package]] 111 | name = "hermit-abi" 112 | version = "0.1.19" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 115 | dependencies = [ 116 | "libc", 117 | ] 118 | 119 | [[package]] 120 | name = "idna" 121 | version = "0.2.3" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 124 | dependencies = [ 125 | "matches", 126 | "unicode-bidi", 127 | "unicode-normalization", 128 | ] 129 | 130 | [[package]] 131 | name = "instant" 132 | version = "0.1.12" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 135 | dependencies = [ 136 | "cfg-if", 137 | ] 138 | 139 | [[package]] 140 | name = "lazy_static" 141 | version = "1.4.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 144 | 145 | [[package]] 146 | name = "libc" 147 | version = "0.2.126" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 150 | 151 | [[package]] 152 | name = "log" 153 | version = "0.4.17" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 156 | dependencies = [ 157 | "cfg-if", 158 | ] 159 | 160 | [[package]] 161 | name = "malloc_buf" 162 | version = "0.0.6" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 165 | dependencies = [ 166 | "libc", 167 | ] 168 | 169 | [[package]] 170 | name = "matches" 171 | version = "0.1.9" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 174 | 175 | [[package]] 176 | name = "num-integer" 177 | version = "0.1.45" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 180 | dependencies = [ 181 | "autocfg", 182 | "num-traits", 183 | ] 184 | 185 | [[package]] 186 | name = "num-traits" 187 | version = "0.2.15" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 190 | dependencies = [ 191 | "autocfg", 192 | ] 193 | 194 | [[package]] 195 | name = "objc" 196 | version = "0.2.7" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 199 | dependencies = [ 200 | "malloc_buf", 201 | ] 202 | 203 | [[package]] 204 | name = "once_cell" 205 | version = "1.13.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 208 | 209 | [[package]] 210 | name = "percent-encoding" 211 | version = "2.1.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 214 | 215 | [[package]] 216 | name = "ppv-lite86" 217 | version = "0.2.16" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 220 | 221 | [[package]] 222 | name = "rand" 223 | version = "0.8.5" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 226 | dependencies = [ 227 | "libc", 228 | "rand_chacha", 229 | "rand_core", 230 | ] 231 | 232 | [[package]] 233 | name = "rand_chacha" 234 | version = "0.3.1" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 237 | dependencies = [ 238 | "ppv-lite86", 239 | "rand_core", 240 | ] 241 | 242 | [[package]] 243 | name = "rand_core" 244 | version = "0.6.3" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 247 | dependencies = [ 248 | "getrandom", 249 | ] 250 | 251 | [[package]] 252 | name = "redox_syscall" 253 | version = "0.2.13" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 256 | dependencies = [ 257 | "bitflags", 258 | ] 259 | 260 | [[package]] 261 | name = "remove_dir_all" 262 | version = "0.5.3" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 265 | dependencies = [ 266 | "winapi", 267 | ] 268 | 269 | [[package]] 270 | name = "scopeguard" 271 | version = "1.1.0" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 274 | 275 | [[package]] 276 | name = "tempfile" 277 | version = "3.3.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 280 | dependencies = [ 281 | "cfg-if", 282 | "fastrand", 283 | "libc", 284 | "redox_syscall", 285 | "remove_dir_all", 286 | "winapi", 287 | ] 288 | 289 | [[package]] 290 | name = "time" 291 | version = "0.1.44" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 294 | dependencies = [ 295 | "libc", 296 | "wasi 0.10.0+wasi-snapshot-preview1", 297 | "winapi", 298 | ] 299 | 300 | [[package]] 301 | name = "tinyvec" 302 | version = "1.6.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 305 | dependencies = [ 306 | "tinyvec_macros", 307 | ] 308 | 309 | [[package]] 310 | name = "tinyvec_macros" 311 | version = "0.1.0" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 314 | 315 | [[package]] 316 | name = "trash" 317 | version = "3.0.1" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "a27b2a127810fceb959593bbc6c7b8e0282c2d318d76f0749252197c52a1dd0c" 320 | dependencies = [ 321 | "chrono", 322 | "libc", 323 | "log", 324 | "objc", 325 | "once_cell", 326 | "scopeguard", 327 | "url", 328 | "windows", 329 | ] 330 | 331 | [[package]] 332 | name = "unicode-bidi" 333 | version = "0.3.8" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 336 | 337 | [[package]] 338 | name = "unicode-normalization" 339 | version = "0.1.21" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" 342 | dependencies = [ 343 | "tinyvec", 344 | ] 345 | 346 | [[package]] 347 | name = "url" 348 | version = "2.2.2" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 351 | dependencies = [ 352 | "form_urlencoded", 353 | "idna", 354 | "matches", 355 | "percent-encoding", 356 | ] 357 | 358 | [[package]] 359 | name = "vimv" 360 | version = "3.1.0" 361 | dependencies = [ 362 | "arguably", 363 | "colored", 364 | "edit", 365 | "rand", 366 | "trash", 367 | ] 368 | 369 | [[package]] 370 | name = "wasi" 371 | version = "0.10.0+wasi-snapshot-preview1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 374 | 375 | [[package]] 376 | name = "wasi" 377 | version = "0.11.0+wasi-snapshot-preview1" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 380 | 381 | [[package]] 382 | name = "which" 383 | version = "4.2.5" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" 386 | dependencies = [ 387 | "either", 388 | "lazy_static", 389 | "libc", 390 | ] 391 | 392 | [[package]] 393 | name = "winapi" 394 | version = "0.3.9" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 397 | dependencies = [ 398 | "winapi-i686-pc-windows-gnu", 399 | "winapi-x86_64-pc-windows-gnu", 400 | ] 401 | 402 | [[package]] 403 | name = "winapi-i686-pc-windows-gnu" 404 | version = "0.4.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 407 | 408 | [[package]] 409 | name = "winapi-x86_64-pc-windows-gnu" 410 | version = "0.4.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 413 | 414 | [[package]] 415 | name = "windows" 416 | version = "0.44.0" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" 419 | dependencies = [ 420 | "windows-targets", 421 | ] 422 | 423 | [[package]] 424 | name = "windows-targets" 425 | version = "0.42.2" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 428 | dependencies = [ 429 | "windows_aarch64_gnullvm", 430 | "windows_aarch64_msvc", 431 | "windows_i686_gnu", 432 | "windows_i686_msvc", 433 | "windows_x86_64_gnu", 434 | "windows_x86_64_gnullvm", 435 | "windows_x86_64_msvc", 436 | ] 437 | 438 | [[package]] 439 | name = "windows_aarch64_gnullvm" 440 | version = "0.42.2" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 443 | 444 | [[package]] 445 | name = "windows_aarch64_msvc" 446 | version = "0.42.2" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 449 | 450 | [[package]] 451 | name = "windows_i686_gnu" 452 | version = "0.42.2" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 455 | 456 | [[package]] 457 | name = "windows_i686_msvc" 458 | version = "0.42.2" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 461 | 462 | [[package]] 463 | name = "windows_x86_64_gnu" 464 | version = "0.42.2" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 467 | 468 | [[package]] 469 | name = "windows_x86_64_gnullvm" 470 | version = "0.42.2" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 473 | 474 | [[package]] 475 | name = "windows_x86_64_msvc" 476 | version = "0.42.2" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 479 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vimv" 3 | version = "3.1.0" 4 | authors = ["Darren Mulholland "] 5 | edition = "2021" 6 | description = "A command line utility for batch-renaming files using a text editor." 7 | license = "0BSD" 8 | homepage = "https://www.dmulholl.com/dev/vimv.html" 9 | repository = "https://github.com/dmulholl/vimv" 10 | readme = "readme.md" 11 | 12 | [dependencies] 13 | arguably = "2.2.0" 14 | edit = "0.1.4" 15 | trash = "3.0.1" 16 | rand = "0.8.5" 17 | colored = "2.0" 18 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Permission to use, copy, modify, and/or distribute this software for any purpose 2 | with or without fee is hereby granted. 3 | 4 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 5 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 6 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 7 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 8 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 9 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 10 | THIS SOFTWARE. 11 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Vimv 2 | 3 | [1]: https://www.dmulholl.com/dev/vimv.html 4 | [2]: https://crates.io/crates/vimv 5 | [3]: https://www.rust-lang.org/tools/install 6 | 7 | 8 | 9 | This simple command line utility lets you batch-rename files from the comfort of your favourite text editor. You specify the files to be renamed as arguments, e.g. 10 | 11 | vimv *.mp3 12 | 13 | The list of files will be opened in the editor specified by the `$EDITOR` environment variable, one filename per line. Edit the list, save, and exit. The files will be renamed to the edited filenames. 14 | 15 | 16 | 17 | ## Installation 18 | 19 | Vimv is written in Rust — if you have a Rust compiler available, you can install it directly from the package index using `cargo`: 20 | 21 | cargo install vimv 22 | 23 | If you don't have a Rust compiler available, you can easily install one by following the instructions [here][3]. You don't need any knowledge of Rust to build, install, or use `vimv`. 24 | 25 | 26 | 27 | ### AUR 28 | 29 | If you are using Arch Linux, you can get `vimv` from the AUR: 30 | 31 | yay -S vimv 32 | 33 | Or 34 | 35 | paru -S vimv 36 | 37 | 38 | 39 | ## Interface 40 | 41 | Run `vimv --help` to view the command line help: 42 | 43 | Usage: vimv [files] 44 | 45 | This utility lets you batch-rename files using a text editor. 46 | Files to be renamed should be supplied as a list of command-line 47 | arguments, e.g. 48 | 49 | $ vimv *.mp3 50 | 51 | The list of files will be opened in the editor specified by the 52 | $EDITOR environment variable, one filename per line. Edit the 53 | list, save, and exit. The files will be renamed to the edited 54 | filenames. Directories along the renamed paths will be created 55 | as required. 56 | 57 | If the input file list is empty, Vimv defaults to listing the 58 | contents of the current working directory. 59 | 60 | Vimv supports cycle-renaming. You can safely rename A to B, B to 61 | C, and C to A in a single operation. 62 | 63 | Use the --force flag to overwrite existing files that aren't part 64 | of a renaming cycle. (Existing directories are never overwritten. 65 | If you attempt to overwrite a directory, the program will exit with 66 | an error message and a non-zero status code.) 67 | 68 | You can delete a file or directory by prefixing its name with a 69 | '#' symbol. Deleted files and directories are moved to the system's 70 | trash/recycle bin. 71 | 72 | Arguments: 73 | [files] List of files to rename. 74 | 75 | Options: 76 | -e, --editor Specify the editor to use. 77 | 78 | Flags: 79 | -f, --force Overwrite existing files. 80 | -h, --help Print this help text. 81 | -q, --quiet Quiet mode -- only report errors. 82 | -s, --stdin Read the list of input files from stdin. 83 | -v, --version Print the version number. 84 | 85 | Vimv simply ignores any filenames that haven't been changed so you don't have to be overly fussy 86 | about specifying its input. You can run: 87 | 88 | vimv * 89 | 90 | to get a full listing of a directory's contents, change just the items you want, and Vimv will 91 | ignore the rest. 92 | 93 | 94 | 95 | ## Cycle Renaming 96 | 97 | Vimv supports cycle-renaming. You can safely rename A to B, B to C, and C to A in a single operation. 98 | 99 | 100 | 101 | ## Deleting Files 102 | 103 | You can delete a file or directory by prefixing its name with a `#` symbol. 104 | Deleted files and directories are moved to the system's trash/recycle bin. 105 | 106 | 107 | 108 | ## Graphical Editors 109 | 110 | If you want to use a graphical editor like VS Code or Sublime Text instead of a terminal editor like Vim then (depending on your operating system) you may need to add a 'wait' flag to the `$EDITOR` variable to force the editor to block, e.g. 111 | 112 | EDITOR="code -w" # for VS Code 113 | EDITOR="subl -w" # for Sublime Text 114 | EDITOR="atom -w" # for Atom 115 | 116 | The same flag can be used with the `--editor` option, e.g. 117 | 118 | vimv *.mp3 --editor "code -w" 119 | 120 | 121 | 122 | ## Piped Input 123 | 124 | You can pipe a list of filenames into Vimv from a tool like `ls` or `fd`, e.g. 125 | 126 | fd .txt | vimv --stdin 127 | 128 | Note that your editor may not appreciate inheriting a standard input stream that's connected to a pipe rather than a terminal. 129 | Graphical editors tend to handle this situation without complaint, as does Neovim in the terminal. 130 | Vim prints a warning, then works, then borks your terminal session until you run `reset`. YMMV. 131 | 132 | (Because of this inconsistent behaviour, this feature is hidden behind a `--stdin/-s` flag.) 133 | 134 | 135 | 136 | ## License 137 | 138 | Zero-Clause BSD (0BSD). 139 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use arguably::ArgParser; 2 | use std::path::Path; 3 | use std::process::exit; 4 | use std::env; 5 | use std::fs; 6 | use std::collections::HashSet; 7 | use rand::Rng; 8 | use std::io::Read; 9 | use colored::*; 10 | 11 | 12 | const HELPTEXT: &str = " 13 | Usage: vimv [files] 14 | 15 | This utility lets you batch-rename files using a text editor. Files to be 16 | renamed should be supplied as a list of command-line arguments, e.g. 17 | 18 | $ vimv *.mp3 19 | 20 | The list of files will be opened in the editor specified by the $EDITOR 21 | environment variable, one filename per line. Edit the list, save, and exit. 22 | The files will be renamed to the edited filenames. Directories along the 23 | renamed paths will be created as required. 24 | 25 | If the input file list is empty, Vimv defaults to listing the contents of 26 | the current working directory. 27 | 28 | Vimv supports cycle-renaming. You can safely rename A to B, B to C, and C 29 | to A in a single operation. 30 | 31 | Use the --force flag to overwrite existing files that aren't part of a 32 | renaming cycle. (Existing directories are never overwritten. If you attempt 33 | to overwrite a directory, the program will exit with an error message and a 34 | non-zero status code.) 35 | 36 | You can delete a file or directory by prefixing its name with a `#` symbol. 37 | Deleted files and directories are moved to the system's trash/recycle bin. 38 | 39 | Arguments: 40 | [files] List of files to rename. 41 | 42 | Options: 43 | -e, --editor Specify the editor to use. Overrides $EDITOR. 44 | 45 | Flags: 46 | -f, --force Allow overwriting existing files. 47 | -h, --help Print this help text and exit. 48 | -q, --quiet Quiet mode -- only report errors. 49 | -s, --stdin Read the list of input files from standard input. 50 | -v, --version Print the version number and exit. 51 | "; 52 | 53 | 54 | fn main() { 55 | let mut parser = ArgParser::new() 56 | .helptext(HELPTEXT) 57 | .version(env!("CARGO_PKG_VERSION")) 58 | .flag("force f") 59 | .flag("quiet q") 60 | .flag("stdin s") 61 | .option("editor e", ""); 62 | 63 | // Parse the command line arguments. 64 | if let Err(err) = parser.parse() { 65 | err.exit(); 66 | } 67 | 68 | // Use the --editor option if present to set $VISUAL. 69 | if parser.found("editor") { 70 | env::set_var("VISUAL", parser.value("editor")); 71 | } 72 | 73 | // Assemble the list of input filenames. 74 | let mut input_files: Vec = parser.args.clone(); 75 | 76 | // If no input files have been specified, use the content of the current directory. 77 | if input_files.is_empty() && !parser.found("stdin") { 78 | let current_dir = env::current_dir().unwrap_or_else(|err| { 79 | eprintln!("error: failed to locate current directory: {}", err); 80 | exit(1); 81 | }); 82 | let current_dir_iterator = fs::read_dir(current_dir).unwrap_or_else(|err| { 83 | eprintln!("error: failed to read current directory: {}", err); 84 | exit(1); 85 | }); 86 | for entry in current_dir_iterator { 87 | let entry = entry.unwrap_or_else(|err| { 88 | eprintln!("error: failed to read current directory entry: {}", err); 89 | exit(1); 90 | }); 91 | let entry_as_string = entry.file_name().into_string().unwrap_or_else(|err| { 92 | eprintln!("error: failed to decode current directory entry name: {:?}", err); 93 | exit(1); 94 | }); 95 | input_files.push(entry_as_string); 96 | } 97 | input_files.sort(); 98 | } 99 | 100 | // If the --stdin flag has been set, try reading from standard input. 101 | if parser.found("stdin") { 102 | let mut buffer = String::new(); 103 | if let Err(err) = std::io::stdin().read_to_string(&mut buffer) { 104 | eprintln!("error: failed to read filenames from standard input: {}", err); 105 | exit(1); 106 | } 107 | if !buffer.trim().is_empty() { 108 | input_files.extend(buffer.lines().map(|s| s.to_string())); 109 | } 110 | } 111 | 112 | // Bail if we have no input filenames to process. 113 | if input_files.is_empty() { 114 | exit(0); 115 | } 116 | 117 | // Sanity check - verify that no input filename begins with '#'. 118 | for input_file in &input_files { 119 | if input_file.starts_with("#") { 120 | eprintln!("error: input filenames cannot begin with '#'"); 121 | exit(1); 122 | } 123 | } 124 | 125 | // Sanity check - verify that all the input files exist. 126 | for input_file in &input_files { 127 | if !Path::new(input_file).exists() { 128 | eprintln!("error: the input file '{}' does not exist", input_file); 129 | exit(1); 130 | } 131 | } 132 | 133 | // Sanity check - verify that the input filenames are unique. 134 | let mut input_set = HashSet::new(); 135 | for input_file in &input_files { 136 | if input_set.contains(input_file) { 137 | eprintln!("error: the filename '{}' appears in the input list multiple times", input_file); 138 | exit(1); 139 | } 140 | input_set.insert(input_file); 141 | } 142 | 143 | // Fetch the output filenames from the editor. 144 | let editor_input = input_files.join("\n") + "\n"; 145 | let editor_output = match edit::edit(editor_input) { 146 | Ok(edited) => edited.trim().to_string(), 147 | Err(err) => { 148 | eprintln!("error: {}", err); 149 | exit(1); 150 | } 151 | }; 152 | let output_files: Vec = editor_output.lines().map(|s| s.to_string()).collect(); 153 | 154 | // Sanity check - verify that we have equal numbers of input and output filenames. 155 | if output_files.len() != input_files.len() { 156 | eprintln!( 157 | "error: the number of input filenames ({}) does not match the number of output filenames ({})", 158 | input_files.len(), 159 | output_files.len() 160 | ); 161 | exit(1); 162 | } 163 | 164 | // Sanity check - verify that the output filenames are unique. 165 | let mut case_sensitive_output_set = HashSet::new(); 166 | for output_file in output_files.iter().filter(|s| !s.starts_with("#")) { 167 | if case_sensitive_output_set.contains(output_file) { 168 | eprintln!("error: the filename '{}' appears in the output list multiple times", output_file); 169 | exit(1); 170 | } 171 | case_sensitive_output_set.insert(output_file); 172 | } 173 | 174 | // Sanity check - verify that the output filenames are case-insensitively unique. 175 | let mut case_insensitive_output_set = HashSet::new(); 176 | for output_file in output_files.iter().filter(|s| !s.starts_with("#")).map(|s| s.to_lowercase()) { 177 | if case_insensitive_output_set.contains(&output_file) { 178 | eprintln!( 179 | "error: the filename '{}' appears multiple times in the output list (case \ 180 | insensitively); this may be intentional but Vimv always treats this situation \ 181 | as an error to avoid accidentally overwriting files on case-insensitive \ 182 | file systems", 183 | output_file 184 | ); 185 | exit(1); 186 | } 187 | case_insensitive_output_set.insert(output_file); 188 | } 189 | 190 | // List of files to delete. 191 | let mut delete_list: Vec<&str> = Vec::new(); 192 | 193 | // List of rename operations as (src, dst) tuples. 194 | let mut rename_list: Vec<(String, String)> = Vec::new(); 195 | 196 | // Set of input files to be renamed. Used to check for cycles. 197 | let mut rename_set: HashSet = HashSet::new(); 198 | 199 | // Populate the task lists. 200 | for (input_file, output_file) in input_files.iter().zip(output_files.iter()) { 201 | if input_file == output_file { 202 | continue; 203 | } 204 | 205 | if Path::new(output_file).is_dir() { 206 | if input_files.contains(output_file) { 207 | rename_list.push((input_file.to_string(), output_file.to_string())); 208 | rename_set.insert(input_file.to_string()); 209 | continue; 210 | } 211 | eprintln!("error: cannot overwrite the existing directory '{}'", output_file); 212 | exit(1); 213 | } 214 | 215 | if output_file.starts_with("#") { 216 | delete_list.push(input_file); 217 | continue; 218 | } 219 | 220 | if Path::new(output_file).is_file() { 221 | if input_files.contains(output_file) { 222 | rename_list.push((input_file.to_string(), output_file.to_string())); 223 | rename_set.insert(input_file.to_string()); 224 | continue; 225 | } 226 | 227 | if parser.found("force") { 228 | rename_list.push((input_file.to_string(), output_file.to_string())); 229 | rename_set.insert(input_file.to_string()); 230 | continue; 231 | } 232 | 233 | eprintln!( 234 | "error: the output file '{}' already exists, use --force to overwrite it", 235 | output_file 236 | ); 237 | exit(1); 238 | } 239 | 240 | rename_list.push((input_file.to_string(), output_file.to_string())); 241 | rename_set.insert(input_file.to_string()); 242 | } 243 | 244 | // Check for cycles. If we find [src] being renamed to [dst] where [dst] is an input file that 245 | // hasn't yet been deleted or renamed, we rename [src] to [tmp] instead and later rename [tmp] 246 | // to [dst]. 247 | for i in 0..rename_list.len() { 248 | if rename_set.contains(&rename_list[i].1) { 249 | let temp_file = get_temp_filename(&rename_list[i].0); 250 | rename_list.push((temp_file.clone(), rename_list[i].1.clone())); 251 | rename_list[i].1 = temp_file 252 | } 253 | rename_set.remove(&rename_list[i].0); 254 | } 255 | 256 | // Deletion loop. We haven't made any changes to the file system up to this point. 257 | for input_file in delete_list { 258 | delete_file(input_file, parser.found("quiet")); 259 | } 260 | 261 | // Rename loop. 262 | for (input_file, output_file) in rename_list { 263 | move_file(&input_file, &output_file, parser.found("quiet")); 264 | } 265 | } 266 | 267 | 268 | // Generate a unique temporary filename. 269 | fn get_temp_filename(base: &str) -> String { 270 | let mut rng = rand::thread_rng(); 271 | for _ in 0..10 { 272 | let candidate = format!("{}.vimv_temp_{:04}", base, rng.gen_range(0..10_000)); 273 | if !Path::new(&candidate).exists() { 274 | return candidate; 275 | } 276 | } 277 | eprintln!( 278 | "error: failed to generate a unique temporary filename of the form '{}.vimv_temp_XXXX'", 279 | base 280 | ); 281 | exit(1); 282 | } 283 | 284 | 285 | // Move the specified file to the system's trash/recycle bin. 286 | fn delete_file(input_file: &str, quiet: bool) { 287 | if !quiet { 288 | println!("{} {}", "Deleting".green().bold(), input_file); 289 | } 290 | if let Err(err) = trash::delete(input_file) { 291 | eprintln!("error: cannot delete the file '{}': {}", input_file, err); 292 | exit(1); 293 | } 294 | } 295 | 296 | 297 | // Rename `input_file` to `output_file`. 298 | fn move_file(input_file: &str, output_file: &str, quiet: bool) { 299 | if !quiet { 300 | println!("{} {}", "Renaming".green().bold(), input_file); 301 | println!(" {} {}", "⮑".green().bold(), output_file); 302 | } 303 | if let Some(parent_path) = Path::new(output_file).parent() { 304 | if !parent_path.is_dir() { 305 | if let Err(err) = std::fs::create_dir_all(parent_path) { 306 | eprintln!("error: cannot create the required directory '{}': {}", parent_path.display(), err); 307 | exit(1); 308 | } 309 | } 310 | } 311 | if let Err(err) = std::fs::rename(input_file, output_file) { 312 | eprintln!("error: cannot rename the file '{}' to '{}': {}", input_file, output_file, err); 313 | exit(1); 314 | } 315 | } 316 | --------------------------------------------------------------------------------