I'm answering this in a slightly different way than
Kusalananda for the fun of it. So if you like this one you should upvote his. This is a little different in that it shows you how to do multiple commands and handle a little more complexity while still making only one call to find.
The Answer
Grep will evaluate as True if it finds a match (i.e. $? == 0). So, grep -l 'str1' filename
will be true if str1 is in filename. If we chain this command to the sed command with &&
we can ensure that sed only runs if grep matched.
The following command will output the filename only if sed is going to make changes:
grep -l 'str1' filename && sed -i 's/str1/str2/g' filename
You cannot use &&
in -exec directly so, we will wrap it in a call to bash.
find ./ -type f \( -name '*.txt' -o -name '*.git' \) \
-exec bash -c "grep -l 'str1' {} && sed -i 's/str1/str2/g' {}" \; > changelist.txt
What makes this appreciably different than Kusalananda's answer is that sed won't even run if grep doesn't match str1. In Kusalananda's answer grep runs for every file and sed runs for every file. Depending on the number of files this could make a huge difference in execution time. For the OP's question though it probably won't make much difference at all.
You could simplify his answer by replacing grep -q
with grep -l
, replacing +
with \;
, and getting rid of the -print
.
find . -type f \( -name '*.txt' -o -name '*.git' \) \
-exec grep -l 'str1' {} \; \
-exec sed -i 's/str1/str2/g' {} \; >changelist.txt
All of that is just nitpicking though. So what follows is my reason for using bash -c
in find's -exec
option. I hope that someone might find it useful.
The Reason for my Approach
I'm here because I wanted to use sed with find to print out a section of a logfile and print the name of the log file only if sed would output anything.
I have some logs that contain something like this:
---- lots of lines before ----
Failed: 0
Summary
( Cases/Passed/Failed)
Frequency Test : ( 69/ 67/ 2)
Carrier/Data Null Test : ( 14/ 13/ 1)
Total Harmonic Distortion: ( 9/ 9/ 0)
Spur Test : ( 0/ 0/ 0)
failed Test
freq, rf2, 0.750e9, -70.0, pm 500, pm 1.0
---- lots of lines after ----
I wanted to just print the test summary and the file name only if it sed detected the test summary.
So for a bunch of files I wanted output like this:
File: ./4662-0003-05132021-0953.log
Summary
--------------------------------------------------
( Cases/Passed/Failed)
Frequency Test : ( 69/ 0/ 69)
Carrier/Data Null Test : ( 14/ 0/ 14)
Total Harmonic Distortion: ( 9/ 9/ 0)
Spur Test : ( 0/ 0/ 0)
File: ./4745-0001-05132021-1017.log
Summary
( Cases/Passed/Failed)
Frequency Test : ( 69/ 68/ 1)
Carrier/Data Null Test : ( 14/ 14/ 0)
Total Harmonic Distortion: ( 9/ 9/ 0)
Spur Test : ( 0/ 0/ 0)
I achieved that with this command:
find ./ -type f -name '*.log' \
-exec bash -c "grep -q Summary {} && echo 'File: {}' && sed -n '/Summary/,/Spur/p' {} && echo" \;
Breaking it down, nothing after grep -q Summary ()
will run if Summary doesn't appear in the log file. sed -n '/Summary/,/Spur/p'
will only print out the section of the log between "Summary" and "Spur".
The difference between -exec cmd {} ; and -exec cmd +
You may be wondering why I used \;
instead of +
. If you use +
, {} will be replaced with as many filenames as can fit on the command line. That is not what we want and in this case find will not even allow it.
From man find:
-exec command {} +
This variant of the -exec action runs the specified command on the selected files, but the command line is built by
appending each selected file name at the end; the total number of invocations of the command will be much less than
the number of matched files. The command line is built in much the same way that xargs builds its command lines.
Only one instance of `{}' is allowed within the command. The command is executed in the starting directory. If
find encounters an error, this can sometimes cause an immediate exit, so some pending commands may not be run at
all. This variant of -exec always returns true.
Conclusion
Sorry for the novel, but I hope it helps someone.
-print
after your-exec
, it will only be executed if the-exec
was successful e.g.find . -type f \( -name \*.git -o -name \*.txt \) -exec sed -i 'blah_blah' {} \; -print
. Sure, you'll have to sort the output then. – don_crissti Jan 21 '16 at 18:11sed -i
is dumb and will "edit" the file even if nothing changes and report success... Add a-exec grep -q str1 {} \;
before the existing-exec sed...
That should do. Oh, and next time you reply, make sure you prepend my username with @ so the system notifies me e.g. @don_crissti otherwise I'll never know you replied (I just happened to return here) – don_crissti Jan 23 '16 at 02:45find
you can sort its output. – mikeserv Jan 23 '16 at 03:24