5

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?

1
  • 2
    Just FYI -- $RANDOM-$RANDOM-$RANDOM doesn't give as much randomness as you might be expecting, even if you're already aware of $RANDOM's limited range. The details depend on the shell, but typically the internal state of the random number generator is only 32 bits, which means that there can't be more than 2³² possible distinct values for $RANDOM-$RANDOM-$RANDOM -- and there can potentially be many fewer than that, and/or some sequences that are much more probable than 1 in 2³². So depending on your needs, you may be better off making use of /dev/urandom.
    – ruakh
    Commented May 17 at 8:49

3 Answers 3

11

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 -n "${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 -n "${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
  • -n: will prevent mv from overwriting a potentially already existing file, in the (unlikely) event that the generated filename happens to match one; in this case, a message will be printed to the screen, and you'll be able to give the command another spin
$ 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 -n "${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

A note on Zsh

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 -n "${f}" "${base_path}${RANDOM}-${RANDOM}-${RANDOM}.png"
done

Extension: using a custom value instead of $RANDOM

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 -n "${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 -n "${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

Extension: dealing with multiple extensions at once

Per OP's request: here's a modified version that will deal with multiple extensions at once (note: this will deal correctly only with extensions such as .bar, extensions such as .foo.bar will need a more involved approach to be dealt with correctly):

shopt -s globstar

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

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

    ext="${f##*.}"

    mv -n "${f}" "${base_path}${RANDOM}-${RANDOM}-${RANDOM}.${ext}"
done
  • **/*.{png,gif}: will expand to **/*.png **/*.gif, ultimately matching files ending with .png and files ending with .gif
  • ${f##*.}: will expand to what's left after having removed everything until the last . character is found (including the . character) from $f, starting from the start (basically, it will expand to $f's extension)
$ tree
.
├── foo
│   ├── 1.png
│   └── bar
│       ├── 2.gif
│       └── baz
│           └── 3.png
└── script.sh

4 directories, 4 files
$ shopt -s globstar
$ for f in **/*.{png,gif}; do
    base_path="${f%/*}"

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

    ext="${f##*.}"

    mv -n "${f}" "${base_path}${RANDOM}-${RANDOM}-${RANDOM}.${ext}"
done
$ tree
.
├── foo
│   ├── 27150-25336-3117.png
│   └── bar
│       ├── 4841-14490-4418.gif
│       └── baz
│           └── 27852-28183-26777.png
└── script.sh

4 directories, 4 files
5
  • Is it possible to join my other objectives (doing the same with .gif, .jpg and so on) to this code? Commented May 17 at 8:39
  • 2
    @NunoFonseca I'll expand the first example to do that later, hold tight
    – kos
    Commented May 17 at 11:08
  • 1
    @NunoFonseca I added a version that will deal with .png and .gif files, you can change png,gif in the condition of the for loop to the arbitrary list of comma-separated extensions you need (png,gif,jpg,...) and it will deal with all of them at once
    – kos
    Commented May 18 at 4:22
  • 1
    This has the (extremely minor) risk of overwriting files if the random name generated has already been used. Your use of three separate random strings makes this even less likely, but you might as well handle it with something like rname="${base_path}${RANDOM}-${RANDOM}-${RANDOM}.${ext}"; while [[ -e "$rname" ]]; do rname="${base_path}${RANDOM}-${RANDOM}-${RANDOM}.${ext}"; done
    – terdon
    Commented May 18 at 12:07
  • @terdon You're 100% right, for now I just added -n and explained that in that case one might just give it another spin, to make the answer "right", but yeah it should be dealt with automatically, I want to take my time and refactor the whole thing better tomorrow (incorporating your suggestion) as this started as a very sleek command and it ended up, after taking care of all edge cases, pretty bloated. I want to see what else could be done better and just do one single (hopefully final) edit. Thank you for bringing up the point
    – kos
    Commented May 18 at 16:04
7

You could use find with -execdir to avoid having to strip the path components:

find . -name '*.png' -execdir bash -c '
  for f; do echo mv -n -- "$f" "$RANDOM-$RANDOM-$RANDOM.png"; done
' find-bash {} +

(remove the echo once you are happy that it's doing the right thing). Note that while the lighter weight POSIX sh is generally preferred for "scriptlets" like this, here you need bash for the RANDOM variable. The -n (or equivalently --no-clobber) option prevents mv from overwriting an existing file in the event of a name collision.


If you don't particularly care whether RANDOM lies in the range [0,32767] then you could consider using the perl-based rename command with the String::Random module (requires Ubuntu package libstring-random-perl) e.g.

shopt -s globstar

rename -n --filename -e '
  BEGIN{use String::Random qw(random_regex)}
  $_ = random_regex("\\d{5}-\\d{5}-\\d{5}") . ".png"
' -- **/*.png

(remove -n when you are happy that it's doing the right thing). The --filename (or equivalently -d, --nopath, --nofullpath) restricts the action to just the filename component:

  -d, --filename, --nopath, --nofullpath
           Do not rename directory:  only  rename  filename  component  of
           path.

With this implementation of rename, the default is to skip name collisions by default1.

One possible advantage of this method is that it supports a wider range of random strings, for example

rename -n --filename -e '
  BEGIN{use String::Random qw(random_string)}
  $_ = random_string("cCncCn") . ".png"
' -- **/*.png
rename(foo/1.png, foo/zS3nD1.png)
rename(foo/bar/2.png, foo/bar/kX2gB9.png)
rename(foo/bar/baz/3.png, foo/bar/baz/iU6eB8.png)

To handle multiple extensions, you could use a regex substitution to replace everything except an arbitrary "dot suffix" using lookahead

rename -n --filename -e '
  BEGIN{use String::Random qw(random_string)}                                  
  s/.*(?=\.[^.]*)/random_string("cCncCn")/e
' -- **/*.{png,jpg}

or similarly with the File::Basename module

rename -n --filename -e '
  BEGIN{use String::Random qw(random_string); use File::Basename qw(fileparse)}
  $_ = random_string("cCncCn") . (fileparse($_, qr/\.[^.]*/))[2]
' -- **/*.{png,jpg}

[1] ex. given

    $ ls *.sh
    best.sh  test.sh

then

    $ rename 's/test/best/' -- *.sh
    test.sh not renamed: best.sh already exists
3
  • Like the other answer, this too has the (extremely minor) risk of overwriting files if the random name generated has already been used. What is this --filename option? My perl-rename doesn't have it (nor does the one I tested on an Ubuntu I ave access to).
    – terdon
    Commented May 18 at 12:11
  • 1
    @terdon thanks - I have added the no-clobber option to the mv version, but AFAIK the perl-based rename (though not necessarily others such as similarly named util-linux command) has always defaulted to nolobber. The name of the --filename option may be new but the functionality has been there for a couple of releases iirc - the maintainers don't seem to be able to decide what to call it ;) Commented May 18 at 12:27
  • 1
    @kos thanks - I had missed that requirement. Added. Commented May 18 at 17:45
0

Note: I'm guessing that when the question says "randomizing" filenames, it refers to replacing the original filenames with random not reversible strings, and it not specify that the names HAVE to be in the #-#-# format-it only shows an example of the conversion.

Using for and globs can fail if the filenames contain spaces, because the mechanism of globbing generates a list of words separated with spaces, and then each word found is assigned one by one to the for variable. A name with a interior space will produce two separate words.

To solve the recursion and the spaces, I would use find, which is recursive, to get the full names each in a line, and then apply the transformation to each name:

#!/bin/bash
find /pictures -name "*png" | while read FILE ; do
    basedir="$(dirname "$FILE")"         # get directory part
    filename="$(basename "$FILE" .png)"  # get name part, no extension
    newname="$(md5sum "$filename")"      # get hash of dir/filename.png
    mv "$FILE" "$basedir/${newname}.png" # "move/rename" to same dir
done

The quotes avoid the word splitting in the spaces, if exist. Also used the md5 hash of the filename without path/extension to minimize the posibility of name collisions in the same directory, and also it is not reversible (cannot recover the original name) just like converting to random numbers. Of course, this method can fail if some name contains a line feed, but that is less probable in names than spaces.

2
  • Your premise is wrong: filename expansion happens after word splitting has taken place already, so each filename will be regarded as a single token, regardless of whitespace (heck, they won't even be split on newlines): gnu.org/software/bash/manual/html_node/Filename-Expansion.html; a for loop on a filename expansion won't fail the way you're saying it will fail. OTOH, your read command will mangle filenames containing leading / trailing whitespace, and it will split filenames containing newlines (admittedly a very edge case, but still).
    – kos
    Commented May 22 at 6:37
  • That makes it overall definetly less safe than using a for loop + filename expansion. Your idea is sensible but it definetly needs some polishing (e.g.: the idiomatic way to process files that way is find . -name "*png" -print0 | while IFS= read -d $'\0' FILE ; do, which will preserve leading / tralinig whitespace and won't split on newlines, also you should avoid using all-caps variables and I'd argue *.png would be a safer pattern to search)
    – kos
    Commented May 22 at 6:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .