Portable check empty directory

While zsh's default behaviour is to give an error, this is controlled by the nomatch option. You can unset the option to leave the * in place the way that bash and dash do:

setopt -o nonomatch

While that command won't work in either of the others, you can just ignore that:

setopt -o nonomatch 2>/dev/null || true ; set *

This runs setopt on zsh, and suppresses the error output (2>/dev/null) and return code (|| true) of the failed command on the others.

As written it's problematic if there is a file, for example, -e: then you will run set -e and change the shell options to terminate whenever a command fails; there are worse outcomes if you're creative. set -- * will be safer and prevent the option changes.


There are several problems with that

set *
if [ -e "$1" ]
then
  echo 'not empty'
else
  echo 'empty'
fi

code:

  • 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 - or +, 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 dotglob or 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 . and ..) in zsh is with the F glob qualifier (F for full, here meaning non-empty):

if [ .(NF) ]; then
  print . is not empty
else
  print "it's empty or I can't read it"
fi

N is the nullglob glob qualifier. So .(NF) expands to . if . is full and nothing otherwise.

After the 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 . nor .. 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"
else
  echo not empty
fi

(assuming no glob-related option is changed from the default (POSIX only specifies noglob) and that the GLOBIGNORE (for bash) and FIGNORE (for 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 $1 == *, 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 . and .. when reported by readdir().

So to make it work in all those shells, you could do:

cwd_empty()
  if [ -n "$ZSH_VERSION" ]; then
    eval '! [ .(NF) ]'
  else
    set .[!.]* '.[!.]'[*] .[.]?* [*] *
    [ "$#$1$2$3$4$5" = '5.[!.]*.[!.][*].[.]?*[*]*' ]
  fi

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, csh, tcsh and fish don't either), and .* including . 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.

That sh emulation is especially there so you can use POSIX code in zsh.

If you want to source a file written in POSIX sh inside 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.


Zsh's syntax is not compatible with sh. It's close enough to look like sh, but not close enough that you can take sh code and run it unchanged.

If you want to run sh code in zsh, for example because you have an sh function or snippet written for sh that you want to use in a zsh script, you can use the emulate builtin. For example, to source a file written for sh in a zsh script:

emulate sh -c 'source /path/to/file.sh'

To write a function or script in sh syntax and make it possible to run it in zsh, put this near the beginning:

emulate -L sh 2>/dev/null || true

In sh syntax, zsh supports all POSIX constructs (it's about as POSIX compliant as bash --posix or ksh93 or mksh). It also supports some ksh and bash extensions such as arrays (0-indexed in ksh, in bash and under emulate sh, but 1-indexed in native zsh) and [[ … ]]. If you want POSIX sh plus ksh globs, use emulate … ksh … instead of emulate … sh …, and add if [[ -n $BASH ]]; then shopt -s extglob; fi for the sake of bash (note that this is not local to the script/function).

The native zsh way to enumerate all the entries in a directory except . and .. is

set -- *(DN)

This uses the glob qualifiers D to include dot files and N to produce an empty list if there are no matches.

The native zsh way to enumerate all the entries in a directory except . and .. is a lot more complicated. You need to list dot files, and if you're listing files in the current directory or in a path that isn't guaranteed to be absolute you need take care in case there is a file name that begins with a dash. Here's one way to do it, by using the patterns ..?* .[!.]* * to list all files except . and .. and removing unexpanded patterns.

set -- ..?*
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi
set -- .[!.]* "$@"
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi
set -- * "$@"
if [ "$#" -eq 1 ] && ! [ -e "$1" ] && ! [ -L "$1" ]; then shift; fi

If all you want to do is to test whether a directory is empty, there's a much easier way.

if ls -A /path/to/directory/ | grep -q '^'; then
  echo "/path/to/directory is not empty"
else
  echo "/path/to/directory is empty"
fi