0

I have following text in a text file

$ cat test
20180618:
20180619:
20180620:
20180621:
20180622:
20180623:
20180624:

I have tried to grep with the range of numbers like below,

$ grep 201806{19..21} test
grep: 20180619: No such file or directory
grep: 20180620: No such file or directory
grep: 20180621: No such file or directory

I'm getting above error on both ZSH and bash. It seems grep taking the search string as files.

I have tried the another way:

$ grep 201806* test       
zsh: no matches found: 201806*

I am getting that error only on ZSH. What is the right way to use * in ZSH and how can I tell grep to grep range of numbers?

1
grep 201806{19..21} test

is expanded by the shell to:

grep 20180619 20180620 20180621 test

Which grep understands as looking for 20180619 inside the 3 files, 20180620, 20180621 and test.

If you change it to:

grep -e201806{19..21} test

Then that's expanded to:

grep -e20180619 -e20180620 -e20180621 test

Which gives 3 expressions for grep to search for in test.

Or you can do:

printf '%s\n' 201806{19..21} | grep -f - test

Where we pass the expressions as a number of lines of input to grep (with some implementations, you may need /dev/stdin in place of -).

With zsh specifically, you can also make it:

numbers=({19..21} 25 31)
grep -E "201801(${(j:|:)numbers})" test

Where we use the (j:|:) parameter expansion flag to join the array elements with | (the extended regular expression alternation operator) so it can be used as an ERE.

Or you could tie that array to a regexp scalar with:

$ typeset -T re numbers '|'
$ numbers=({19..21} 25 31)
$ echo $re
19|20|21|25|31

While regular expressions usually don't have number range matching capabilities, zsh patterns (which with extendedglob are functionally equivalent to regular expressions) do with the <x-y> operator (only for sequences of decimal digits):

print -rl -- ${(M)${(f)"$(<test)"}:#*201806<19-21>*}
  • +1 for grep -e201806{19..21} test. – smc Jun 22 '18 at 15:02
6

Yes, grep only treats its first argument as a regular expression by default.

This means that

grep {1..9} file

which expands to

grep 1 2 3 4 5 6 7 8 9 file

would invoke grep with 1 as the expression to match in the other operands, and that these other operands would be expected to be filenames.

Your other command:

grep 201806* test

This would try to match 201806* as a filename globbing pattern. You have no files whose names start with 201806 in the current directory so the zsh shell fails to expand the pattern and gives you the error message no matches found.

In other Bourne-like shells, if the pattern hadn't matched any filenames, it would have remained unexpanded and used as the regular expression with grep. The expression 201806*, when taken as a regular expression, matches 20180 followed by zero or more 6 characters, e.g. 2018066666.

Instead, you may want to construct a regular expression to match your range:

grep -E '201806(19|20|21)' test

or

grep -E '201806(19|2[01])' test

The -E is needed to have grep understand | (alternation) in the expression (this alternation makes it an extended regular expression).


You could also construct a regular expression from a brace expansion:

set -- {19..21}
re=$( IFS='|'; printf '201806(%s)' "$*" )

grep -E "$re" test

This would first set the positional parameters, $1, $2 and $3, to the wanted numbers in the range. The variable re would then be set to 201806(%s) where printf would replace %s with these numbers delimited by |.

The grep call would use 201806(19|20|21) as the regular expression.

  • Or it could be done with a newline separated list of fixed strings maybe? i.e. printf '%s\n' 201806{19..21} | grep -Ff- test – steeldriver Jun 21 '18 at 12:06
  • @steeldriver Yes, but I think you have to use /dev/stdin as the input file name, unless your grep interprets - as standard input. – Kusalananda Jun 21 '18 at 12:08
  • It does - I didn't realize others didn't :( – steeldriver Jun 21 '18 at 12:10
2

unquoted strings are interpreted by the shell before command is executed, in your case, the command you tried would be expanded to grep 20180619 20180620 20180621 test

$ echo grep 201806{19..21} test
grep 20180619 20180620 20180621 test

One workaround is to specify regular expression alternation:

$ grep -E '201806(19|20|21)' test
20180619:
20180620:
20180621:

You can construct a numeric range with regular expressions, but it is not easy. See https://www.regular-expressions.info/numericranges.html for details


Another option is to use awk

$ awk -F: '$1>=20180619 && $1<=20180621' ip.txt
20180619:
20180620:
20180621:

Here, we split the line on : and then compare the first field $1 with range required

1
  1. POSIX shell (no bash) with utils:

    seq 20180618 20180624 | grep -f - test
    
  2. numgrep:

    numgrep '/20180618..20180624/' < test
    
  • 2
    (just to clear some possible confusion, note that neither seq nor numgrep are POSIX commands). – Stéphane Chazelas Jun 22 '18 at 17:13

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.