How to randomize filenames recursively

command linefilename

I already know how to do it in a given directory:

for file in *.png; do
    mv -- "$file" "$RANDOM-$RANDOM-$RANDOM.png"
done

How do I turn this into a recursive command?

Best Answer

From GNU Bash manual - The Shopt Builtin:

globstar

If set, the pattern ‘**’ used in a filename expansion context will match all files and zero or more directories and subdirectories. If the pattern is followed by a ‘/’, only directories and subdirectories match.

So, in a pure-Bash fashion, I'd do:

shopt -s globstar

for f in **/*.png; do
    base_path="${f%/*}"

    [ "${base_path}" = "${f}" ] &&
        base_path= ||
        base_path="${base_path}/"

    mv "${f}" "${base_path}${RANDOM}-${RANDOM}-${RANDOM}.png"
done
  • ${f%/*}: will expand to what's left after having removed everything until a / character is found (including the / character) from $f, starting from the end (basically, it will expand to $f's base path), or to the value of $f if the pattern didn't match (e.g., in case of a .png file in the current working directory)
  • The section following base_path="${f%/*}" and preceding mv "${f}" "${base_path}${RANDOM}-${RANDOM}-${RANDOM}.png" will set $base_path to an empty string if the value of $base_path is stringwise-equal to $f, or will add a trailing / to $base_path otherwise
$ tree                                                                                
.                                                                                                                      
├── foo                                                                                                                
│   ├── 1.png                                                                                                          
│   └── bar                                                                                                            
│       ├── 2.png                                                                                                      
│       └── baz                                                                                                        
│           └── 3.png                                                                                                  
└── script.sh                                                                                                          
                                                        
4 directories, 4 files
$ shopt -s globstar
$ for f in **/*.png; do
    base_path="${f%/*}"

    [ "${base_path}" = "${f}" ] &&
        base_path= ||
        base_path="${base_path}/"

    mv "${f}" "${base_path}${RANDOM}-${RANDOM}-${RANDOM}.png"
done
$ tree
.
├── foo
│   ├── 21462-21532-6024.png
│   └── bar
│       ├── 8568-7432-8514.png
│       └── baz
│           └── 19171-25385-32563.png
└── script.sh

4 directories, 4 files

In Zsh, you'd be able to do the exact same without setting any shell option:

for f in **/*.png; do
    base_path="${f%/*}"

    [ "${base_path}" = "${f}" ] &&
        base_path= ||
        base_path="${base_path}/"

    mv "${f}" "${base_path}${RANDOM}-${RANDOM}-${RANDOM}.png"
done

If you want $RANDOM to be something other than a number in the 0-32767 range, you can leverage /dev/urandom to generate custom random values.

For example, to generate a random alphanumeric string of length 5:

tr -dc '[:alnum:]' </dev/urandom | fold -w5 | head -n1
  • tr -dc '[:alnum:]' </dev/urandom: will read undefinetly from /dev/urandom, printing only characters in the character set [:alnum:] (equivalent to [0-9A-Za-z], hence printing only digits and upper/lower-case alphabetical characters)
  • fold -w5: will split the output of tr in lines of length 5
  • head -n1: will print the first line, immediately closing the pipe

See man tr for more options.

Applying the above to your command (making tr -dc '[:alnum:]' </dev/urandom | fold -w5 | head -n1 into a function, to avoid awkwardly calling it 3 times and setting 3 $rand_n variables):

function rand() {
    tr -dc '[:alnum:]' </dev/urandom | fold -w5 | head -n1
}

shopt -s globstar

for f in **/*.png; do
    base_path="${f%/*}"

    [ "${base_path}" = "${f}" ] &&
        base_path= ||
        base_path="${base_path}/"

    mv "${f}" "${base_path}"$(rand)"-"$(rand)"-"$(rand)".png"
done
$ function rand() {
    tr -dc '[:alnum:]' </dev/urandom | fold -w5 | head -n1
}
$ shopt -s globstar
$ for f in **/*.png; do
    base_path="${f%/*}"

    [ "${base_path}" = "${f}" ] &&
        base_path= ||
        base_path="${base_path}/"

    mv "${f}" "${base_path}"$(rand)"-"$(rand)"-"$(rand)".png"
done
$ tree
.
├── foo
│   ├── 5dL7N-1DHgd-YV6Uw.png
│   └── bar
│       ├── 2nwhr-N0YpM-1ABn0.png
│       └── baz
│           └── IiohU-JTW1k-4PgFk.png
└── script.sh

4 directories, 4 files