Search code examples
bashglob

Extended glob does not expand as expected


I have a bash script named except.sh which is passed a list of files/directories like so:

$ ls
a b c d/
$ ./except.sh b c

When calling except this way, it should expand to a d/ i.e. all files/directories except the given names.

Here's how I tried to implement this:

#!/usr/bin/env bash

# enable extended globbing
shopt -s extglob

# set IFS to | so that $* expands correctly
IFS='|'

printf '%s' !("$*")

Given b c as parameters, the last line should expand to

printf '%s' !(b|c)

resulting in a d being printed. But to my suprise,

abcd

is printed. What am I doing wrong?


Solution

  • The problem is that $* is in double quotes, which means that its contents will not be treated as a pattern, just like echo "*" does not expand the asterisk. Combining the outer pattern with the inner quoted portion automatically escapes the latter, so !("b|c") is treated like !(b\|c). Negation of the nonexistent b|c file naturally expands to all files in the directory.

    An additional problem is that extended globbing is messed up by IFS being set to |, so you must reset it before expanding the pattern. Therefore, you must do the expansion in two steps: first, calculate the pattern, then reset IFS and expand it:

    #!/usr/bin/env bash
    
    # enable extended globbing
    shopt -s extglob
    
    # temporarily set IFS to | so that $* expands to part an extended pattern
    old_ifs=$IFS
    IFS='|'
    pattern="!($*)"
    IFS=$old_ifs
    
    printf '%s' $pattern