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?
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