Executing commands against a found file
suggest changeSometimes 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
.