Skip to main content
2 of 4
added 317 characters in body
Stéphane Chazelas
  • 584.8k
  • 96
  • 1.1k
  • 1.7k

Most of your questions are already answered at Why is nullglob not default?.

One thing to bear in mind is that in:

ls -d /some/{path1,path2}/*

Which 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 match 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).


¹ 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

Stéphane Chazelas
  • 584.8k
  • 96
  • 1.1k
  • 1.7k