7

With Bash and Dash, you can check for an empty directory using just the shell (ignore dotfiles to keep things simple):

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

However I recently learned that Zsh fails spectacularly in this case:

% set *
zsh: no matches found: *

% echo "$? $#"
1 0

So not only does the set command fail, but it doesn't even set $@. I suppose I could test if $# is 0, but it appears that Zsh even stops execution:

% { set *; echo 2; }
zsh: no matches found: *

Compare with Bash and Dash:

$ { set *; echo 2; }
2

Can this be done in a way that works in bash, dash and zsh?

6

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.

2

Most portable way would be via set and globstar for all POSIX-compliant shells. This has been shown in Gilles's answer on a related question. I've adapted the method slightly into a function:

rm -rf empty_dir/
mkdir empty_dir/
pwd
cd empty_dir/
pwd
dir_empty(){

    # https://stackoverflow.com/a/9911082/3701431
    if [ -n "$ZSH_VERSION" ]; then
        # https://unix.stackexchange.com/a/310553/85039
        setopt +o nomatch 
    fi    

    set -- * .*
    echo "$@"
    for i; do
        [ "$i" = "." ] || [ "$i" = ".." ] && continue
        [ -e "$i" ] && echo "Not empty" && return 1
    done
    echo "Empty" && return 0
}
dir_empty
touch  '*'
dir_empty

The big problem with zsh is that while ksh and bash behave in more or less consistent manner - that is when we do set * .* you will have 3 positional parameters * . .. in really empty directory - in zsh you will get * .* as positional parameters. Luckily at least for i ; do ... done to iterate over positional parameters works consistently. The rest is just iteration and check for existence of the filename, with . and .. skipped.


Try it online in ksh!

Try it online in zsh!

  • @Three I've adapted the answer to check for zsh. Unfortunatelly for us zsh decided to go the weird way instead of similar behavior to bash or other shells, since according to POSIX: "If the pattern does not match any pathnames, the returned number of matched paths is set to 0, and the contents of pglob->gl_pathv are implementation-defined." source – Sergiy Kolodyazhnyy Jan 7 at 2:25
  • turning nomatch everywhere risks breaking code that assumes the default (and very sensible) setting that sh and bash get wrong, so the change really should be localized only to this function – thrig Jan 7 at 3:02
  • @thrig Well, considering that so far others haven't found a way to make glob work without nomatch, that's the best we got. We can also toggle it back before function exits, of course. Or we could just abandon shell ways and just use something else, like find for instance. – Sergiy Kolodyazhnyy Jan 7 at 3:16
  • @Three I've revised the answer again. Probably this is the best I can do, as zsh seems to favor features instead of consistency. Hope this helps somewhat. – Sergiy Kolodyazhnyy Jan 7 at 3:18
  • @SergiyKolodyazhnyy yes thanks - I would just avoid Zsh totally but its used on Manjaro - maybe I will avoid that too :) – Three Jan 7 at 3:20
0

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 [ -n "$(find /path/to/directory | head -n 1)" ]; then
  echo "/path/to/directory is not empty"
else
  echo "/path/to/directory is empty"
fi
  • shorter if ls -A | read q; then echo not empty; else echo empty; fi – Three Jan 13 at 14:25

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.