3

Save I have a series of files called {1..40}0.txt (so 10.txt through 400.txt by 10):

➜ ls
10.txt  60.txt   110.txt  160.txt  210.txt  260.txt  310.txt  360.txt
20.txt  70.txt   120.txt  170.txt  220.txt  270.txt  320.txt  370.txt
30.txt  80.txt   130.txt  180.txt  230.txt  280.txt  330.txt  380.txt
40.txt  90.txt   140.txt  190.txt  240.txt  290.txt  340.txt  390.txt
50.txt  100.txt  150.txt  200.txt  250.txt  300.txt  350.txt  400.txt

I want to delete all the files that are between 100 and 300, but aren't evenly divisible by 100. Ie. leave {1..9}0.txt, 100.txt, 200.txt, 3{1..9}0.txt:

rm 110.txt  130.txt  150.txt  170.txt  190.txt  220.txt  240.txt  260.txt  280.txt
   120.txt  140.txt  160.txt  180.txt  210.txt  230.txt  250.txt  270.txt  290.txt

In zsh, I can glob the files between 100 and 300 via <100-300>.txt and I can glob those evenly divisible by 100 via ^(*00).txt (with the extendedglob option set).

Is it possible to chain file glob patterns one after the other? So glob the 100-300 files, then glob the remaining files for those without the 00?

Note, that the way to do this in a single glob is <100-300>.txt~(*00).txt, where the ~ provides an exclusion (see zsh documentation). Edit: playing glob golf, I can get it down to <10-30>0*~*00*.

4
  • Did you not just answer your question in your question? What's wrong with (<100-300>~*00).txt or your <100-300>.txt~*00.txt? May 20, 2020 at 13:49
  • @StéphaneChazelas I answered the example problem in the question, not the question itself. The problem with <100-300>.txt~*00.txt is that it took me about an hour of scouring through documentation to find it. Since I'm doing regex-esque commands here, my instinct is to chain multiple regex together like I would do when using grep on a list. Hence the question regarding whether or not that's possible. May 20, 2020 at 16:22
  • 1
    I still don't get it. How would you do it in regexp? Do you mean something like (<101-199>|<201-299>).txt. What do you mean by chaining globs together (if it's not that ~ except zsh operator? BTW, few regexp syntaxes have AND or NOT let alone AND-NOT operators). May 20, 2020 at 18:13
  • @StéphaneChazelas As an example, if the text files were a list, pipe the results of one grep to another grep: ls | grep -E '^[1-2][0-9]0.*' | grep -E '^[1-2][^0].*'. That is what I mean by chaining. Doing an operation back to back, where the result of the first is the input of the second. May 20, 2020 at 18:50

2 Answers 2

2
$ touch {10..400..10}.txt
$ print -rC1 -- <100-300>.txt(e['(( $REPLY:r % 100 != 0 ))'])
110.txt
120.txt
130.txt
140.txt
150.txt
160.txt
170.txt
180.txt
190.txt
210.txt
220.txt
230.txt
240.txt
250.txt
260.txt
270.txt
280.txt
290.txt

The touch above creates the files.

The <100-300>.txt bit on the next line selects the files that start with a number between 100 and 300 inclusively.

The glob qualifier, (e['(( $REPLY:r % 100 != 0 ))']), selects the filenames whose "root" (name without extension, had via :r) is not divisible by 100.


You could do it all via the glob qualifier too, of course:

*.txt(e['(( r=$REPLY:r, r >= 100 && r <= 300 && r % 100 != 0 ))'])
1

If by chaining globs one after the other, you mean the equivalent of grep pattern | grep another-pattern or grep pattern | grep -v excluded-pattern, that is get one list of matching files and later, further refine that list in a second iteration, you'd use the ${array:#exclude-pattern} and ${(M)array:#only-pattern} operators:

list=(*(N))                      # all non-hidden files
list=(${(M)list:#<100-300>.txt}) # only retain the ones matching that pattern
list=(${list:#*00.txt})          # remove the ones matching that other pattern

Otherwise, in your case, the most obvious approach (to me anyway) is to use that ~ AND-NOT (except) operator (needs extendedglob). You can also do it as:

list=((<100-300>~*00).txt(N))

"Chaining" glob operators are:

  • OR: | (like in EREs, but only inside (...) as | is also the pipe operator): (patternA|patternB)
  • AND-NOT: ~ (with extendedglob): patternA~patternB
  • NOT: ^ (with extendedglob) or !(pattern) (with kshglob and without histexpand or with a different character as the first character of $HISTCHARS)
  • AND: ~^ (that is, AND-NOT-NOT, with extendedglob), or !(!(patternA)|!(patternB)) with kshglob.

As @Kusalananda has shown, you can also use the e glob qualifier to further refine the glob selection based on the execution of arbitrary shell code.

You could even do things like:

list=(*(Ne['[[ $REPLY = <100-300>.txt ]] && [[ $REPLY != *00.txt ]]']))
3
  • Correct me if I'm wrong, but is seems like you're basically just grepping rather than actual file globing. ie. Writing out a list of files, then operating on that text. This seemingly runs into the same trap described in Why you shouldn't parse the output of ls (though feel free to correct me if it doesn't fall into that trap). In my mind, the solution to this is to "chain" globs together by "passing" the "file objects" between the globs. I maybe overthinking this though. May 20, 2020 at 19:07
  • 1
    @JamesWright, the problem with parsing ls is that it is text-line based and lines can't hold arbitrary file names (file paths can be any sequence of non-NUL bytes, doesn't even have to be text). zsh arrays members can hold any sequence of byte (including NUL), so can hold file paths alright. Note that globbing in any case involves pattern match on directory entries, there's no such thing as a "file object" concept involved. In dir/*.txt, the shell reads the contents of dir, and matches the *.txt pattern against the entries to generate the list. May 21, 2020 at 6:55
  • Ah, ok. Didn't realize that was feeding into an array. That makes sense. Thanks! May 21, 2020 at 16:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .