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.