Search code examples
zshglob

Zsh glob: objects recursivley under sudirectories, excluding current/base directory


I am trying to do a file name generation of all objects (files, directories, and so on) recursively under all subdirectories of the current directory. Excluding the objects in said current directory.

In other words, given:

--dir1 --dir2.1
|      | dir2.2 --file3.1
|      --file2.1
--file1

I want to generate:

./dir2.1
./dir2.2
./dir2.2/file3.1
./file2.1

I have set the EXTENDED_GLOB option, and I assumed that the following pattern would do the trick:

./**/*~./*

But it returns:

zsh: no matches found: ./**/*~./*

I don't know what the problem is, it should work.

./**/* gives:

./dir1
./dir2.1
./dir2.2
./dir2.2/file3.1
./file2.1
./file1

And ./* gives:

./dir1
./file1

How come ./**/*~./* fails? And more important, how can I generate the name of the elements recursively in the subdirectories excluding the elements in current/base directory?

Thanks.


Solution

  • The (1)x~y glob operator uses y as a shell's ordinally pattern matching rather than a file name generation, so ./**/*~./* gives "no matches found":

    % print -l ./**/*~./*
    ;# ./dir1   # <= './*' matches, so exclude this entry
    ;# ./dir2.1 # <= './*' matches, so exclude this entry
    ;# .. # ditto...
    ;# => finally, no matches found
    

    The exclusion pattern ./* matches everything generated by the glob ./**/*, so zsh finally yields "no matches found". (zsh does not do filename generations for the ~y part.)

    We could make the exclusion pattern a little more precise/complicated form for excluding the elements in current directory. Such that it starts with ./ and has one or more characters other than /.

    % print -l ./**/*~./[^/]## ;# use '~./[^/]##' rather than '~./*'
    ./dir1/dir2.1
    ./dir1/dir2.2
    ./dir1/dir2.2/file3.1
    ./dir1/file2.1
    

    Then, to strip the current-dir-component /dir1, we could use the (2)estring glob qualifier, such that it removes the first occurrence of /[^/]## (for example /dir1):

    # $p for avoiding repetitive use of the exclusion pattern.
    % p='./[^/]##'; print -l ./**/*~${~p}(e:'REPLY=${REPLY/${~p[2,-1]}}':)
    ./dir2.1
    ./dir2.2
    ./dir2.2/file3.1
    ./file2.1
    

    Or to strip it using ordinally array/replace rather than estring glob qualifier:

    % p='./[^/]##'; a=(./**/*~${~p}) ; a=(${a/${~p[2,-1]}}); print -l $a
    ./dir2.1
    ./dir2.2
    ./dir2.2/file3.1
    ./file2.1
    

    At last, iterating over current dir's dirs could do the job, too:

    a=(); dir=;
    for dir in *(/); do
      pushd "$dir"
      a+=(./**/*)
      popd
    done
    print -l $a
    #=> ./dir2.1
        ./dir2.2
        ./dir2.2/file3.1
        ./file2.1
    

    Here are some zsh documents.

    (1)x~y glob operator:

    x~y

    (Requires EXTENDED_GLOB to be set.) Match anything that matches the pattern x but does not match y. This has lower precedence than any operator except ‘|’, so ‘*/*~foo/bar’ will search for all files in all directories in ‘.’ and then exclude ‘foo/bar’ if there was such a match. Multiple patterns can be excluded by ‘foo~bar~baz’. In the exclusion pattern (y), ‘/’ and ‘.’ are not treated specially the way they usually are in globbing.

    --- zshexpn(1), x~y, Glob Operators

    (2)estring glob qualifier:

    estring
    +cmd

    ...
    During the execution of string the filename currently being tested is available in the parameter REPLY; the parameter may be altered to a string to be inserted into the list instead of the original filename.

    --- zshexpn(1), estring, Glob Qualifiers