Bulk renamer shell script - General Shell Scripting
phillbush
I have just written an interactive POSIX shell script for bulk renaming files.
I think it might be useful for more people.

When you call `bulkrename *`, it opens your favorite editor with the names of the files in the current directory for you to edit. Then, after closing the file, it renames the files to the names you edited.

`ls | bulkrename` also works.

Code:
#!/bin/sh
# bulkrename: bulk rename files using $EDITOR and temporary files
# this file in public domain

set -f -e -u

EDITOR="${EDITOR:-${VISUAL:-vi}}"

usage()
{
    echo "usage: bulkrename [file...]" >&2
    exit 1
}

# populate the files $old and $new with filenames, one per line
populatefiles()
{
    if [ $# -eq 0 ]
    then                            # run on stdin
        cat | tee "$old" > "$new"
    else                            # run on arguments
        for i
        do
            printf "%s\n" "$i" >> "$old"
            printf "%s\n" "$i" >> "$new"
        done
    fi
}

# edit the filenames in $new, test if they are valid, and rename the
# filenames in $old to the ones in $new
bulkrename()
{
    "$EDITOR" "$new" </dev/tty

    if cmp -s "$old" "$new"
    then
        echo "bulkrename: no change"
        exit 0
    fi

    if [ $(wc -l <"$new") -ne $(wc -l <"$old") ]
    then
        echo "bulkrename: number of filenames not equal to number of files"
        exit 1
    fi

    if [ "$(wc -l <"$new")" -ne "$(sort -u "$new" | wc -l)" ]
    then
        echo "bulkrename: repeated filenames to be renamed"
        exit 1
    fi

    # escape backslashes and insert 'mv ' before each line in $old
    sed -i "s/'/'\\\\''/g; s/^/mv '/; s/\$/'/" "$old"
    sed -i "s/'/'\\\\''/g; s/^/'/; s/\$/'/" "$new"

    # create file with the commands for renaming
    paste "$old" "$new" > "$cmd"

    sh -x "$cmd"
}

# remove temporary files
cleanup()
{
    rm -f "$old" "$new" "$cmd"
}

while getopts 'h' c
do
    case "$c" in
    h|*)
        usage
        ;;
    esac
done
shift $((OPTIND -1))

old=$(mktemp)
new=$(mktemp)
cmd=$(mktemp)

trap cleanup EXIT

populatefiles "$@"
bulkrename

exit 0

bulkrename(1) receives filenames from its arguments (one per argument) or from stdin (one per line) and opens an editor to edit them; then, it renames each file to the corresponding edited one.

If there are no arguments, bulkrename operates on stdin, reading one filename per line. If there are arguments, each argument is read as a filename.

The environment variables $EDITOR and $VISUAL are checked, in this order, for an editor program. If both are unset, use vi(1) by default.
jkl
(19-04-2020, 12:06 PM)phillbush Wrote: I have just written an interactive POSIX shell script for bulk renaming files.

