Most of your questions are already answered at Why is nullglob not default?.
One thing to bear in mind is that:
ls -d /some/{path1,path2}/*
In csh/tcsh/zsh/bash/ksh (but not fish, see below) is the same as:
ls -d /some/path1/* /some/path2/*
As the brace expansion is performed before (not as part of) the glob expansion, it's the shell that expands those /some/pathx/* patterns into the list of matching files to pass as separate arguments to ls.
bash, like ksh which it mostly copied has inherited a misfeature introduced by the Bourne shell in that, when a glob pattern doesn't match any file, it is passed as-is, literally as an argument to the command.
So, in those shells, if /some/path1/* matches at least one file and /some/path2/* matches none, ls will be called with -d, /some/path1/file1, /some/path1/file2 and a literal /some/path2/* as arguments. As the /some/path2/* file doesn't exist, ls will report an error.
csh behaves like in the early Unix versions, where globs were not performed by the shell but by a /etc/glob helper utility (which gave their name to globs). That helper would perform the glob expansions before invoking the command and report a No match error without running the command if all the glob patterns failed to match any file. Otherwise, as long as there was at least one glob with matches, all the non-matching ones would simply be removed.
So in our example above, with csh / tcsh or the ancient Thompson shell and its /etc/glob helper, ls would be called with -d, /some/path1/file1 and /some/path1/file2 only, and would likely succeed (as long as /some/path1 is searchable).
zsh is both a Korn-like and csh-like shell. It does not have that misfeature of the Bourne shell whereby unmatched globs are passed as is¹, but by default is stricter than csh in that, all failing globs are considered as a fatal error. So in zsh, by default, if either /some/path1/* or /some/path2/* (or both) fails to match, the command is aborted. A similar behaviour can be enabled in the bash shell with the failglob option².
That makes for more predictable / consistent behaviours but means that you can run into that problem when you want to pass more than one glob expansion to a command and would not like it to fail as long as one of the globs succeeds. You can however set the cshnullglob option to get a csh-like behaviour (or emulate csh).
That can be done locally by using an anonymous function:
() { set -o localoptions -o cshnullglob; ls -d /some/{path1,path2}/*; }
Or just using a subshell:
(set -o cshnullglob; ls -d /some/{path1,path2}/*)
However here, instead of using two globs, you could use one that matches all of them using the alternation glob operator:
ls -d /some/(path1|path2)/*
Here, you could even do:
ls -d /some/path[12]/*
In bash, you can enable the extglob option for bash to support a subset of ksh's extended glob operator, including alternation:
(shopt -s extglob; ls -d /some/@(path1|path2)/*)
Now, because of that misfeature inherited from the Bourne shell, if that glob doesn't match any file, /some/@(path1|path2)/* would be passed as-is to ls and ls could end up listing a file called literally /some/@(path1|path2)/*, so you'd also want to enable the failglob option to guard against that:
(shopt -s extglob failglob; ls -d /some/@(path1|path2)/*)
Alternatively, you can use the nullglob option (which bash copied from zsh) for all non-matching globs to expand to nothing. But:
(shopt -s nullglob; ls -d /some/path1/* /some/path2/*)
Would be wrong in the special case of the ls command, which, if not passed any argument lists .. You could however use nullglob to store the glob expansion into an array, and only call ls with the member of the arrays as argument if it is non-empty:
(
shopt -s nullglob
files=( /some/path1/* /some/path2/* )
if (( ${#files[@]} > 0 )); then
ls -d -- "${files[@]}"
else
echo >&2 No match
exit 2
fi
)
In zsh, instead of enabling nullglob globally, you can enable it on a per-glob basis with the (N) glob qualifier (which inspired ksh's ~(N), not copied by bash yet), and use an anonymous function again instead of an array:
() {
(( $# > 0 )) && ls -d -- "$@"
} /some/path1/*(N) /some/path2/*(N)
The fish shell now behaves similarly to zsh where failing globs cause an error, except when the glob is used with for, set (which is used to assign arrays) or count where it behaves in a nullglob fashion instead.
Also, in fish, the brace expansion though not a glob operator in itself is done as part of globbing, or at least a command is not aborted when brace expansion is combined with globbing and at least one element can be returned.
So, in fish:
ls -d /some/{path1,path2}/*
Would end up in effect behaving like in csh.
Even:
{ls,-d,/xx*}
Would result in ls being called with -d alone if /xx* was not matched instead of failing (behaving differently from csh in this instance).
In any case, if it's just to print the matching file paths, you don't need ls. In zsh, you could use its print builtin to print in columns:
print -rC3 /some/{path1,path2}/*(N)
Would print the paths raw on 3 columns (and print nothing if there's no match with the Nullglob glob qualifier).
If instead you want to check if there's at least one non-hidden file in any of those two directories, you can do:
# bash
if (shopt -s nullglob; set -- /some/path1/* /some/path2/*; (($#))); then
echo yes
else
echo no
fi
Or using a function:
has_non_hidden_files() (
shopt -s nullglob
set -- "$1"/*
(($#))
)
if has_non_hidden_files /some/path1 || has_non_hidden_files /some/path2
then
echo yes
else
echo no
fi
# zsh
if ()(($#)) /some/path1/*(N) /some/path2/*(N); then
echo yes
else
echo no
fi
Or with a function:
has_non_hidden_files() ()(($#)) $1/*(NY1)
if has_non_hidden_files /some/path1 || has_non_hidden_files /some/path2
then
echo yes
else
echo no
fi
(Y1 as an optimisation to stop after finding the first file)
Beware those has_non_hidden_files would (silently) return false for directories that are not readable by the user (whether they have files or not). In zsh, you could detect this kind of situation with its $ERRNO special variable.
¹ The Bourne behaviour (which was specified by POSIX) can be enabled though in zsh by doing emulate sh or emulate ksh or with set +o nomatch
² beware there are significant differences in behaviour as to what exactly is cancelled when the glob doesn't match, the fish behaviour being generally the more sensible, and the bash -O failglob probably the worst
ls, and ls doesn't care whether it's being executed by bash or tcsh (also, i tested it on my system, and got an exit code of 2 in both bash and tcsh). In any case, you probably don't want to be relying on ls to check whether a directory exists or not. Usetest -d dir(or[ -d dir ]-[). If you need to test existence of more than one, you can use boolean&&or||operators to combine tests, or use a for loop. – cas Mar 05 '22 at 09:08{foo,bar}/*, the/*makes the difference. It's anullglobissue. – terdon Mar 05 '22 at 11:29zsh, you can useset -o cshnullglobto enable the csh behaviour, though you'd rather dols -d (foo|bar)/*there. See also Why is nullglob not default? – Stéphane Chazelas Mar 05 '22 at 12:17