Portable check empty directory
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?
bash zsh wildcards portability dash
add a comment |
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?
bash zsh wildcards portability dash
Related: Why is nullglob not default?
– Stéphane Chazelas
Jan 26 at 22:30
add a comment |
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?
bash zsh wildcards portability dash
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?
bash zsh wildcards portability dash
bash zsh wildcards portability dash
edited Jan 7 at 1:28
terdon♦
133k32264444
133k32264444
asked Jan 7 at 1:06
ThreeThree
663
663
Related: Why is nullglob not default?
– Stéphane Chazelas
Jan 26 at 22:30
add a comment |
Related: Why is nullglob not default?
– Stéphane Chazelas
Jan 26 at 22:30
Related: Why is nullglob not default?
– Stéphane Chazelas
Jan 26 at 22:30
Related: Why is nullglob not default?
– Stéphane Chazelas
Jan 26 at 22:30
add a comment |
4 Answers
4
active
oldest
votes
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.
add a comment |
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
shorterif ls -A | read q; then echo not empty; else echo empty; fi
– Three
Jan 13 at 14:25
add a comment |
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 forzsh
. Unfortunatelly for uszsh
decided to go the weird way instead of similar behavior tobash
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
turningnomatch
everywhere risks breaking code that assumes the default (and very sensible) setting thatsh
andbash
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 withoutnomatch
, 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, likefind
for instance.
– Sergiy Kolodyazhnyy
Jan 7 at 3:16
@Three I've revised the answer again. Probably this is the best I can do, aszsh
seems to favor features instead of consistency. Hope this helps somewhat.
– Sergiy Kolodyazhnyy
Jan 7 at 3:18
1
The expansion of.*
not including.
and..
is what you generally want and was done in the public domain version ofksh
beforezsh
(pdksh inherited it from the Forsyth shell on which it is based). In other shells whether.
and..
is included depends on whetherreaddir()
returns them or not which is not guaranteed.
– Stéphane Chazelas
Jan 27 at 0:01
add a comment |
There are several problems with that
set *
if [ -e "$1" ]
then
echo 'not empty'
else
echo 'empty'
fi
code:
- if the
nullglob
option (fromzsh
but now supported by most other shells) is enabled,set *
becomesset
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 byset
. Those two issues can be fixed by usingset -- *
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 adotglob
orglobdot
option of play with aFIGNORE
special variable to work around that.
[ -e "$1" ]
tests whether astat()
system call succeeds or not. If the first file a symlink to an inaccessible location, that will return false. You shouldn't need tostat()
(not evenlstat()
) 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
echo . is not empty
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
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 that you 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.
thanks. a shorter, but less robust version for non-zsh isset .[!.]* *; [ "$*" = '.[!.]* *' ]
– Three
Jan 26 at 23:50
@Three, yes, it fails when the first character of$IFS
is not space or when there's a file called*
or.[!.]*
or..foo
...
– Stéphane Chazelas
Jan 26 at 23:53
add a comment |
Your Answer
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "106"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2funix.stackexchange.com%2fquestions%2f492912%2fportable-check-empty-directory%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
4 Answers
4
active
oldest
votes
4 Answers
4
active
oldest
votes
active
oldest
votes
active
oldest
votes
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.
add a comment |
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.
add a comment |
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.
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.
edited Jan 7 at 2:02
answered Jan 7 at 1:55
Michael HomerMichael Homer
50.3k8138176
50.3k8138176
add a comment |
add a comment |
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
shorterif ls -A | read q; then echo not empty; else echo empty; fi
– Three
Jan 13 at 14:25
add a comment |
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
shorterif ls -A | read q; then echo not empty; else echo empty; fi
– Three
Jan 13 at 14:25
add a comment |
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
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
edited Jan 26 at 22:27
Stéphane Chazelas
311k57587945
311k57587945
answered Jan 13 at 10:35
GillesGilles
544k12811021620
544k12811021620
shorterif ls -A | read q; then echo not empty; else echo empty; fi
– Three
Jan 13 at 14:25
add a comment |
shorterif ls -A | read q; then echo not empty; else echo empty; fi
– Three
Jan 13 at 14:25
shorter
if ls -A | read q; then echo not empty; else echo empty; fi
– Three
Jan 13 at 14:25
shorter
if ls -A | read q; then echo not empty; else echo empty; fi
– Three
Jan 13 at 14:25
add a comment |
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 forzsh
. Unfortunatelly for uszsh
decided to go the weird way instead of similar behavior tobash
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
turningnomatch
everywhere risks breaking code that assumes the default (and very sensible) setting thatsh
andbash
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 withoutnomatch
, 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, likefind
for instance.
– Sergiy Kolodyazhnyy
Jan 7 at 3:16
@Three I've revised the answer again. Probably this is the best I can do, aszsh
seems to favor features instead of consistency. Hope this helps somewhat.
– Sergiy Kolodyazhnyy
Jan 7 at 3:18
1
The expansion of.*
not including.
and..
is what you generally want and was done in the public domain version ofksh
beforezsh
(pdksh inherited it from the Forsyth shell on which it is based). In other shells whether.
and..
is included depends on whetherreaddir()
returns them or not which is not guaranteed.
– Stéphane Chazelas
Jan 27 at 0:01
add a comment |
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 forzsh
. Unfortunatelly for uszsh
decided to go the weird way instead of similar behavior tobash
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
turningnomatch
everywhere risks breaking code that assumes the default (and very sensible) setting thatsh
andbash
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 withoutnomatch
, 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, likefind
for instance.
– Sergiy Kolodyazhnyy
Jan 7 at 3:16
@Three I've revised the answer again. Probably this is the best I can do, aszsh
seems to favor features instead of consistency. Hope this helps somewhat.
– Sergiy Kolodyazhnyy
Jan 7 at 3:18
1
The expansion of.*
not including.
and..
is what you generally want and was done in the public domain version ofksh
beforezsh
(pdksh inherited it from the Forsyth shell on which it is based). In other shells whether.
and..
is included depends on whetherreaddir()
returns them or not which is not guaranteed.
– Stéphane Chazelas
Jan 27 at 0:01
add a comment |
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!
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!
edited Jan 7 at 3:13
answered Jan 7 at 2:02
Sergiy KolodyazhnyySergiy Kolodyazhnyy
10.7k42763
10.7k42763
@Three I've adapted the answer to check forzsh
. Unfortunatelly for uszsh
decided to go the weird way instead of similar behavior tobash
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
turningnomatch
everywhere risks breaking code that assumes the default (and very sensible) setting thatsh
andbash
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 withoutnomatch
, 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, likefind
for instance.
– Sergiy Kolodyazhnyy
Jan 7 at 3:16
@Three I've revised the answer again. Probably this is the best I can do, aszsh
seems to favor features instead of consistency. Hope this helps somewhat.
– Sergiy Kolodyazhnyy
Jan 7 at 3:18
1
The expansion of.*
not including.
and..
is what you generally want and was done in the public domain version ofksh
beforezsh
(pdksh inherited it from the Forsyth shell on which it is based). In other shells whether.
and..
is included depends on whetherreaddir()
returns them or not which is not guaranteed.
– Stéphane Chazelas
Jan 27 at 0:01
add a comment |
@Three I've adapted the answer to check forzsh
. Unfortunatelly for uszsh
decided to go the weird way instead of similar behavior tobash
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
turningnomatch
everywhere risks breaking code that assumes the default (and very sensible) setting thatsh
andbash
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 withoutnomatch
, 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, likefind
for instance.
– Sergiy Kolodyazhnyy
Jan 7 at 3:16
@Three I've revised the answer again. Probably this is the best I can do, aszsh
seems to favor features instead of consistency. Hope this helps somewhat.
– Sergiy Kolodyazhnyy
Jan 7 at 3:18
1
The expansion of.*
not including.
and..
is what you generally want and was done in the public domain version ofksh
beforezsh
(pdksh inherited it from the Forsyth shell on which it is based). In other shells whether.
and..
is included depends on whetherreaddir()
returns them or not which is not guaranteed.
– Stéphane Chazelas
Jan 27 at 0:01
@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
@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
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
@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
@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
1
1
The expansion of
.*
not including .
and ..
is what you generally want and was done in the public domain version of ksh
before zsh
(pdksh inherited it from the Forsyth shell on which it is based). In other shells whether .
and ..
is included depends on whether readdir()
returns them or not which is not guaranteed.– Stéphane Chazelas
Jan 27 at 0:01
The expansion of
.*
not including .
and ..
is what you generally want and was done in the public domain version of ksh
before zsh
(pdksh inherited it from the Forsyth shell on which it is based). In other shells whether .
and ..
is included depends on whether readdir()
returns them or not which is not guaranteed.– Stéphane Chazelas
Jan 27 at 0:01
add a comment |
There are several problems with that
set *
if [ -e "$1" ]
then
echo 'not empty'
else
echo 'empty'
fi
code:
- if the
nullglob
option (fromzsh
but now supported by most other shells) is enabled,set *
becomesset
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 byset
. Those two issues can be fixed by usingset -- *
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 adotglob
orglobdot
option of play with aFIGNORE
special variable to work around that.
[ -e "$1" ]
tests whether astat()
system call succeeds or not. If the first file a symlink to an inaccessible location, that will return false. You shouldn't need tostat()
(not evenlstat()
) 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
echo . is not empty
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
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 that you 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.
thanks. a shorter, but less robust version for non-zsh isset .[!.]* *; [ "$*" = '.[!.]* *' ]
– Three
Jan 26 at 23:50
@Three, yes, it fails when the first character of$IFS
is not space or when there's a file called*
or.[!.]*
or..foo
...
– Stéphane Chazelas
Jan 26 at 23:53
add a comment |
There are several problems with that
set *
if [ -e "$1" ]
then
echo 'not empty'
else
echo 'empty'
fi
code:
- if the
nullglob
option (fromzsh
but now supported by most other shells) is enabled,set *
becomesset
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 byset
. Those two issues can be fixed by usingset -- *
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 adotglob
orglobdot
option of play with aFIGNORE
special variable to work around that.
[ -e "$1" ]
tests whether astat()
system call succeeds or not. If the first file a symlink to an inaccessible location, that will return false. You shouldn't need tostat()
(not evenlstat()
) 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
echo . is not empty
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
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 that you 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.
thanks. a shorter, but less robust version for non-zsh isset .[!.]* *; [ "$*" = '.[!.]* *' ]
– Three
Jan 26 at 23:50
@Three, yes, it fails when the first character of$IFS
is not space or when there's a file called*
or.[!.]*
or..foo
...
– Stéphane Chazelas
Jan 26 at 23:53
add a comment |
There are several problems with that
set *
if [ -e "$1" ]
then
echo 'not empty'
else
echo 'empty'
fi
code:
- if the
nullglob
option (fromzsh
but now supported by most other shells) is enabled,set *
becomesset
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 byset
. Those two issues can be fixed by usingset -- *
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 adotglob
orglobdot
option of play with aFIGNORE
special variable to work around that.
[ -e "$1" ]
tests whether astat()
system call succeeds or not. If the first file a symlink to an inaccessible location, that will return false. You shouldn't need tostat()
(not evenlstat()
) 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
echo . is not empty
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
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 that you 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.
There are several problems with that
set *
if [ -e "$1" ]
then
echo 'not empty'
else
echo 'empty'
fi
code:
- if the
nullglob
option (fromzsh
but now supported by most other shells) is enabled,set *
becomesset
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 byset
. Those two issues can be fixed by usingset -- *
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 adotglob
orglobdot
option of play with aFIGNORE
special variable to work around that.
[ -e "$1" ]
tests whether astat()
system call succeeds or not. If the first file a symlink to an inaccessible location, that will return false. You shouldn't need tostat()
(not evenlstat()
) 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
echo . is not empty
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
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 that you 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.
edited Jan 28 at 17:32
answered Jan 26 at 23:26
Stéphane ChazelasStéphane Chazelas
311k57587945
311k57587945
thanks. a shorter, but less robust version for non-zsh isset .[!.]* *; [ "$*" = '.[!.]* *' ]
– Three
Jan 26 at 23:50
@Three, yes, it fails when the first character of$IFS
is not space or when there's a file called*
or.[!.]*
or..foo
...
– Stéphane Chazelas
Jan 26 at 23:53
add a comment |
thanks. a shorter, but less robust version for non-zsh isset .[!.]* *; [ "$*" = '.[!.]* *' ]
– Three
Jan 26 at 23:50
@Three, yes, it fails when the first character of$IFS
is not space or when there's a file called*
or.[!.]*
or..foo
...
– Stéphane Chazelas
Jan 26 at 23:53
thanks. a shorter, but less robust version for non-zsh is
set .[!.]* *; [ "$*" = '.[!.]* *' ]
– Three
Jan 26 at 23:50
thanks. a shorter, but less robust version for non-zsh is
set .[!.]* *; [ "$*" = '.[!.]* *' ]
– Three
Jan 26 at 23:50
@Three, yes, it fails when the first character of
$IFS
is not space or when there's a file called *
or .[!.]*
or ..foo
...– Stéphane Chazelas
Jan 26 at 23:53
@Three, yes, it fails when the first character of
$IFS
is not space or when there's a file called *
or .[!.]*
or ..foo
...– Stéphane Chazelas
Jan 26 at 23:53
add a comment |
Thanks for contributing an answer to Unix & Linux Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2funix.stackexchange.com%2fquestions%2f492912%2fportable-check-empty-directory%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Related: Why is nullglob not default?
– Stéphane Chazelas
Jan 26 at 22:30