Shell programming trap: batch rename with find

When we want to rename all files under the current directory and its sub-directories, say renaming files with .shell suffix to those with .sh suffix. we may write this command:

find . -name "*.shell" -exec mv {} $(echo {} | sed -e 's/\.shell$/.sh/') \;

Here, find will replace all occurrence of “{}” with  the current file name being processed, and call the mv command to rename it. We use Shell command substitution to generate the new file name. It seems great, but things will not go as we want. When execute the above command, we will get some error message like

mv: `./t/file1.shell' and `./t/file1.shell' are the same file

It seems that echo and sed in command substitution didn’t generate the correct new file name for us. And it really didn’t.

In fact,  Shell do the command substitution before invoking find, at that point, “{}” doesn’t have any special meaning, it’s just “{}” literally, not the file name. So any processing against it should produce a wrong result.

We can do the rename using “sh -c” instead:

find . -name "*.shell" -exec sh -c 'mv $1 $(echo $1 | sed -e "s/.shell$/.sh/")' mv  {} \;

Here, when a matching file is found, find will start a sh shell, passing it mv as the command name, current file name(“{}”) as the first and sole argument , sh then execute command

mv $1 $(echo $1 | sed -e "s/.shell$/.sh/")

In the sh shell, $1 is the first positional parameter(thus the file name found). This way,everything goes fine.

Here comes the Conclusion In Shell environment, command substitution happens at argument processing stage, before the upper most command(like the find command here) getting executed, so it can’t use any result produced by the upper most command. If we want such result, use sh -c instead.

This entry was posted in Programming, System Administration and tagged . Bookmark the permalink.

Leave a Reply