Skip to content
Advertisement

Renaming sub-directories recursively to a new pattern in bash

I want to rename sub-directories to my new pattern but some results may be dangerous in my own script:

dst=$1
dirs=$( find $dst -type d )

for dir in $dirs; do
    path=$( echo $dir | awk 'BEGIN{FS=OFS="/"}{NF--; print}' )
    new_pattern=$( mkdir "$(head /dev/random | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
    mv -v $dir $path/$new_pattern
done

For example if I use /tmp/etc as input argument I would have the nasty results in the first iteration:

mv -v /tmp/etc /tmp/<newpattern>

So in next iteration there will not be /tmp/etc to work at.

The second problem in my script is when I use relative path like etc as input argument. So the first output is:

mv -v etc /<newpattern>

Which is a very bad practice. My ideal way is to detect all directories and then rename them in my own pattern recursively.

Update

My pattern is: head /dev/random | tr -dc A-Za-z0-9 | head -c 15 ; echo '' or may be anything else.

All of directories all empty without any files. Then I want to rename all of these directories to a new name base on my pattern.

dst=$1
dirs=$(find $dst -type d | sort -r)

for dir in $dirs; do
    path=$( echo $dir | awk 'BEGIN{FS=OFS="/"}{NF--; print}' )
    new_pattern=$(head /dev/random | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
    mv -v $dir $path/$new_pattern
done

When use relative path in input argument:

# pwd
/tmp
# bash a.sh etc
renamed 'etc/apache2/conf-enabled' -> 'etc/apache2/6jFtEeT27SHre1t'
renamed 'etc/apache2' -> 'etc/0rP0r7p63CzICdE'
renamed 'etc/alternatives' -> 'etc/I3Yog16F2ijlcYe'
renamed 'etc' -> '/zKpmJT351VuDrVK'

The last output renamed 'etc' -> renamed 'etc' -> '/zKpmJT351VuDrVK' is the main problem.

Advertisement

Answer

You can run find -depth to always visit subdirectories before their parents.

Your code has a number of different problems which would cause it to fail on any nontrivial file names.

dirs=$( find $dst -type d )

This causes dirs to contain the tokens output by find; but if any of those file names contain whitespace, that will result in multiple tokens, which will subsequently incorrectly be interpreted as separate file names if you attempt to loop over this variable. Similarly, if any of the file names contain shell metacharacters like quotes or wildcard characters, your downstream code will either break or need insane complexities to work around this.

(Of course, because of quoting errors the code inside the loop would also fail for similar reasons.)

The simplest fix by far is to use find -exec instead.

find "$1" -depth -type d -exec sh -c 'for dir; do
    path=${dir%/*}
    new_pattern=$(head /dev/random | tr -dc A-Za-z0-9 | head -c 15)
    mv -v "$dir" "$path/$new_pattern"
done' _ {} +

The crucial fix here is that find knows how to pass everything correctly quoted to -exec ... {} so you don’t have to worry about that part. Just make sure you always use double quotes around the arguments inside the -exec sh -c '...' to avoid breaking the good work find already did. (In the absence of an explicit argument, for dir means for dir in "$@".)

I also switched to a parameter expansion to avoid the rather unnecessary Awk invocation. Notice also how there is no need to echo anything at the end of the command substitution, as the shell trims the final newline anyway. (Probably switch to /dev/urandom to avoid depleting your /dev/random though. It works fine for a while, but once you run it on a few hundred files, it will start to block as the random number generator needs to collect more entropy. The randomness from urandom is less random, but probably quite sufficient for this particular use case.)

http://mywiki.wooledge.org/UsingFind has an extensive treatment of the various problems related to common beginner mistakes in this area. The better part of the entire site is about quoting issues.

User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement