Inspired by a recent question to rename files by editing the list of names in an editor, I put together a similar script.
Why: if you need to perform some complex renames that are not easy to formulate with patterns (as with the rename.pl
utility), it might be handy to be able to edit the list of names in a text editor, where you will see the exact names you will get.
Features:
- Edit names in a text editor
- Use the names specified as command line arguments, or else the files and directories in the current directory (resolve
*
) - Use a sensible default text editor
- According to
man bash
, READLINE commands try$VISUAL
or else$EDITOR
-> looks like a good example to follow. - Abort if cannot determine a suitable editor.
- According to
- Abort (do not rename anything) if the editor exits with error
- Paths containing newlines are explicitly unsupported
- Perform basic sanity checks: the edited text should have the same number of lines as the paths to rename
Here's the script:
#!/usr/bin/env bash
#
# SCRIPT: mv-many.sh
# AUTHOR: Janos Gyerik <[email protected]>
# DATE: 2019年07月27日
#
# PLATFORM: Not platform dependent
#
# PURPOSE: Rename files and directories by editing their names in $VISUAL or $EDITOR
#
set -euo pipefail
usage() {
local exitcode=0
if [[ $# != 0 ]]; then
echo "$*" >&2
exitcode=1
fi
cat << EOF
Usage: 0ドル [OPTION]... [FILES]
Rename files and directories by editing their names in $editor
Specify the paths to rename, or else * will be used by default.
Limitations: the paths must not contain newline characters.
EOF
if [[ $editor == *vim ]]; then
echo "Tip: to abort editing in $editor, exit with :cq command."
fi
cat << "EOF"
Options:
-h, --help Print this help
EOF
exit "$exitcode"
}
fatal() {
echo "Error: $*" >&2
exit 1
}
find_editor() {
# Following READLINE conventions, try VISUAL first and then EDITOR
if [[ ${VISUAL+x} ]]; then
echo "$VISUAL"
return
fi
# shellcheck disable=SC2153
if [[ ${EDITOR+x} ]]; then
echo "$EDITOR"
return
fi
fatal 'could not determine editor to use, please set VISUAL or EDITOR; aborting.'
}
editor=$(find_editor)
oldnames=()
while [[ $# != 0 ]]; do
case 1ドル in
-h|--help) usage ;;
-|-?*) usage "Unknown option: 1ドル" ;;
*) oldnames+=("1ドル") ;;
esac
shift
done
work=$(mktemp)
trap 'rm -f "$work"' EXIT
if [[ ${#oldnames[@]} == 0 ]]; then
oldnames=(*)
fi
printf '%s\n' "${oldnames[@]}" > "$work"
"$editor" "$work" || fatal "vim exited with error; aborting without renaming."
mapfile -t newnames < "$work"
[[ "${#oldnames[@]}" == "${#newnames[@]}" ]] ||
fatal "expected ${#oldnames[@]} lines in the file, got ${#newnames[@]}; aborting without renaming."
for ((i = 0; i < ${#oldnames[@]}; i++)); do
old=${oldnames[i]}
new=${newnames[i]}
if [[ "$old" != "$new" ]]; then
mv -vi "$old" "$new"
fi
done
What do you think? I'm looking for any and all kinds of comments, suggestions, critique.
1 Answer 1
Looks pretty good to me, and to Shellcheck too.
find_editor
is quite long-winded; it could be a one-liner:
editor=${VISUAL:-${EDITOR:?could not determine editor to use, please set VISUAL or EDITOR; aborting.}}
There's a couple of Vim-isms remaining. The test if [[ $editor == *vim ]]
could be generalised to a case "$editor" in ... esac
to support adding more editor hints, but more significantly, we have an error message that mentions vim
where $editor
would be more appropriate.
The current implementation is very simplistic when the new names might overlap with the old names. We might need a topological sort to perform the renames in the correct order in that case.
Perhaps it should be an error if the user asks for two or more files to be moved to the same target name?
-
\$\begingroup\$ Thanks! Great point about multiple renames to the same file, that could lead to data loss. \$\endgroup\$janos– janos2022年03月02日 15:44:17 +00:00Commented Mar 2, 2022 at 15:44