My tldr answer is:
function emptydir {
[ "$1/"* "" = "" ] 2> /dev/null &&
[ "$1/"..?* "" = "" ] 2> /dev/null &&
[ "$1/".[^.]* "" = "" ] 2> /dev/null ||
[ "$1/"* = "$1/*" ] 2> /dev/null && [ ! -e "$1/*" ] &&
[ "$1/".[^.]* = "$1/.[^.]*" ] 2> /dev/null && [ ! -e "$1/.[^.]*" ] &&
[ "$1/"..?* = "$1/..?*" ] 2> /dev/null && [ ! -e "$1/..?*" ]
}
It's POSIX compliant and, not that it matters much, it's typically faster than the solution that lists the directory and pipes the output to grep.
Usage:
if emptydir adir
then
echo "nothing found"
else
echo "not empty"
fi
I like the answer https://unix.stackexchange.com/a/202276/160204, which I rewrite as :
function emptydir {
! { ls -1qA "./$1/" | grep -q . ; }
}
It lists the directory and pipe the result to grep. Instead, I propose a simple function that is based on glob expansion and comparison.
function emptydir {
[ "$(shopt -s nullglob; echo "$1"/{,.[^.],..?}*)" = "" ]
}
This function is not standard POSIX and calls a subshell with $()
. I explain this simple function first so that we can better understand the final solution (see the tldr answer above) later.
Explanation:
The left hand side (LHS) is empty when no expansion occurs, which is the case when the directory is empty. The nullglob option is required because otherwise when there is no match, the glob itself is the result of the expansion. (Having the RHS matches the globs of the LHS when the directory is empty does not work because of false positives that occur when a LHS glob matches a single file named as the glob itself: the *
in the glob matches the substring *
in the file name.) The brace expression {,.[^.],..?}
covers hidden files, but not ..
or .
.
Because shopt -s nullglob
is executed inside $()
(a subshell), it does not change the nullglob
option of the current shell, which is normally a good thing. On the other hand, it's a good idea to set this option in scripts, because it's error prone to have a glob returns something when there is no match. So, one could set the nullglob option at the start of the script and it will not be needed in the function. Let's keep this in mind: we want a solution that works with the nullglob option.
Caveats:
If we don't have read access to the directory, the function reports the same as if there was an empty directory. This applies also to a function that lists the directory and grep the output.
The shopt -s nullglob
command is not standard POSIX.
It uses the subshell created by $()
. It's not a big deal, but it's nice if we can avoid it.
Pro:
Not that it really matters, but this function is four times faster than the previous one, measured with the amount of CPU time spent in the kernel within the process.
Other solutions:
We can remove the non POSIX shopt -s nullglob
command on the LHS and put the string "$1/* $1/.[^.]* $1/..?*"
in the RHS and eliminate separately the false positives that occur when we only have files named '*'
, .[^.]*
or ..?*
in the directory:
function emptydir {
[ "$(echo "$1"/{,.[^.],..?}*)" = "$1/* $1/.[^.]* $1/..?*" ] &&
[ ! -e "$1/*" ] && [ ! -e "$1/.[^.]*" ] && [ ! -e "$1/..?*" ]
}
Without the shopt -s nullglob
command, it now makes sense to remove the subshell, but we have to be careful because we want to avoid word splitting and yet allow glob expansion on the LHS. In particular quoting to avoid word splitting does not work, because it also prevents glob expansion. Our solution is to consider the globs separately:
function emptydir {
[ "$1/"* = "$1/*" ] 2> /dev/null && [ ! -e "$1/*" ] &&
[ "$1/".[^.]* = "$1/.[^.]*" ] 2> /dev/null && [ ! -e "$1/.[^.]*" ] &&
[ "$1/"..?* = "$1/..?*" ] 2> /dev/null && [ ! -e "$1/..?*" ]
}
We still have word splitting for the individual glob, but it's ok now, because it will only result in an error when the directory is not empty. We added 2> /dev/null, to discard the error message when there are many files matching the given glob on the LHS.
We recall that we want a solution that works with the nullglob option as well. The above solution fails with the nullglob option, because when the directory is empty, the LHS's is also empty. Fortunately, it never says that the directory is empty when it is not. It only fails to say that it is empty when it is. So, we can manage the nullglob option separately. We cannot simply add the cases [ "$1/"* = "" ]
etc. because these will expand as [ = "" ]
, etc. which are syntactically incorrect. So, we use [ "$1/"* "" = "" ]
etc. instead. We again have to consider the three cases *
, ..?*
and .[^.]*
to match the hidden files, but not .
and ..
. These will not interfere if we don't have the nullglob option, because they also never say that it is empty when it is not. So, the final proposed solution is:
function emptydir {
[ "$1/"* "" = "" ] 2> /dev/null &&
[ "$1/"..?* "" = "" ] 2> /dev/null &&
[ "$1/".[^.]* "" = "" ] 2> /dev/null ||
[ "$1/"* = "$1/*" ] 2> /dev/null && [ ! -e "$1/*" ] &&
[ "$1/".[^.]* = "$1/.[^.]*" ] 2> /dev/null && [ ! -e "$1/.[^.]*" ] &&
[ "$1/"..?* = "$1/..?*" ] 2> /dev/null && [ ! -e "$1/..?*" ]
}
Security concern:
Create two files rm
and x
in an empty directory and execute *
on the prompt. The glob *
will expand to rm x
and this will be executed to remove x
. This is not a security concern, because in our function, the globs are located where the expansions are not seen as commands, but as arguments, just like in for f in *
.