6

I'm performing a nested grep like this:

grep -ir "Some string" . |grep "Another string I want to find in the other grep's results"

This works perfectly as intended (I get the results from the first grep filtered by the second grep as well), but as soon as I add an "-l" option so I only get the list of files from the second grep, I don't get anything.

grep -ir "Some string" . |grep -l "Another string I want to find in the other grep's results"

This results in the following output:

(standard input)

I guess piping doesn't work when I just want the list of files. Any alternatives?

OMA
  • 165
  • 2
    @don_crissti, I wouldn't say it's a duplicate as here the OP wants a recursive search and only one of the matches is case sensitive. – Stéphane Chazelas Apr 20 '17 at 12:09
  • @Stéphane - indeed, the case sensitivity makes it different (I missed the -i of the initial grep) - I'm retracting my close vote – don_crissti Apr 20 '17 at 13:19

5 Answers5

17

The -l option to grep will make the utility print only the name of the file containing the specified pattern. The manual on my system says the following about this option:

Only the names of files containing selected lines are written to standard output. grep will only search a file until a match has been found, making searches potentially less expensive. Pathnames are listed once per file searched. If the standard input is searched, the string "(standard input)" is written.

Since the second grep in your pipeline is reading from standard input, not from a file, it is unaware of where the data is coming from other than that it's arriving on its standard input stream. This is why it's returning the text string (standard input). This is as close as it can get to where the match was located.

To combine the two patterns in the first grep (which does know what files it's looking in), see How to run grep with multiple AND patterns?

Kusalananda
  • 333,661
  • Thanks for the easy to understand explanation. Points for that. I was doing it wrong by using -l in a piped grep command, since now I realize that using it that way grep is just getting text, not files, as source. I accepted another answer, though, because it provided an easy to use working alternative. – OMA Apr 20 '17 at 15:48
  • @OMA No worries. I'm happy to have provided helpful input. – Kusalananda Apr 20 '17 at 15:49
6

use "cut" to remove strings after ":" then you get file parts (assuming file paths don't contain colon or newline characters and don't match the second pattern themselves).

grep -ir "Some string" . |grep "Another string I want to find in the other grep's results" | cut -d ":" -f 1

in case of duplications use "uniq"

grep -ir "string1" . | grep "string2" | cut -d: -f1 | uniq
Lesmana
  • 27,439
5

(I'm assuming you intended the second grep to match on the content of line and not on the file names or both as your approach was doing)

POSIXly:

find . -type f -exec awk '
  FNR == 1 {found = 0}
  !found && tolower($0) ~ /some string/ && /other string/ {
    print FILENAME
    found = 1
    nextfile
  }' {} +

The business about found is for awk implementations that don't support nextfile yet (and where nextfile is then a no-op). If you know your awk implementation supports nextfile, you can simplify it to:

 find . -type f -exec awk 'tolower($0) ~ /some string/ && /other string/ {
    print FILENAME; nextfile}' {} +

With GNU grep built with PCRE support, since you want one match to be case insensitive and not the other:

grep -rlP '^(?=.*(?i:some string))(?=.*other string)' .

(?=...) is a perl look-ahead operator. (?i:pattern) turns on case-insensitive match for pattern only. So here we're matching on the beginning of the line (^) provided it's followed by any number of characters (.*) followed by some string (case insensitive) and also that it (the beginning of the line) is followed by any number of characters and other string (case sensitive).

If your grep doesn't support -P, you may have the pcregrep command instead (replace grep -rlP with pcregrep -rl), or if the patterns don't overlap, you could do:

grep -rl -e '[sS][oO][mM][eE] [sS][tT][rR][iI][nN][gG].*other string' \
         -e 'other string.*[sS][oO][mM][eE] [sS][tT][rR][iI][nN][gG]' .

Or if you don't care both matches being case insensitive:

grep -ril -e 'some string.*other string' \
          -e 'other string.*some string' .
  • Thanks for your reply. I really don't need the second grep to be case insensitive since I know the exact string I want to look for, but I guess it wouldn't matter if it was case insensitive as well. My grep doesn't support the -P argument, unfortunately – OMA Apr 20 '17 at 15:38
  • @OMA, see edit with some more alternatives. – Stéphane Chazelas Apr 20 '17 at 16:00
2

You can place both paterns in one query (based on this answer) using the following pattern:

grep -P '^(?=.*pattern1)(?=.*pattern2)'

In your case You can add the -ir -l parameters in the following form:

grep -irlP '^(?=.*pattern1)(?=.*pattern2)' .
Yaron
  • 4,289
  • Thanks for your answer. This just gives me the "usage" message (as if I just typed "grep" with no arguments) – OMA Apr 20 '17 at 15:34
  • Probably my grep doesn't support this. This is what I get: "usage: grep [-abcDEFGHhIiJLlmnOoqRSsUVvwxZ] [-A num] [-B num] [-C[num]] [-e pattern] [-f file] [--binary-files=value] [--color=when] [--context[=num]] [--directories=action] [--label] [--line-buffered] [--null] [pattern] [file ...]" – OMA Apr 20 '17 at 15:35
  • @OMA - I'm using grep (GNU grep) 2.25, on Ubuntu 16.04 (it worked for me). I've noticed that in your original command you added a dot . as the path , what happened if you happened dot . after the grep command ? – Yaron Apr 20 '17 at 15:37
1

This is the shortest solution of all provided.

find . -type f -exec perl -lne '
   /Some string/i and /other string/ and print($ARGV),close(*ARGV);
' {} +

grep -irZ "Some string" . |
perl -lsF'/\n/' -0ne '
   s/^/\n/ if $. == 1; s/$/\n/ if eof;

   $. == 1 and $prev = $F[1],next;
   push @{$h{$prev}}, $F[0];
   $prev = $F[1];

   END {
      grep($_ =~ /\Q${str2}/, @{$h{$_}}) and print for keys %h;
   }
' -- -str2="Another string"

Working principle: Here the first grep does a recursive, case-insensitive search for "some string" in the current directory and downwards and generates null-separated(\0) records due to the -Z option given to grep.

Each of these records contain the the filename and the line that matched. Only hitch is that the ordering is not in step, due to grep's behavior of not putting a \0 after the matching line. To get around this limitation, we make use of Perl reading null-separated records and splitting these records on \n to separate out the lines from the filenames.

Hence their is no limitation on the kinds of file names that can be involved, with the exception of \0, which anyway are forbidden.

  • That's different from what was asked as it searches for another string anywhere in the files that contain some string, not only in the lines that contains some string. – Stéphane Chazelas Apr 20 '17 at 12:01
  • Thanks S.C. for catching that egregious error. I have amended and placed another fix which should ameliorate the situation. –  Apr 20 '17 at 13:29