3

summary:

  1. A given system has lots text files with names ~= [type of file].[8-digit date].
  2. To search these files, I like (and wanna keep) using this idiom: find /path/ -name 'file.nnnn*' -print | xargs -e fgrep -nH -e 'text I seek' (where nnnn == 4-digit year)
  3. ... and in the past decade I also made find glob across years like find /path/ -name 'file.201[89]*' -print | xargs ...
  4. ... but now I can't make find glob across 2019 and 2020 with find /path/ -name 'file.20{19,20}*' -print | xargs ...
  5. ... although that "curly-brace globbing" (correct term?) works fine with ls!

Is there a {concise, elegant} way to tell find what I want, without instead doing post-find cleanup (i.e., what I'm doing now) à la

find /path/ -name 'file.*' -print | grep -e '\.2019\|\.2020' | xargs ...

? FWIW, I'd prefer a solution that works with xargs.

details:

I work on a system with lotsa conventions which long precede me and which I cannot change. One of those is, it has lotsa text files with names ~= [type of file].[8-digit date], e.g., woohoo_log.20191230. When searching within these files for some given text, I typically (as in, almost always) use the find ... grep idiom (often using Emacs' M-x find-grep). (FWIW, this is a Linux system with

$ find --version
find (GNU findutils) 4.4.2
...
$ bash --version
GNU bash, version 4.3.30(1)-release (x86_64-pc-linux-gnu)

and I currently lack status to change either of those, if I wanted to.) I often kinda know the year range of the matter-at-hand, and so will try to constrain what find returns (to speed processing), with (e.g.)

find /path/ -type f -name 'file.nnnn*' -print | xargs -e fgrep -nH -e 'text I seek'

where nnnn == 4-digit year. This WFM, and I like (and wanna keep) using the above idiom ... especially since I can also use it to search across years like

find /path/ -type f -name 'file.201[89]*' -print | xargs ...

But this new decade seems to be breaking that idiom, and (to me at least) most oddly. (I wasn't here when the last decade changed.) Suppose I choose text that I know is in a file from 2019 && a file from 2020 (as in, I can open the files and see the text). If I currently do

find /path/ -name 'file.20{19,20}*' -print | xargs ...

grep unexpectedly/annoyingly finishes with no matches found, because

$ find /path/ -name 'file.20{19,20}*' -print | wc -l
0

But if I do

find /path/ -type f -name 'file.*' -print | grep -e '\.2019\|\.2020' | xargs ...

grep returns the expected results. Which is nice, but ... ummm ... that's just ugly, esp since this "curly-brace glob" (please correct me if this usage is incorrect or otherwise deprecated) works from ls! I.e., this shows me the files in the relevant year range (i.e., 2019..2020)

ls -al /path/file.20{19,20}*

Hence I'd like to know:

  1. Am I just not giving find the right glob for this usecase? What do I need to tell find to make it do what ls is capably/correctly doing?
  2. Is this a problem with xargs? If so, I can live with a find ... -exec solution, but ... my brain works better with xargs, so I'd prefer to stay with that if possible. (Call me feebleminded, but -exec's syntax makes my brain hurt.)
  • No nice way, sorry. You may try eval find /path '\(' -name no-such-name-ever '-o -name file.20'{19,20}* '\)'. Or simpler, with GNU find: eval find /path '\(' -false '-o -name file.20'{19,20}* '\)'. You better use a function which creates the command line. – mosvy Jan 15 at 6:17
  • If you don't have any decade-old (or future-dated) files, you could use find /path/ -name 'file.20[12][09]*' ... -- that'll match 2010, 2019, 2020, and 2029. – Gordon Davisson Jan 15 at 7:19
  • I appreciate that you added enough informations about the context as well. Your -name 'file.20{19,20}*' isn't expanded because it is inside single quotes. But if it was, it wouldn't do what you want either as it would expand to something along: -name file.2019* file.2020* or -name "file.2019* file.2020*", which would be incorrect. See Stephane Chazelas's correct posix answer : ` ( -name 'file.2019*' -o -name 'file.2020*' )` is what you'd need. – Olivier Dulac Jan 16 at 0:06
11

With zsh, you could use recursive globbing and its <x-y> glob operator which matches on ranges of decimal numbers:

grep -nHFe 'text I seek' /path/**/file.<2019-2020>*(D-.)

(the (D) to also look into hidden (Dot) dirs as find would; presumably you can omit it if you don't want them, and -. is to restrict to regular file (.) identified after symlink resolution (-)).

Note that it would also match on file.00002020 (as that's a decimal number between 2019 and 2020) and like in your approach on file.20201234 as its file.2020 which matches file.<2019-2020> followed by 1234 which matches *.

The standard (POSIX sh and utilities) way to do it would be with:

find /path \( -name 'file.2019*' -o -name 'file.2020*' \) -type f \
  -exec grep -Fne 'text I seek' /dev/null {} +

(where adding /dev/null gets you the same effect as GNU grep's -H to force the file name to be displayed)

Note that the output of find -print is not compatible with the expected input format of xargs. With GNU utilities, you can use find -print0 and xargs -r0, but that's not needed as find -exec ... {} + has the same behaviour, is shorter and more portable.

6

In ls -al /path/file.20{19,20}*, it's not ls that does anything with {19,20}*. In that command, the shell performs brace expansion and globbing on /path/file.20{19,20}* as it is not quoted:

bash-5.0$ set -x
bash-5.0$ echo {a,b}
+ echo a b
a b
bash-5.0$ ls {a,b}
+ ls a b
ls: cannot access 'a': No such file or directory
ls: cannot access 'b': No such file or directory
bash-5.0$ find -iname {a,b}
+ find -iname a b
find: paths must precede expression: `b'

In find /path/ -name 'file.20{19,20}*', 'file.20{19,20}*' is quoted, so the shell leaves it alone, and find then applies its own pattern matching rules, which do not support brace expansion. Here quoting the GNU find manual:

Braces within the pattern (‘{}’) are not considered to be special (that is, find . -name 'foo{1,2}' matches a file named foo{1,2}, not the files foo1 and foo2.

If you really want to use brace expansion to recursively search a directory, in bash, you can enable recursive globbing (globstar) (and possibly dotglob to look into hidden directory like find would), and use printf with xargs:

shopt -s globstar
printf "%s\0" /path/**/file.20{19,20}* | xargs -0 ...

Or you can use find with -regex instead of -name as supported by a few find implementations. With GNU find:

find  /path -regextype posix-extended -regex '.*/file.20(19|20)[^/]*'
  • 6
    \( -name 'file.2019*' -o -name 'file.2020*' \) would be shorter to type, and harder to get wrong (arguably), and more portable. – Kusalananda Jan 15 at 6:54
  • 1
    Or -regex .*/file.20\(19\|20\)[^/]* without -regextype posix-extended, also shorter. – muru Jan 15 at 7:31
0

It's not a general case answer to your question, but there may be an easy way to do it depending on how much file history you have. I have often run into a similar situation with months when looking for things in September / October. One easy workaround is to use the a pattern like this:

file.20[12][90]*

It's not identical, because in addition to 2019 and 2020, it will also match 2010 and 2029. Presumably you don't have any files yet dated 2029. If your archive doesn't go back as far as 2010, than this should be functionally equivalent.

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.