ryeballar
ryeballar

Reputation: 30118

Shell globbing exclude directory patterns

Disregarding commands that can be used in association with shell globs, I would like to accomplish excluding specific directory patterns across all directories and sub-directories from a glob pattern.

Let's say I have this directory structure:

+-- a
    +-- a1
         +-- assets
                  +-- a1-1
                         +-- a1-1.js
                  +-- a1-2
                         +-- a1-2.js
    +-- a2
         +-- a2.js
+-- b
    +-- b1
         +-- b1-2
                +-- b1-2-3
                         +-- assets
                                  +-- b1-2-3.js
    +-- b.js
+-- c
    +-- c.js

I would like to list all js files that do not have assets in their file paths.

What I've tried so far:

$ shopt -s globstar extglob
$ ls ./**/!(assets)/**/*.js

Not only did the pattern above achieve its goal, it even shows duplicate outputs. I am aware that I can do something like this:

$ ls ./**/*.js | grep -v "assets"

Or any other commands that can be piped, I simply want a pure shell glob pattern.

Expected Output:

a/a2/a2.js  
b/b.js  
c/c.js

Upvotes: 9

Views: 9224

Answers (4)

Superclarkk
Superclarkk

Reputation: 11

If this is something you search for a lot, then I'd just write a simple script.

For testing, set up files and directories, as described in the question:

(notice added c/assets.js to test edge case):

mkdir -p a/a1/assets/a1-1 a/a1/assets/a1-2 a/a2 b/b1/b1-2/b1-2-3/assets c
touch a/a1/assets/a1-1/a1-1.js a/a1/assets/a1-2/a1-2.js a/a2/a2.js b/b1/b1-2/b1-2-3/assets/b1-2-3.js b/b.js c/c.js
touch c/assets.js

The script:

#!/bin/bash

shopt -s globstar  # enable ** to recurse all the directories

files=()  # create array variable

for f in **/*.js; do  # glob pattern match all .js files
  if [[ ! "$f" =~ "assets/" ]]; then  # regex match all paths that do NOT contain an 'assets' directory
    files+=("$f")  # add it to the files array
  fi
done

printf %s\\n "${files[@]}"  # print the files array

Results:

a/a2/a2.js
b/b.js
c/assets.js
c/c.js

Upvotes: 1

anishsane
anishsane

Reputation: 20980

Michael's answer is right. ** matches too much (greedy match), including assets.

So, with this tree:

.
|-- a
|   |-- a1
|   |   +-- assets
|   |       |-- a1-1
|   |       |   +-- a1-1.js
|   |       +-- a1-2
|   |           +-- a1-2.js
|   +-- a2
|       +-- a2.js
|-- assets
|   +-- xyz.js
|-- b
|   |-- b1
|   |   +-- b1-2
|   |       +-- b1-2-3
|   |           |-- assets
|   |           |   +-- b1-2-3.js
|   |           +-- test
|   |               |-- test2
|   |               |   +-- test3
|   |               |       +-- test4
|   |               |           +-- test4.js
|   |               +-- test.js
|   +-- b.js
|-- c
|   +-- c.js
+-- x.js

The .js files are:

$ find . -name '*.js'
./x.js
./assets/xyz.js
./a/a2/a2.js
./a/a1/assets/a1-2/a1-2.js
./a/a1/assets/a1-1/a1-1.js
./c/c.js
./b/b.js
./b/b1/b1-2/b1-2-3/test/test2/test3/test4/test4.js
./b/b1/b1-2/b1-2-3/test/test.js
./b/b1/b1-2/b1-2-3/assets/b1-2-3.js

There is a bash variable GLOBIGNORE to do exactly what you are trying to do.

So, this would work:

$ GLOBIGNORE='**/assets/**:assets/**:**/assets'
$ ls -1 **/*.js
a/a2/a2.js
b/b1/b1-2/b1-2-3/test/test2/test3/test4/test4.js
b/b1/b1-2/b1-2-3/test/test.js
b/b.js
c/c.js
x.js

Upvotes: 8

Georg P.
Georg P.

Reputation: 3164

This might be the perfect time for you to start using zsh :-) Removing matches is simply done with ~:

% setopt extended_glob
% ls -1 **/*.js~*assets*
a/a2/a2.js
b/b.js
c/c.js

Upvotes: 1

Michael Vehrs
Michael Vehrs

Reputation: 3373

The problem is that ** matches too much or too little in your case. In particular, the ** preceding !(assets) will match a1/assets/, then !(assets) will match a1-1 and the following ** will match nothing, so that the overall match succeeds.

Consider this simplified example:

touch a/a1/x.js a/a2/y.js a/a3/z.js
ls ./*/!(a2)/*.js
ls ./**/!(a2)/**/*.js

The first ls works as intended, the second does not.

Upvotes: 3

Related Questions