Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I have a set of txt files whose names may contain space or special characters like #.

I have a grep solution grep -L "cannot have" $(grep -l "must have" *.txt) to list all the files who have must have but not cannot have.

For instance, there is a file abc defg.txt which contains only 1 line: must have.

So normally the grep solution should find out abc defg.txt, but it returns:

grep: abc: No such file or directory
grep: defg.txt: No such file or directory

I think for filenames containing #, the grep solution is also invalid.

Could anyone help me amend the grep solution?

share|improve this question
    
# would not be a problem (except with zsh -o globsubst -o extendedglob), but *, ?, [, space, tab, newline would be with bash. –  Stéphane Chazelas May 8 at 8:46

3 Answers 3

IF you're willing to go further afield, awk can do it in one pass:

awk 'function s(){if(a&&!b){print f}} FNR==1{s();f=FILENAME;a=b=0} 
  /must have/{a=1} /cannot have/{b=1} END{s()}' filepattern

For recentish gawk you can simplify with BEGINFILE and ENDFILE. (Like all awk answers you can put the awk commands in a file with -f, and like most you can easily convert to perl if you prefer.)

share|improve this answer
1  
Note however that grep -l/L stops reading at the first match so is likely to be more efficient (also because of the general awk code interpretation overhead). With GNU awk, you could use nextfile to avoid reading the whole file when it can be avoided (when cannot have is found). –  Stéphane Chazelas May 8 at 11:24

Since you're already using GNU specific options (-L), you could do:

grep -lZ -- "must have" *.txt | xargs -r0 grep -L -- "cannot have"

The idea being to use -Z to print the list of file names NUL-delimited and use xargs -r0 to pass that list as arguments to the second grep.

Command substitution, by default, splits on space, tab and newline (and NUL in zsh). Non-zsh shells also perform globbing upon each word resulting of that splitting.

You could do:

IFS='
' # split on newline only
set -f # disable globbing
grep -L -- "cannot have" $(
    set +f # we need globbing for *.txt in this subshell though
    grep -l -- "must have" *.txt
  )

But that would still break on filenames containing newline characters.

In zsh (and zsh only), you can do:

IFS=$'\0'
grep -L -- "cannot have" $(grep -l -- "must have" *.txt)

Or:

grep -L -- "cannot have" ${(ps:\0:)"$(grep -l -- "must have" *.txt)"}
share|improve this answer
1  
What's the point in specifying *.txt as the argument after disabling globbing? –  devnull May 8 at 8:49
    
@devnull, good point. Fixed now. –  Stéphane Chazelas May 8 at 9:15

Consider using find instead and grep using a shell command:

find . -name '*.txt' -print0 | xargs -0 -I{} sh -c 'grep -q "must have" -- "{}" && grep -L "cannot have" -- "{}"'
share|improve this answer
1  
never embed {} in the shell code! –  Stéphane Chazelas May 8 at 8:31

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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