I want to put together a small script that changes the file extension of all the files in a directory that have a certain extension, such as .png or .gif.

My thought is that the script just take 2 arguments at execution. One being the extension to change and the second being the extension to change it to.

Originally I was thinking I would use read to get the target directory but that makes this too complicated so instead I am just making it work in the current directory. I am running Debian 9 so the rename command is not available.

This works with fixed content (txt to rdf):

$ find -depth -name "*.txt" -exec sh -c 'mv "$1" "${1%.txt}.rdf"' _ {} \;

however, I cannot figure out how to change the extensions to the content of variables that inherit the passed arguments. these will be $1 $2 by default but when I change it as below, it does not work.

$ find -depth -name "*.$1" -exec sh -c 'mv "$1" "${1%.$1}.$2"' _ {} \;

This example just appends a . to the end of all the matching files. What am I missing here?

  • from the sh man page. The positional parameters were quite confusing here. -c Read commands from the command_string operand instead of from the standard input. Special parameter 0 will be set from the command_name operand and the positional parameters ($1, $2, etc.) set from the remaining argument operands.
    – DanMan3395
    Commented Jul 6, 2018 at 23:53

1 Answer 1


find . -depth -name "*.$1" -exec sh -c '
    shift 2
    for pathname do
        mv "$pathname" "${pathname%.$from}.$to"
    done' sh "$1" "$2" {} +

This is letting the internal script take the filename suffixes as parameters as well as a list of pathnames to change. In the internal script, you extract the two suffixes and shift them off the list of arguments, then loop over the patnames given to the script from find.

Less efficient (one invocation of sh -c per pathname instead of looping over a batch of them):


find . -depth -name "*.$1" -exec sh -c 'mv "$3" "${3%.$1}.$2"' sh "$1" "$2" {} ';'

As a side note: Do make it a habit to use sh or find-sh or some other relevant name for the zeroth argument to the internal script. This argument is used in error messages from the script and having the error message come from _ might be confusing under some circumstances.

  • 2
    Nice answer as always. You should probably start linking these types of A'ers to your more canonical A'er - unix.stackexchange.com/questions/389705/…. This would likely be helpful to less experienced ppl learning the more general form of doing this type of work.
    – slm
    Commented Jul 6, 2018 at 20:56
  • I tried several variations of your second example and could not get any of them to work. I had the same thought with using $3 to deal with the conflict in my example but nothing works. Since you posted this I tried your second example and it produces the following error: sh: 1: sh: Bad substitution sh: 1: sh: Bad substitution sh: 1: sh: Bad substitution sh: 1: sh: Bad substitution
    – DanMan3395
    Commented Jul 6, 2018 at 23:10
  • also, what exactly is sh doing here? why spin up a new shell for this?
    – DanMan3395
    Commented Jul 6, 2018 at 23:18
  • 1
    @DanMan3395 There was a typo that I have now fixed. You need to use a child shell to be able to remove the suffix from the pathname. You can't do that on {} with a parameter substitution directly. Or do you mean the second sh, after the script? That's what gets put into $0 in the script. It's mostly used for error messages.
    – Kusalananda
    Commented Jul 6, 2018 at 23:42
  • i did mean the latter, i see what you mean. instead of sending error's from _ which is not normal.
    – DanMan3395
    Commented Jul 6, 2018 at 23:48

You must log in to answer this question.

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