Here are some things that may help improve your code.
Avoid the complication of using make
if practical
Rather than invoking make install
, I'd instead advocate something like this:
read -p "All .cfc and .cfm files in the $1 directory and lower directories will be converted. Do you wish to continue?" affirm
case "$affirm" in
y|Y ) echo "yes";;
n|N ) echo "no -- quitting program"; exit;;
* ) echo "Invalid response -- quitting program"; exit;;
esac
The difference is that now the user doesn't need make
. Reducing dependencies results in a more portable script.
Pass in the target directory as a parameter
Rather than hard code theradoc
, it might be more convenient, especially for testing, to have the directory name as a command line argument.
Use full path names for security
It's probably better to invoke /usr/bin/sed
than just sed
because the latter would be easy to substitute. I could have a malicious sed
on my path somewhere ahead of the real sed
but it's generally harder to overwrite a system file. Specifying a full path uses that to your (and your user's) advantage.
Use find
directly
Rather than creating multiple files and processing them, I think you will find that it's faster to simply use find
directly rather than redirecting to a file and then processing that file line by line.
Use awk
for complex replacement
While perl
can do regular expression matching and everything awk
can do, you're currently invoking perl
once per filename per file. That is, if there are 1500 files, you're invoking perl
1500 * 1500 times = 2,250,000 times. It's already been suggested to just do the whole thing in perl
which is certainly another option, although I find perl
to be a "write only" language. Once I write it, six months later I find that even I can't read and understand it.
Construct just the replacement list
The only output file you really need is the one containing just the list of base file names. That can be done like this:
find $1 -type f -iname '*.cf[cm]' -exec /usr/bin/basename {} ';' >basename.txt
Note that this uses -exec
to run basename
to extract just the base part of the name (e.g. /usr/bin/basename
would be converted to basename
).
Create a bash
function to handle the replacement
I'd advocate exporting a function and then invoking find
again like this:
export -f replaceAndRename
find $1 -type f -iname '*.cf[cm]' -exec bash -c 'replaceAndRename "$0"' {} \;
The replaceAndRename
function might be implemented like this with awk
:
replaceAndRename () {
lcfile="$(echo $(/usr/bin/basename "$1") | tr '[A-Z]' '[a-z]')"
lcdir="$(/usr/bin/dirname "$1")"
lcfile="${lcdir}/${lcfile}"
/usr/bin/awk 'NR==FNR { map[$1]=tolower($1); next }{
for (old in map) {
gsub(old,map[old])
}
print
}' basename.txt "$1" >"tmp.foo"
retval="$?"
if [ "$retval" -eq 0 ]; then
rm "$1"
mv tmp.foo "$lcfile"
fi
}
This probably looks more complicated than it really is. The first three lines simply create a version of the name that uses a lowercase base name. Note, too that if there is something like a subdirectory with a name like My Directory
we don't want to alter the directory name -- just the file name.
Next, we invoke awk
and pass in the basenames.txt
file created by the first invocation of find
as well as the current file name to be processed, redirecting the output to a temporary file I've arbitrarily named tmp.foo
but one could probably improve that by using mktemp
instead.
The awk
script reads the first file in and creates an associative array of the original version of the filename mapped to the lowercase version. Next, the second file is scanned and the map is used to make the appropriate substitutions. Finally the print
within awk
just prints the possibly modified line to the output which is redirected to tmp.foo
.
Finally, if awk
seemed to run successfully, we remove the original file and the move the tmp.foo
into place using the lowercase version of the name. We do it in this order in case one of the files is already in lowercase but may have had file references changed.
Putting it all together
I don't have a convenient way to do benchmarking, but I believe this version of the script will improve your speed.
#!/bin/bash
read -p "All .cfc and .cfm files in the $1 directory and lower directories will be converted. Do you wish to continue?" affirm
case "$affirm" in
y|Y ) echo "yes";;
n|N ) echo "no -- quitting program"; exit;;
* ) echo "Invalid response -- quitting program"; exit;;
esac
find $1 -type f -iname '*.cf[cm]' -exec /usr/bin/basename {} ';' >basename.txt
replaceAndRename () {
lcfile="$(echo $(/usr/bin/basename "$1") | tr '[A-Z]' '[a-z]')"
lcdir="$(/usr/bin/dirname "$1")"
lcfile="${lcdir}/${lcfile}"
/usr/bin/awk 'NR==FNR { map[$1]=tolower($1); next }{
for (old in map) {
gsub(old,map[old])
}
print
}' basename.txt "$1" >"tmp.foo"
retval="$?"
if [ "$retval" -eq 0 ]; then
rm "$1"
mv tmp.foo "$lcfile"
fi
}
export -f replaceAndRename
find $1 -type f -iname '*.cf[cm]' -exec bash -c 'replaceAndRename "$0"' {} \;