I'm attempting a batch rename of camelcase-named files to include spaces between adjacent upper and lower case leters. I'm using Mac OS so the utils I'm using are the BSD-variant.

For example:

250 - ErosPhiliaAgape.mp3 => 250 - Eros Philia Agape.mp3

I'm trying to find the relevant files and pipe them to mv which runs sed in a subshell, using this command:

find . -name "*[a-z][A-Z]*" -print0 | xargs -0 -I {} mv {} "$(sed -E 's/([a-z])([A-Z])/\1 \2/g')"

Individually, these commands work fine: find pulls up the correct files and sed renames it correctly, but when I combine them with xargs, nothing happens.

What do I need to change to make this work?

  • BTW, there's also a Perl-based rename which allows you to rename multiple files using Perl regexps. – dirkt Dec 25 '16 at 8:33

The problem is that the $(...) sub-shell in your command is evaluated at the time you run the command, and not evaluated by xargs for each file. You could use find's -exec instead to evaluate commands for each file in a sh, and also replace the quoting appropriately:

find . -name "*[a-z][A-Z]*" -type f -exec sh -c 'echo mv -v "{}" "$(echo "{}" | sed -E "s/([a-z])([A-Z])/\1 \2/g")"' \;

If the output of this looks good, drop the echo in echo mv. Note that due to echo | sed, this won't work with filenames with embedded \n. (But that was an already existing limitation in your own attempt, so I hope that's acceptable.)

| improve this answer | |
  • 1
    Note that since {} is replaced by the file name, this will choke if the file name contains shell special characters, in this case $"\\`. You should never include {} inside a shell snippet, always pass it as a separate arguments. Some implementations of find and xargs wisely substitute {} only when it's the argument; if your version substitutes it when it's a substring of an argument, that's almost never desirable. – Gilles 'SO- stop being evil' Dec 25 '16 at 0:56

Your sed command is taking input from the pipe. It's competing with xargs, so each of the two commands will get part of the data written by find (each byte goes to only one reader), and it's unpredictable which receives what.

You would need to pipe each file name into sed, which means you need an intermediate shell to set up the pipe.

find . -name "*[a-z][A-Z]*" -print0 | xargs -0 -I {} sh -c 'mv "$0" "$(echo "$0" | sed -E '\''s/([a-z])([A-Z])/\1 \2/g'\'')"' {}

You don't need xargs, it's mostly useless. find can call a program directly.

find . -name "*[a-z][A-Z]*" -exec sh -c 'mv "$0" "$(echo "$0" | sed -E '\''s/([a-z])([A-Z])/\1 \2/g'\'')"' {} \;

Alternatively, install any of the Perl rename implementations, for example File::Rename or the one from Unicode::Tussle.

cpan File::Rename
find -depth . -exec rename 's!([a-z])([A-Z])(?!.*/)!$1 $2!g' {} +

The (?!.*/) bit prevents the replacement from triggering in directory names, in case they contain camelcase. Passing -depth to find ensures that if a directory is renamed, this happens after find has traversed it.

| improve this answer | |

If you prefer using GNU Parallel the solution is:

find . -name "*[a-z][A-Z]*" -type f | parallel mv -v {} '{= s:([a-z])([A-Z])(?!.*/):$1 $2:g =}'
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy