How to check if a glob has an expansion?
With bash
:
shopt -s nullglob
files=(/mydir/*.gz)
((${#files[@]} == 0)) || gzip -d -- "${files[@]}"
With zsh
:
files=(/mydir/*.gz(N))
(($#files == 0)) || gzip -d -- $files
Note that in zsh
, without (N)
, like in pre-Bourne shells, csh or tcsh, if the glob doesn't match, the command is not run, you'd only do the above to avoid the resulting error message (of no match found as opposed to gzip
failing on the expanded glob in the case of bash
or other Bourne-like shells). You can achieve the same result with bash
with shopt -s failglob
.
In zsh
, a failing glob is a fatal error that causes the shell (when not interactive) to exit. You can prevent your script from exiting in that case either by using a subshell or using zsh
error catching mechanism ({ try-block; } always { error-catching; }
), (or by setting the nonomatch
(to work like sh
), nullglob
or noglob
option of course, though I wouldn't recommend that):
$ zsh -c 'echo zz*; echo not output'
zsh:1: no matches found: zz*
$ zsh -c '(echo zz*); echo output'
zsh:1: no matches found: zz*
output
$ zsh -c '{echo zz*;} always {TRY_BLOCK_ERROR=0;}; echo output'
zsh:1: no matches found: zz*
output
$ zsh -o nonomatch -c 'echo zz*; echo output'
zz*
output
With ksh93
ksh93
eventually added a mechanism similar to zsh
's (N)
glob qualifier to avoid having to set a nullglob
option globally:
files=(~(N)/mydir/*.gz)
((${#files[@]} == 0)) || gzip -d -- "${files[@]}"
POSIXly
Portably in POSIX sh
, where non-matching globs are passed unexpanded with no way to disable that behaviour (the only POSIX glob related option is noglob
to disable globbing altogether), the trick is to do something like:
set -- /mydir/[*].gz /mydir/*.gz
case $#$1$2 in
'2/mydir/[*].gz/mydir/*.gz') : no match;;
*) shift; gzip -d -- "$@"
esac
The idea being that if /mydir/*.gz
doesn't match, then it will expand to itself (/mydir/*.gz
). However, it could also expand to that if there was one file actually called /mydir/*.gz
, so to differentiate between the cases, we also use the /mydir/[*].gz
glob that would also expand to /mydir/*.gz
if there was a file called like that.
As that's pretty awkward, you may prefer using find
in those cases:
find /mydir/. ! -name . -prune ! -name '.*' \
-name '*.gz' -type f -exec gzip -d {} +
The ! -name . -prune
is to not look into subdirectories (some find
implementations have -depth 1
or -mindepth 1 -maxdepth 1
as an equivalent). ! -name '.*'
is to exclude hidden files like globs do.
A benefit is that it still works if the list of files is too big to fit in the limit of the size of arguments to an executed command (find
will run several gzip
commands if need to avoid that, ksh93
and zsh
also have mechanisms to work around that).
Another benefit is that you will get error messages if find
cannot read the content of /mydir
or can't determine the type of the files (globs would just silently ignore the problem and act as if the corresponding files don't exist).
A small down side is that you lose the exact value of gzip
's exit status (if any one gzip
invocation fails with a non-zero exit status, find
will still exit with a non-zero exit status (though not necessarily the same) though, so that's good enough for most use cases).
Another benefit is that you can add the -type f
to avoid trying to uncompress directories or fifos/devices/sockets... whose name ends in .gz
. Except in zsh
(*.gz(.)
for regular files only), globs cannot filter by file types, you'd need to do things like:
set --
for f in /mydir/*.gz
[ -f "$f" ] && [ ! -L "$f" ] && set -- "$@" "$f"
done
[ "$#" -eq 0 ] || gzip -d -- "$@"
One way to do this is:
shopt -s nullglob
for f in /mydir/*.gz; do
gzip -d /mydir/*.gz
break
done
The for
loop with nullglob set on will only execute the loop at all if the glob has an expansion, and the unconditional break
statement ensures that the gzip
command will only be executed once.
It's a bit funny because it uses a for
loop as an if
, but it works.