3

How can I can I find files that contain a specific pattern on a specific line number ? Let's assume I have a directory with a bunch of text files containing 3 lines, such as:

Title A
Category X
Description Y

How can I grep / filter every files that have Category X on line 2? How can I find files that have Title A as line 1?

I looked at the grep man page, ripgrep and alternative but not sure you can limit the search of a pattern to specific line numbers.

0

5 Answers 5

9

You can use awk like that:

awk 'FNR == 2 && /Category X/ {print FILENAME}' *
16
  • Nice one! Do you think performance wise it is crazy to do so / not elegant or you believe it's ok? Feb 13 at 18:45
  • 2
    Is always amazing to me the things awk can do with a minuscule amount of code.
    – Seamus
    Feb 13 at 18:49
  • 1
    @pierre-jean: you could use nextfile on FNR==3, if your awk version provides it.
    – RudiC
    Feb 13 at 18:51
  • 1
    @Pierre-Jean: yes, it could. In bash you could also use ** Feb 13 at 18:53
  • 1
    @ArkadiuszDrabczyk Not millions of files: the constraint is the length of the filenames in the args array (which is provided on-stack to the process). Usual limit on that is 2MB on actual pathnames (not file count). As we can break at line 3 of each file (with nextfile), the file size is of no relevance, as it does not get read. Feb 13 at 20:50
7

Your can use find with awk in order to exit processing the rest of the file when pattern found in the 2nd line, or even also exit if it was not found that in the 2nd line.

find -type f -name 'xyz*.txt' -exec \
    awk 'NR==2{ if(/pattern/) print FILENANE; exit }' {} \;
2
  • why not awk 'NR == 2 && /pattern/' {print FILENAME; exit} ? Feb 13 at 22:10
  • 3
    @DanieleGrassini Because if Line 2 does not match the pattern, it will not take the action, will not exit, and will read the other million lines of the file waiting hopelessly for NR == 2 to happen again. Feb 13 at 22:22
6

grep only, for the fun of it:

PAT="Category X"
LN=2
> grep -n "$PAT" file* | grep ":$LN:$PAT$" | grep -o "^[^:]*"
file1
file2
1
  • I didn't realize that using the line number option plus piping several search could be an answer to my problem! I need to check the comment above for the issue raised but I like the initial idea at least! Nice one! Feb 13 at 18:57
4

To test just the files in your current directory (assuming there's no sub-dirs or unreadable files in this directory and not so many as to exceed ARGS_MAX) would be:

awk 'FNR==2{ if (/Category X/) print FILENAME; nextfile }' *

but from your comments it sounds like you want to descend a hierarchy which would be:

find . -type f -exec \
    awk 'FNR==2{ if (/Category X/) print FILENAME; nextfile }' {} +

The use of + in the find command (may require GNU find) will cause it to run awk on batches of files instead of 1 at a time, and the use of nextfile (if your awk supports it - many do, some don't) will cause awk to stop reading the current file and move on to the next one once the 2nd line is read. Since your input files are each only 3 lines long it'll be very efficient whether your awk supports nextfile or not.

3

GNU grep can be used for your use case:

$ grep -Plzr '^(?:.*\n){1}.*Category X' .

grep normally works on a per-line basis, but GNU grep has added the -z option where in it treats the whole file as a line because it separates records on a character NOT found in text files (\0).

So now we can apply the regex on the whole file. Your requirement is searching only the second line, hence we drive past one line without doing anything ^(?:.*\n){1}

The caret ^ anchors the regex to begin from the beginning. The dot can not span lines because it doesn't match a newline.

Then the .*Category X will start looking in the next line, meaning the second, but won't span lines , so it matches if the pattern is found on the second line.

If there's a match, the -l option will list out the filename to STDOUT.

The -r option will make grep run recursively (GNU feature).

The -P will enable to write Perl style regexes (GNU feature).


Here is another stab at the problem, with GNU find+sed combo:

$ find . -type f -exec sed -ns '2{/Category X/F;}' {} +

GNU find + GNU xargs feed into Perl can also do it:

find . -type f ! -size 0 -print0 |
xargs -r0 perl -lne '
  (eof||$.==2)&&do{
    print $ARGV if $.==2 && /Category X/;
    close  ARGV; undef $.;
  };
'
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.