36

Is it possible to use 2 commands in the -exec part of find command ?

I've tried something like:

find . -name "*" -exec  chgrp -v new_group {}  ; chmod -v 770 {}  \;

and I get:

find: missing argument to -exec
chmod: cannot access {}: No such file or directory
chmod: cannot access ;: No such file or directory

1
  • 1
    Note that -name "*" matches every name, so it's a no-op test that can be removed. Also, both chmod and chgrp has a -R option for recursive operation.
    – Kusalananda
    Dec 18, 2020 at 20:20

3 Answers 3

51

As for the find command, you can also just add more -exec commands in a row:

find . -name "*" -exec chgrp -v new_group '{}' \; -exec chmod -v 770 '{}' \;

Note that this command is, in its result, equivalent of using

chgrp -v new_group file && chmod -v 770 file

on each file.

All the find's parameters such as -name, -exec, -size and so on, are actually tests: find will continue to run them one by one as long as the entire chain so far has evaluated to true. So each consecutive -exec command is executed only if the previous ones returned true (i.e. 0 exit status of the commands). But find also understands logic operators such as or (-o) and not (!). Therefore, to use a chain of -exec tests regardless of the previous results, one would need to use something like this:

find . -name "*" \( -exec chgrp -v new_group {} \; -o -true \) -exec chmod -v 770 {} \; 
4
  • 4
    +1: Yes, that's the most elegant way to do it. If you can explain, why you use'{}' (apostrophes around the braces), please visit: unix.stackexchange.com/q/8647/4485 Aug 4, 2011 at 22:37
  • 1
    @user Unfortunately, I don't know if it is still necessary. I did some test just now and haven't come across a situation where it would change anything. I guess it's just "good practice" that will die out. Aug 5, 2011 at 8:04
  • 2
    The quotes are important for files with spaces in their names.
    – naught101
    Sep 27, 2016 at 2:19
  • @naught101 No, the quotes may be needed in some non-standard shells, such as fish and csh, but are definitely not needed in POSIX-like shells (sh, dash, bash, zsh, ksh, yash).
    – Kusalananda
    Dec 18, 2020 at 20:22
20
find . -name "*" -exec sh -c 'chgrp -v new_group "$0" ; chmod -v 770 "$0"' {} \;
5
  • @Gilles: The wonders of -c's odd handling of $0 make me think this is wrong every time I glance at it, but its definitely correct.
    – derobert
    Aug 24, 2012 at 16:52
  • I like the explicit shell being defined...
    – djangofan
    Aug 24, 2012 at 19:05
  • This answer (and Giles's answer) seems like the better answer for the question given the sh -c.
    – user56041
    Mar 30, 2019 at 16:59
  • 1
    Instead of passing the argument in as $0, pass sh as the first argument and then reference $1, i.e. find -exec sh -c 'chgrp...' sh {} \; The value of $0 is used e.g. as the command name when printing error messages from the shell. Jul 15, 2021 at 1:39
  • @Angel, what does this answer do that's different from the advice in the linked answer? Dec 19, 2021 at 1:51
16

Your command is first parsed by the shell into two commands separated by a ;, which is equivalent to a newline:

find . -name "*" -exec chgrp -v new_group {}
chmod -v 770 {} \;

If you want to run a shell command, invoke a shell explicitly with bash -c (or sh -c if you don't care that the shell is specifically bash):

find . -name "*" -exec sh -c 'chgrp -v new_group "$0"; chmod -v 770 "$0"' {} \;

Note the use of {} as an argument to the shell; it's the zeroth argument (which is normally the name of the shell or script, but this doesn't matter here), hence referenced as "$0".

You can pass multiple file names to the shell at a time and make the shell iterate through them, it'll be faster. Here I pass _ as the script name and the following arguments are file names, which for x (a shortcut for for x in "$@") iterates over.

find . -name "*" -exec sh -c 'for x; do chgrp -v new_group "$x"; chmod -v 770 "$x"; done' _ {} +

Note that since bash 4, or in zsh, you don't need find at all here. In bash, run shopt -s globstar (put it in your ~/.bashrc) to activate **/ standing for a recursive directory glob. (In zsh, this is active all the time.) Then

chgrp -v new_group -- **/*; chmod -v 770 -- **/*

or if you want the files to be iterated on in order

for x in **/*; do
  chgrp -v new_group -- "$x"
  chmod -v 770 -- "$x"
done

One difference with the find command is that the shell ignores dot files (files whose name begins with a .). To include them, in bash, first set GLOBIGNORE=.:..; in zsh, use **/*(D) as the glob pattern.

2
  • This answer (and Glenn's answer) seems like the better answer for the question given the sh -c.
    – user56041
    Mar 30, 2019 at 16:58
  • Both chgrp and chmod has a -R option for recursive operation.
    – Kusalananda
    Dec 18, 2020 at 20:35

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .