Find and replace last char '_' with '.' on filenames recursively

It's replacing all instances of _ because ${1//_/.} is global (${1/_/.} would be non-global, but replace the first match rather than the last).

Instead you could use POSIX ${1%_*} and ${1##*_} to remove the shortest suffix and longest prefix, then rejoin them:

find . -name '*_pdf' -type f -exec sh -c 'mv "$1" "${1%_*}.${1##*_}"' sh {} \;

or

find . -name '*_pdf' -type f -exec sh -c 'for f do mv "$f" "${f%_*}.${f##*_}"; done' sh {} +

For multiple extensions:

find . \( -name '*_pdf' -o -name '*_jpg' -o -name '*_jpeg' \) -type f -exec sh -c '
  for f do mv "$f" "${f%_*}.${f##*_}"; done
' sh {} +

I removed the -- end-of-options delimiter - it shouldn't be necessary here since find prefixes the names with ./.

You may want to add a -i option to mv if there's risk that both a file_pdf and file.pdf exist in a given directory and you want to be given a chance not to clobber the exising file.pdf.


With perl rename tool:

find ... rename 's/_pdf$/.pdf/' {} +

You can run that individually for your different extensions, or replace multiple "common extensions" at once:

find ... rename 's/_(pdf|jpg|jpeg)$/.\1/' {} +

If you don't have perl rename, you can use other rename tool:

find ... rename '_pdf' '.pdf' {} +

The obligatory zsh version:

autoload zmv # best in ~/.zshrc
zmv -v '(**/)(*)_(pdf|jpg|jpeg)(#q.)' '$1$2.$3'

((#q.) being to restrict to regular files as find's -type f does. Change to (#qD.) if you also want to rename hidden files or files in hidden directories like find does. Replace -v with -n for a dry-run).