It is always good to have more alternatives to GNU rename. (Here's mine, not a shell script though.)

(19-04-2020, 12:06 PM)phillbush Wrote: If both are unset, use vi(1) by default.

Whichever default you choose, it is safe to assume that your first actual user will have a system which does not support that.

-----------------

Looks like your script does not support directories though:

Code:
+ mv TECOC TECOC
mv: cannot move 'TECOC' to a subdirectory of itself, 'TECOC/TECOC'
phillbush
(20-04-2020, 08:04 AM)jkl Wrote: Looks like your script does not support directories though:

Code:
+ mv TECOC TECOC
mv: cannot move 'TECOC' to a subdirectory of itself, 'TECOC/TECOC'

It does work with directories, but you tried to rename a directory to itself.
I will fix that, thanks for reporting the issue!

EDIT: Here is the new, fixed version. I just had to play with the read shell built-in and with file descriptors:

Code:
#!/bin/sh
# bulkrename: bulk rename files using $EDITOR and temporary files
# this file in public domain

set -f -e -u

EDITOR="${EDITOR:-${VISUAL:-vi}}"

usage()
{
    echo "usage: bulkrename [file...]" >&2
    exit 1
}

# quote and escape filename for moving
quote()
{
    echo "$1" | sed "s/'/'\\\\''/g; s/^/'/; s/\$/'/"
}

# populate the files $old and $new with filenames, one per line
populatefiles()
{
    if [ $# -eq 0 ]
    then                            # run on stdin
        cat | tee "$old" > "$new"
    else                            # run on arguments
        for i
        do
            printf "%s\n" "$i" >> "$old"
            printf "%s\n" "$i" >> "$new"
        done
    fi
}

# edit the filenames in $new and test if they are valid
editfile()
{
    if [ "$(wc -l <"$old")" -ne "$(sort -u "$old" | wc -l)" ]
    then
        echo "bulkrename: repeated filenames to be renamed" >&2
        exit 1
    fi

    "$EDITOR" "$new" </dev/tty

    if [ $(wc -l <"$new") -ne $(wc -l <"$old") ]
    then
        echo "bulkrename: number of filenames not equal to number of files" >&2
        exit 1
    fi

    if [ "$(wc -l <"$new")" -ne "$(sort -u "$new" | wc -l)" ]
    then
        echo "bulkrename: repeated target filenames" >&2
        exit 1
    fi
}

# create $cmd file
createcmd()
{
    cat /dev/null >"$cmd"
    while read -r from <&3 && read -r to <&4
    do
        if [ "$from" != "$to" ]
        then
            from=$(quote "$from")
            to=$(quote "$to")
            printf "mv %s\t%s\n" "$from" "$to" >> "$cmd"
        fi
    done 3<"$old" 4<"$new"
}

# remove temporary files
cleanup()
{
    rm -f "$old" "$new" "$cmd"
}

while getopts 'h' c
do
    case "$c" in
    h|*)
        usage
        ;;
    esac
done
shift $((OPTIND -1))

old=$(mktemp)
new=$(mktemp)
cmd=$(mktemp)

trap cleanup EXIT

populatefiles "$@"
editfile
createcmd
sh -x "$cmd"

exit 0
z3bra
This is not something I do quite often to be fair, so I never really made a dedicated script for it. This falls into the "one-shot scripted operation" for me, which is pretty close to what your script is doing actually.

When faced with your problem, I generate a script and run it afterwards like so:

Code:
find . -type f -exec printf 'mv "%s" "%s"\n' {} {} \; | vis - | sh

There's no safety net of course, and I assume that "rename samename samename" will generate errors, but I'll ignore them as the script will do what I want anyway.
The advantage is that you seen the actual script that will be executed, rather than having the "mv" command added afterwards. What You See Is What You Run, hehe !

That is a good use of `cmp` there so kudos for that ! You don't get to use it that often ;)
phillbush
(20-04-2020, 10:21 AM)z3bra Wrote: When faced with your problem, I generate a script and run it afterwards like so:

Code:
find . -type f -exec printf 'mv "%s" "%s"\n' {} {} \; | vis - | sh

That's basically what my script does, but with more tests.
Basically, I run this one liner as:
Code:
ls | bulkrename
Which does the same thing, but I only have to edit the new filename, not the entire mv command.

I also use bulkrename as a command in my filemanager, lf, so I visually select the files to rename and type `:bulkrename`.

(20-04-2020, 10:21 AM)z3bra Wrote: That is a good use of `cmp` there so kudos for that ! You don't get to use it that often ;)
Yeah, most people don't know cmp and use diff for that cases...
z3bra
I finally remember what your tool made me think of ! vidir(1) from moreutils !

It opens the "content" of a directory in $EDITOR, end "commits" the changes when you quit, which means that your can rename entries, delete them or create new ones.

And btw, moreutils should be installed everywhere, be it only for "sponge(1)"…
jkl
(20-04-2020, 09:26 AM)phillbush Wrote: EDIT: Here is the new, fixed version.

Tested with Schily's latest bosh shell: Works just fine. :)
phillbush
(20-04-2020, 03:46 PM)jkl Wrote: Tested with Schily's latest bosh shell: Works just fine. :)

Thanks for testing! I'm really thankful of that.

(20-04-2020, 03:45 PM)z3bra Wrote: And btw, moreutils should be installed everywhere, be it only for "sponge(1)"…

Moreutils is indeed very useful. I use ifne(1) for a dmenu of manpages. It makes zathura not open if the input is empty.

Code:
man -k . | sort -t ' ' -k2,2 -k1,1 | dmenu -i -l 8 -p man: | sed -E 's/^([^        ]+) \(([^       ]+)\).*/\2 \1/' | xargs -r man -Tpdf | ifne zathura -

I had no idea of the existence of vidir(1), even though I have moreutils installed.
I only have moreutils installed for ifne, but now, after you have opened my eyes, I will check the other utilities of its.

For sponge(1), I have yet another shell script that do changes inplace:
Code:
#!/bin/sh
# inplace: make changes from file inplace

usage() {
    echo "usage: inplace file cmd [args...]" 1>&2
    exit 2
}

[ "$#" -le 1 ] && usage

file="$1"
shift

new=$(mktemp)
old=$(mktemp)

trap 'rm -f $new $old; exit 1' EXIT     # clean up files

if "$@" <"$file" >"$new"                # collect input
then
    cp "$file" "$old"                   # save original file
    trap '' EXIT                        # we are commited, ignore signals
    cp "$new" "$file"
else
    echo "inplace: $1 failed, $file unchanged" 1>&2
    exit 1
fi

rm -f $new $old

exit 0

So the following two one-liners are equivalent:
Code:
inplace file sed ...
sed ... <file >file.tmp && mv -f file.tmp file

inplace(1) is based on overwrite(1), a script I found on the book The UNIX Programming Environment.
ckester
(20-04-2020, 03:45 PM)z3bra Wrote: I finally remember what your tool made me think of ! vidir(1) from moreutils !

It opens the "content" of a directory in $EDITOR, end "commits" the changes when you quit, which means that your can rename entries, delete them or create new ones.

And btw, moreutils should be installed everywhere, be it only for "sponge(1)"…

I originally installed moreutils years ago precisely because I wanted something like sponge. But oddly enough, the thing from the suite that I've used more than any other is vidir.

Next most used is probably parallel, which I prefer over the GNU utility by the same name.

(Re moreutils-parallel vs. GNU-parallel, see this interesting Debian bug report & discussion: https://bugs.debian.org/cgi-bin/bugrepor...bug=597050. The final message from Joey Hess is particularly amusing.)




Members  |  Stats  |  Night Mode