Bash Executing commands against a found file


Sometimes we will need to run commands against a lot of files. This can be done using xargs.

find . -type d -print | xargs -r chmod 770

The above command will recursively find all directories (-type d) relative to . (which is your current working directory), and execute chmod 770 on them. The -r option specifies to xargs to not run chmod if find did not find any files.

If your files names or directories have a space character in them, this command may choke; a solution is to use the following

find . -type d -print0 | xargs -r -0 chmod 770

In the above example, the -print0 and -0 flags specify that the file names will be separated using a null byte, and allows the use of special characters, like spaces, in the file names. This is a GNU extension, and may not work in other versions of find and xargs.

The preferred way to do this is to skip the xargs command and let find call the subprocess itself:

find . -type d -exec chmod 770 {} \;

Here, the {} is a placeholder indicating that you want to use the file name at that point. find will execute chmod on each file individually.

You can alternatively pass all file names to a single call of chmod, by using

find . -type d -exec chmod 770 {} +

This is also the behaviour of the above xargs snippets. (To call on each file individually, you can use xargs -n1).

A third option is to let bash loop over the list of filenames find outputs:

find . -type d | while read -r d; do chmod 770 "$d"; done

This is syntactically the most clunky, but convenient when you want to run multiple commands on each found file. However, this is unsafe in the face of file names with odd names.

find . -type f | while read -r d; do mv "$d" "${d// /_}"; done

which will replace all spaces in file names with underscores.(This example also won't work if there are spaces in leading directory names.)

The problem with the above is that while read -r expects one entry per line, but file names can contain newlines (and also, read -r will lose any trailing whitespace). You can fix this by turning things around:

find . -type d -exec bash -c 'for f; do mv "$f" "${f// /_}"; done' _ {} +

This way, the -exec receives the file names in a form which is completely correct and portable; the bash -c receives them as a number of arguments, which will be found in $@, correctly quoted etc. (The script will need to handle these names correctly, of course; every variable which contains a file name needs to be in double quotes.)

The mysterious _ is necessary because the first argument to bash -c 'script' is used to populate $0.