There are several problems with that
if [ -e "$1" ]
echo 'not empty'
- if the
nullglob option (from
zsh but now supported by most other shells) is enabled,
set * becomes
set which lists all the shell variables (and functions in some shells)
- if the first non-hidden file has a name that starts with
+, it will be treated as an option by
set. Those two issues can be fixed by using
set -- * instead.
* expands only non-hidden files, so it's not a test whether the directory is empty or not but whether it contains non-hidden files or not. With some shells, you can use a
globdot option or play with a
FIGNORE special variable depending on the shell to work around that.
[ -e "$1" ] tests whether a
stat() system call succeeds or not. If the first file is a symlink to an inaccessible location, that will return false. You shouldn't need to
stat() (not even
lstat()) any file to know whether a directory is empty or not, only check that it has some content.
* expansion involves opening the current directory, retrieving all the entries, storing all the non-hidden one and sorting them, which is also quite inefficient.
The most efficient way to check if a directory is non-empty (has any entry other than
zsh is with the
F glob qualifier (
F for full, here meaning non-empty):
if [ .(NF) ]; then
print . is not empty
print "it's empty or I can't read it"
N is the
nullglob glob qualifier. So
.(NF) expands to
. is full and nothing otherwise.
lstat() on the directory, if
zsh finds it has a link-count greater than 2, then that means it has at least one subdirectory so is not empty, so we don't even need to open that directory (that also means, in that case we can tell that the directory is non-empty even if we don't have read access to it). Otherwise, zsh opens the directory, reads its content and stops at the first entry that is neither
.. without having to read, store nor sort everything.
With POSIX shells (
zsh only behaves (more) POSIXly in
sh emulation), it is very awkward to check that a directory is non-empty with globs only.
One way is with:
set .[!.]* '.[!.]'[*] .[.]?* [*] *
if [ "$#$1$2$3$4$5" = '5.[!.]*.[!.][*].[.]?*[*]*' ]; then
echo "empty or can't read"
echo not empty
(assuming no glob-related option is changed from the default (POSIX only specifies
noglob) and that the
ksh) variables are not set, and that (for
yash) none of the file names contain sequences of bytes not forming valid characters).
The idea is that in POSIX shells, when a glob doesn't match, it is left unexpanded (a misfeature introduced by the Bourne shell in the late 70s). So with
set -- *, if we get
*, we don't know whether it was because there was no match or whether there was a file called
Your (flawed) approach to work around that was to use
[ -e "$1" ]. Here instead, we use
set -- [*] *. That allows to disambiguate the two cases, because if there is no file, the above will stay
[*] *, and if there is a file called
*, that becomes
* *. We do something similar for hidden files. That is a bit awkward because of yet another misfeature of the Bourne shell (also fixed by
zsh, the Forsyth shell, pdksh and
fish) whereby the expansion of
.* does include the special (pseudo-)entries
.. when reported by
So to make it work in all those shells, you could do:
if [ -n "$ZSH_VERSION" ]; then
eval '! [ .(NF) ]'
set .[!.]* '.[!.]'[*] .[.]?* [*] *
[ "$#$1$2$3$4$5" = '5.[!.]*.[!.][*].[.]?*[*]*' ]
In any case, the syntax of
zsh by default is not compatible with the POSIX sh syntax as it has fixed most of the major issues in the Bourne shell (well before POSIX.2 was first published) in a non-backward compatible way, including that
* left unexpanded when there's no match (pre-Bourne shells didn't have that issue,
fish don't either), and
.. but several others like split+glob performed upon parameter or arithmetic expansion, so you can't expect code written in the POSIX sh to always work in
zsh unless you turn on
sh emulation is especially there so you can use POSIX code in
If you want to
source a file written in POSIX
zsh, you can do:
emulate sh -c 'source that-file'
Then that file will be evaluated in
sh emulation and any function declared within will retain that emulation mode.