118

Better to explain on examples.

I can:

find . -name "*.py" -type f > output.txt

But how can I store the output to the same file for:

find . -name "*.py" -type f -exec grep "something" {} \

I can't just do

find . -name "*.py" -type f -exec grep "something" {} \ > output.txt
John
  • 215
bakytn
  • 2,951

7 Answers7

174

If I understand you correctly this is what you want to do:

find . -name '*.py' -print0 | xargs -0 grep 'something' > output.txt

Find all files with extension .py, grep only rows that contain something and save the rows in output.txt. If the output.txt file exists, it will be truncated, otherwise it will be created.

Using -exec:

find . -name '*.py' -exec grep 'something' {} \; > output.txt

I'm incorporating Chris Downs comment here: The above command will result in grep being executed as many times as find finds pathnames that passes the given tests (only the single -name test above). However, if you replace the \; with a +, grep is called with multiple pathnames from find (up to a certain limit).

See question Using semicolon (;) vs plus (+) with exec in find for more on the subject.

Kusalananda
  • 333,661
johnny
  • 2,001
  • 24
    Use + instead of \;, it will improve execution time significantly (since it will contatenate arguments prior to execution until ARG_MAX). – Chris Down Nov 06 '11 at 14:31
  • 7
    Usegrep -H if you want to include the filename of the file in the output. – Steinar Apr 23 '18 at 13:18
  • Use option 1. And add LC_ALL=C right before xargs to get a lot of extra speed. And you can use a -P6 flag on xargs to run in parallel with (in this case) 6 processes (feel free to change the number from 6 to something higher or lower, to see what's faster on your machine and on your search). Reference: https://unix.stackexchange.com/questions/131535/recursive-grep-vs-find-type-f-exec-grep-which-is-more-efficient-faster – Michele Piccolini May 22 '20 at 08:24
  • Using + instead of \; will pass multiple files to grep, and if any file in that set contains the text 'something', all those files will pass the test. So it won't do what you want. – Alexander Torstling Nov 17 '20 at 19:37
25

If you want to save all the matching lines across all files in output.txt, your last command does work, except that you're missing the required ; at the end of the command.

find . -name "*.py" -type f -exec grep "something" {} \; > output.txt

If you want each run of grep to produce output to a different file, run a shell to compute the output file name and perform the redirection.

find . -name "*.py" -type f -exec sh -c 'grep "something" <"$0" >"$0.txt"' {} \;
  • To expand on @gilles answer to make it a little more informative, especially if the list of files you're dealing with is large, you can report the file name (relative path) of each file along with the grep'ed results using this:

    find . -name "*.py" -type f -exec grep "something" {} \; -print > output.txt

    And if you'd like to see the line numbers of the grep'ed lines you can, of course, use grep -n "something"

    – JJMpls Jul 24 '14 at 02:27
  • For the last answer (save the output for each file it's own new file) you need to trim off the full path (leaving just the discovered file name) if you are searching for files somewhere other than the current directory. This can be done using ${0##*/}. I ended up with find /search/here/ -name "*pattern*" -type f -exec sh -c 'grep "pattern2" <"$0" > "filtered_${0##*/}"' {} \; This finds all files whose name contains pattern in /search/here/, greps them for pattern2, and outputs the results for each file as a new file in the current working directory naming each filtered_<file name> – Cole Mar 16 '23 at 12:41
17

For the record, GNU grep has --include and --exclude arguments that you can use to filter the files it searches:

grep -r --include="*.py" "something" > output.txt
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
hunse
  • 271
2

Use tee:

find . -name '*.py' | tee output.txt | xargs grep 'something'

The caveat, is if you have any files with special characters (including spaces) that xargs and grep won't work well with (a file.txt will be interpreted as two files, a and file.txt). The alternative to that is to use either the -x or -print0, but either of those will pollute your output.txt. The -x will use \ to escape certain special characters and this will be in output.txt. The -print0 will use a null byte as a field separator (which also requires xargs -0) and output.txt will look like one long contiguous line of text.

How you deal (or don't) with this is up to you.

bahamat
  • 39,666
  • 4
  • 75
  • 104
2

You ask:

But how can I store the output to the same file for:

find . -name "*.py" -type f -exec grep "something" {} \

You misunderstand. The reason you can't pipe this to anything is that it is not a full command. The closing \ tells your shell to continue reading the command on the next line, which is not what you intend to do here. When you try:

find . -name "*.py" -type f -exec grep "something" {} \ > output.txt

the \ escapes a blank character that, as a result, will be passed as an extra argument to find, and find will complain that the command passed to its -exec option isn't being terminated.

Terminating that command must be done by adding a + or ; token, and the ; token is special to the shell, so it must be escaped or quoted. Additionally, { and } are also special in most shells, so they must be escaped or quoted as well:

find . -name "*.py" -type f -exec grep "something" \{} \;

or

find . -name "*.py" -type f -exec grep "something" '{}' ';'

These are valid commands, so you can redirect and pipe their output just fine:

find . -name "*.py" -type f -exec grep "something" \{} \; > output.txt
find . -name "*.py" -type f -exec grep "something" '{}' + | fgrep -v notthesefiles: > output.txt

By the way, I prefer to single-quote arguments if I don't want the shell to interpret any characters in them, even though (as @Kusalananda points out), that is not necessary in this case:

find . -name '*.py' -type f -exec grep 'something' \{} \; > output.txt

For the difference between ; and +, try man find.

0
grep -n CThread '`find . -name "*.cpp"`'
manatwork
  • 31,277
  • The shell would not expand the command substitution within those single quotes. If you changed to double quotes, the command would fail to operate as expected if the find command found files in paths containing spaces, tabs, or newlines, or potentially if they contained filename globbing characters. You would also run inte problems if the find command returned too many files. – Kusalananda Jul 17 '22 at 13:21
-1

To find files in a directory and search for a string in the result: find $dir -name "*partoffilename*" | xargs -I '{}' grep -ri $searchstring {}

  • If searchstring is a variable containing a regular expression, then you would need to protect it from the shell so that it does not use it as a filename globbing pattern. Additionally, if the string starts with a dash, it may be mistaken for a set of options. You would correct for both these eventualities using grep -i -E -e "$searchstring". Your command would still fail if any filename contained quotes or spaces, tabs, or newlines though. – Kusalananda Jul 17 '22 at 13:25