5

Is there a way with only the find command to prepend a string to filenames?

So that this:

201a.txt
201b.png
...

Becomes this:

foo_201a.txt
foo_201b.png
...

This:

find . -name '201*' -execdir mv {} foo_{} \;

Does not work because the {} part includes the leading ./ in the filename, and therefore tries to write to foo_./201*.

If this is not possible with only find, what is the most portable (read: only coreutils, no shell scripts, easiest to understand) way to prepend a string to filenames?

  • 1
    "no shell scripts" . . . are you ruling out for example -execdir sh -c 'mv "$1" "foo_${1#*/}"' sh {} \; ? – steeldriver Apr 12 '17 at 18:01
  • @steeldriver only if it is not possible without sh and/or coreutils. – iquoth Apr 12 '17 at 18:09
  • @steeldriver what does the 1#*/ part do? – iquoth Apr 12 '17 at 18:10
  • 1
    does it have to be with the find? How about a for loop that executes on every file with said "201*" string? for f in 201*; do mv $f "foo_$f"; done – bretonics Apr 12 '17 at 18:13
  • ${1#*/} is the value of the first positional parameter with the shortest leading component that matches */ removed. AFAIK it's a feature of POSIX sh and so should be fairly portable - see for example 2.6.2 Parameter Expansion – steeldriver Apr 12 '17 at 18:16
2

No. But the rename command offers a simple solution.

$ ls -1
201a.txt
201b.png
$ rename 201 foo_201 201*
$ ls -1
foo_201a.txt
foo_201b.png
$
2

This is not possible with find alone, not even with GNU find. GNU find can print files with the command line prefix removed and another prefix put instead, by using -printf foo_%p instead of -print, but there's nothing similar for -exec.

(You could use find -printf 'mv foo_%p …' | sh, if you like living dangerously. This only works with “tame” file names and breaks horribly if there are spaces, quotes and other special characters in the file names. So don't do this.)

The standard way to do this (as in a way that works on any POSIX system, and also as in a common way) is to call a shell to do the string manipulation.

find . -name '201*' -exec sh -c 'mv -- "$0" "${0%/*}/foo_${0##*/}"' {} \;

I don't use -execdir because it's a GNU extension and you asked for portability. Note that {} is passed to the shell as an argument. Never use {} inside the shell code: not only because it isn't portable, but most importantly because it would cause the file name to be interpreted as shell code, which fails if the file name contains special characters.

On modern POSIX systems (anything from the last decade), you can speed this command up a little by batching the shell calls.

find . -name '201*' -exec sh -c 'for x do mv -- "$x" "${x%/*}/foo_${x##*/}"; done' sh {} +

Alternatively, in ksh, bash or zsh, you can use recursive globbing instead of calling find.

set -o globstar # ksh only
shopt -s globstar # bash only
for x in **/201*; do
  mv -- "$x" "${x%/*}/foo_${x##*/}"
done
1

How about

find . -name '201*' -execdir basename {} \; | xargs -I{} mv {} foo_{}

basename is provided by coreutils and since xargs is provided by findutils it should be at least as portable as find -execdir itself.


Alternatively, using only POSIX shell features

find . -name '201*' -execdir sh -c 'mv "$1" "foo_${1#*/}"' sh {} \;
  • Your xargs solution fails if the file or dir contains: touch '"*'" '" (two spaces). – Ole Tange Apr 13 '17 at 20:24
1

With GNU Parallel it looks like this:

# Make an annoying dir and a couple of annoying files
mkdir '"*'"  '"
touch '"*'"  '"/201'"*'"  '".txt
touch '"*'"  '"/201'"*'"  '".png

# GNU Parallel deals nicely with annoying files
find . -name '201*' | parallel mv {} {//}/foo_{/}
0

You could use an asterisk (*) instead which strips the ./ part:

find * -name '201*' -exec mv {} foo_{} \;  

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.