3

I have a list of files in a certain folder without extenstion. I want to rename them and add an extension. The name should be UUID. I know how to find them and there's the utility "uuidgen" for generating UUID, how can I do the rest?

find . -type f -name 'file*' # ??? -exec .... ???
5

Try:

find . -type f -name 'file*' -execdir sh -c 'mv "$1" "$(uuidgen).ext"' Renamer {} \;

How it works

  • find . -type f -name 'file*' -execdir ... \;

    This part you appear to be familiar with: it finds files whose names match the glob file* and runs the command ... on them where, in our case, ... is replaced by:

  • sh -c 'mv "$1" "$(uuidgen).ext"' Renamer {}

    This starts a shell, runs a mv command, and makes the file name available as $1. (Renamer is an arbitrary string that the shell assigns to $0 which the shell would use if it generates an error message.) The find command replaces {} with the name of the file it found and this is what the shell assigns to $1. Let's look just at the shell command:

  • mv "$1" "$(uuidgen).ext"

    This renames the file from $1 to a name created by uuidgen.

    Because the file name might contain spaces or other difficult characters, we place $1 in double-quotes to protect the name from word splitting or shell expansions.

We used -execdir instead of -exec for two reasons:

  1. It makes the mv command easier: it takes no extra code to make sure that the renamed file stays in the same directory.

  2. -execdir is safer: if a directory were renamed while the find command was running, -exec, but not -execdir, can fail badly.

Why start a new shell?

It might be tempting to try to use:

find . -type f -name 'file*' -execdir mv {} "$(uuidgen).ext" \; # Don't do this

The issue here is that uuidgen would be run only once. The shell would run uuidgen before it even starts the find command. As a consequence, all mv commands will have the same target file name. We want uuidgen to be run one time for every file that is found by find. That is why we need to run mv and uuidgen under the sh command.

More efficient version

The above starts a new shell for each file found. The following approach, by contrast, can process many thousands of files per shell process started:

find . -type f -name 'fi*' -execdir sh -c 'for f; do mv "$f" "$(uuidgen).ext"; done' Renamer {} +
  • what's Renamer? – Johshi Aug 28 '16 at 6:32
  • 1
    The shell assigns the first argument, Rename in this case, to $0 which the shell would use as the script name if the shell had to generate an error message. If you don't care about possible error messages and just want to shorten the command, you could replace Renamer with _. – John1024 Aug 28 '16 at 6:36
  • 1
    Why start a new shell? Couldn't you just do -execdir mv {} "$(uuidgen).ext" \;? Is this just to get better error messages? – Bakuriu Aug 28 '16 at 12:30
  • 1
    @Cbhihe The {} is part of the find command, not sh: it tells find where to put the file names. As for the Renamer part, it is documented, albeit tersely, as part of the explanation of the shell's option -c. As for why use Renamer instead of some other string, the discussion here may be helpful. – John1024 Aug 31 '16 at 18:23
  • 1
    @Cbhihe Yes, that is about it except that the word subshell has a specific meaning and mv is a spawned process but it is not subshell nor does it run in a subshell. Separately, if efficiency is a concern, I added code to the answer that runs only one shell for every several thousand files found. – John1024 Sep 1 '16 at 18:20

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.