Linux find exec two commands

bash — can I do : find . -exec this && that?

Is there a way to logically combine two shell commands that are invoked with find — exec? For instance to print out all the .csv files that contain the string foo together with its occurrence I would like to do:

find . -iname \*.csv -exec grep foo <> && echo <> \; 

You could use 2 -exec in sequence or use a single -exec sh -c ‘grep foo «$0» && printf %s\\n «$0″‘ <> \; .

This has tripped me up repeatedly: I always expect that the first argument passed to sh (in this case <> ) will be $1 and $0 will be something like sh . But in fact, you are correct, the first argument shows up as $0 . Having the first argument be the name of the invoking command is just a convention, that isn’t automatically enforced in these cases.

3 Answers 3

-exec is a predicate that runs a command (not a shell) and evaluates to true or false based on the outcome of the command (zero or non-zero exit status).

find . -iname '*.csv' -exec grep foo <> \; -print 

would print the file path if grep finds foo in the file. Instead of -print you can use another -exec predicate or any other predicate

find . -iname '*.csv' -exec grep foo <> \; -exec echo <> \; 

See also the ! and -o find operators for negation and or.

Alternatively, you can start a shell as:

find . -iname '*.csv' -exec sh -c ' grep foo "$1" && echo "$1"' sh <> \; 

Or to avoid having to start a shell for every file:

find . -iname '*.csv' -exec sh -c ' for file do grep foo "$file" && echo "$file" done' sh <> + 

The problem you’re facing is that the shell first parses the command line, and sees two simple commands separated by the && operator: find . -iname \*.csv -exec grep foo <> , and echo <> \; . Quoting && ( find . -iname \*.csv -exec grep foo <> ‘&&’ echo <> \; ) bypasses that, but now the command executed by find is something like grep with the arguments foo , wibble.csv , && , echo and wibble.csv . You need to instruct find to run a shell that will interpret the && operator:

find . -iname \*.csv -exec sh -c 'grep foo "$0" && echo "$0"' <> \; 

Note that the first argument after sh -c SOMECOMMAND is $0 , not $1 .

Читайте также:  Install linux on a chromebook

You can save the startup time of a shell process for every file by grouping the command invocations with -exec … + . For ease of processing, pass some dummy value as $0 so that «$@» enumerates the file names.

find . -iname \*.csv -exec sh -c 'for x in "$@"; do grep foo "$x" && echo "$x"; done' \ <> + 

If the shell command is just two programs separated by && , find can do the job by itself: write two consecutive -exec actions, and the second one will only be executed if the first one exits with the status 0.

find . -iname \*.csv -exec grep foo <> \; -exec echo <> \; 

(I assume that grep and echo are just for illustration purpose, as -exec echo can be replaced by -print and the resulting output is not particularly useful anyway.)

Источник

How can I use two bash commands in -exec of find command?

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.

3 Answers 3

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

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 <> \; 

+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

Читайте также:  Sap gui linux java

@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.

@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 ).

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

@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.

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.

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» .

Читайте также:  Linux mint удаление пакета

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.

Источник

Оцените статью
Adblock
detector