├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── screencast.gif ├── test └── test_vimv.bats └── vimv /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/bats"] 2 | path = test/bats 3 | url = https://github.com/bats-core/bats-core.git 4 | [submodule "test/test_helper/bats-support"] 5 | path = test/test_helper/bats-support 6 | url = https://github.com/bats-core/bats-support.git 7 | [submodule "test/test_helper/bats-assert"] 8 | path = test/test_helper/bats-assert 9 | url = https://github.com/bats-core/bats-assert.git 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Thameera Senanayaka 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | clean: 4 | 5 | install: all 6 | mkdir -p ${DESTDIR}${PREFIX}/bin 7 | cp -f vimv ${DESTDIR}${PREFIX}/bin 8 | chmod 755 ${DESTDIR}${PREFIX}/bin/vimv 9 | 10 | uninstall: 11 | rm -f ${DESTDIR}${PREFIX}/bin/vimv 12 | 13 | .PHONY: all clean install uninstall 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vimv 2 | 3 | vimv is a terminal-based file rename utility that lets you easily mass-rename files using Vim. 4 | 5 | ## Installing 6 | 7 | 1. For the current user: 8 | ``` 9 | curl https://raw.githubusercontent.com/thameera/vimv/master/vimv > ~/.local/bin/vimv && chmod +755 ~/.local/bin/vimv 10 | ``` 11 | 2. For the current system: 12 | ``` 13 | sudo PREFIX=/usr/local make install 14 | ``` 15 | 16 | Or simply copy the `vimv` file to a location in your `$PATH` and make it executable. 17 | 18 | ## Usage 19 | 20 | 1. Go to a directory and enter `vimv` with optionally, a list of files to rename. 21 | 2. A Vim window will be opened with names of all files. 22 | 3. Use Vim's text editing features to edit the names of files. For example, search and replace a particular string, or use visual selection to delete a block. 23 | 4. Save and exit. Your files should be renamed now. 24 | 25 | ## Other features 26 | 27 | * If you want to list only a group of files, you can pass them as an argument. eg: `vimv *.mp4` 28 | * If you have an `$EDITOR` environment variable set, vimv will use its value by default. 29 | * If you are inside a Git directory, vimv will use `git mv` (instead of `mv`) to rename the files. 30 | * You can use `/some/path/filename` format to move the file elsewhere during renaming. If the path is non-existent, it will be automatically created before moving. 31 | 32 | ## Screencast 33 | 34 | ![alt text](screencast.gif "vimv in action") 35 | 36 | ## Gotchas 37 | 38 | Don't delete or swap the lines while in Vim or things will get ugly. 39 | 40 | ## Running tests 41 | 42 | Tests are written using [bats](https://github.com/bats-core/bats-core). To run the tests: 43 | 44 | ```sh 45 | git submodule update --init 46 | test/test_vimv.bats 47 | ``` 48 | -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thameera/vimv/1504303d4820604c60bd432f84d8a3fcf7d15c06/screencast.gif -------------------------------------------------------------------------------- /test/test_vimv.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | declare TMPDIR 4 | 5 | setup() { 6 | load 'test_helper/bats-support/load' 7 | load 'test_helper/bats-assert/load' 8 | 9 | # Get directory of test file 10 | DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" 11 | 12 | # Temp dir to create files in 13 | TMPDIR=$(mktemp -d) 14 | cd "$TMPDIR" 15 | 16 | PATH="$DIR/..:$TMPDIR:$PATH" 17 | } 18 | 19 | teardown() { 20 | rm -rf "$TMPDIR" 21 | } 22 | 23 | @test "Basic renaming" { 24 | touch file1 file2 25 | 26 | # A mock EDITOR that renames file1 to renamed_file1 and file2 to renamed_file2 27 | MOCK_EDITOR=$(cat < mock_editor 34 | chmod +x mock_editor 35 | 36 | run env EDITOR="mock_editor" vimv 37 | 38 | # Test that the files were renamed 39 | [ -e renamed_file1 ] && [ -e renamed_file2 ] 40 | [ ! -e file1 ] && [ ! -e file2 ] 41 | 42 | # Test that the output is correct 43 | assert_output "2 files renamed." 44 | } 45 | 46 | @test "Argument handling" { 47 | touch file1.mp4 file2.mp4 file3.txt 48 | 49 | MOCK_EDITOR=$(cat < mock_editor 56 | chmod +x mock_editor 57 | 58 | run env EDITOR="mock_editor" vimv *.mp4 59 | 60 | # Test that the files were renamed 61 | [ -e renamed_file1.mp4 ] && [ -e renamed_file2.mp4 ] 62 | [ ! -e file1.mp4 ] && [ ! -e file2.mp4 ] 63 | 64 | # The txt file should be untouched 65 | [ -e file3.txt ] 66 | 67 | assert_output "2 files renamed." 68 | } 69 | 70 | @test "Directory creation with renamed files" { 71 | touch file1 file2 72 | 73 | MOCK_EDITOR=$(cat < mock_editor 80 | chmod +x mock_editor 81 | 82 | run env EDITOR="mock_editor" vimv 83 | 84 | # Test that directories were created and files were properly renamed 85 | [ -e dir1/renamed_file1 ] 86 | [ -e dir2/subdir/renamed_file2 ] 87 | [ ! -e file1 ] && [ ! -e file2 ] 88 | 89 | assert_output "2 files renamed." 90 | } 91 | 92 | @test "Git integration" { 93 | # Initialize a git repo and add some files 94 | git init . 95 | touch git_tracked_file1 git_tracked_file2 untracked_file 96 | git add git_tracked_file1 git_tracked_file2 97 | git config --local user.email "test@example.com" 98 | git config --local user.name "Test User" 99 | git commit -m "Initial commit" 100 | 101 | MOCK_EDITOR=$(cat < mock_editor 108 | chmod +x mock_editor 109 | 110 | run env EDITOR="mock_editor" vimv 111 | assert_output "3 files renamed." 112 | 113 | # Test that files were renamed 114 | [ -e renamed_git_file1 ] && [ -e renamed_git_file2 ] && [ -e renamed_untracked_file ] 115 | [ ! -e git_tracked_file1 ] && [ ! -e git_tracked_file2 ] && [ ! -e untracked_file ] 116 | 117 | # Verify git-tracked files were renamed with git mv by checking git status 118 | run git ls-files 119 | assert_line "renamed_git_file1" 120 | assert_line "renamed_git_file2" 121 | 122 | # Ensure the untracked file is still not tracked 123 | run bash -c "git ls-files | grep -q renamed_untracked_file" 124 | [ "$status" -ne 0 ] 125 | } 126 | -------------------------------------------------------------------------------- /vimv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # Lists the current directory's files in Vim, so you can edit it and save to rename them 5 | # USAGE: vimv [file1 file2] 6 | # https://github.com/thameera/vimv 7 | 8 | declare -r FILENAMES_FILE=$(mktemp "${TMPDIR:-/tmp}/vimv.XXXXXX") 9 | 10 | trap '{ rm -f "${FILENAMES_FILE}" ; }' EXIT 11 | 12 | if [ $# -ne 0 ]; then 13 | src=( "$@" ) 14 | else 15 | IFS=$'\r\n' GLOBIGNORE='*' command eval 'src=($(ls))' 16 | fi 17 | 18 | for ((i=0;i<${#src[@]};++i)); do 19 | echo "${src[i]}" >> "${FILENAMES_FILE}" 20 | done 21 | 22 | ${EDITOR:-vi} "${FILENAMES_FILE}" 23 | 24 | IFS=$'\r\n' GLOBIGNORE='*' command eval 'dest=($(cat "${FILENAMES_FILE}"))' 25 | 26 | if (( ${#src[@]} != ${#dest[@]} )); then 27 | echo "WARN: Number of files changed. Did you delete a line by accident? Aborting.." >&2 28 | exit 1 29 | fi 30 | 31 | declare -i count=0 32 | for ((i=0;i<${#src[@]};++i)); do 33 | if [ "${src[i]}" != "${dest[i]}" ]; then 34 | mkdir -p "$(dirname "${dest[i]}")" 35 | if git ls-files --error-unmatch "${src[i]}" > /dev/null 2>&1; then 36 | git mv -- "${src[i]}" "${dest[i]}" 37 | else 38 | mv -- "${src[i]}" "${dest[i]}" 39 | fi 40 | ((++count)) 41 | fi 42 | done 43 | 44 | echo "$count" files renamed. 45 | --------------------------------------------------------------------------------