Why does a failed filename generation make zsh stop processing a script

zsh

I was trying to write a short script which would write all the executable programs found in $PATH:

for dir in $(tr ':' ' ' <<<"${PATH}"); do
  for pgm in $dir/*; do
    if command -v "${pgm}" >/dev/null 2>&1; then
      echo "${pgm}"
    fi
  done
done | sort >file

In bash, it works as expected, but zsh stops processing the script as soon as a filename generation fails in the inner loop:

for pgm in $dir/*; do
           ^^^^^^
  ...
done

As a result, since my $PATH contains a directory which doesn't contain any file (/usr/local/sbin), in zsh, the script fails to write the executables found in the directories afterwards.

Here's another code showing the same issue:

for f in /not_a_dir/*; do
  echo 'in the loop'
done
echo 'after the loop'

In bash, this command outputs:

in the loop
after the loop

And exits with the code 0.

While in zsh, the same command outputs:

no matches found: /not_a_dir/*

And exits with the code 1.

The difference of behavior between the shells seems to come from the nomatch option, which is described in man zshoptions:

NOMATCH (+3) <C> <Z>

If a pattern for filename generation has no matches, print an error, instead of leaving it unchanged
in the argument list. This also applies to file expansion of an initial ~ or =.

And also explained in man zshexpn (section FILENAME GENERATION):

The word is replaced with a list of sorted filenames that match the pattern. If no matching pattern is found, the shell gives an error message, unless the NULL_GLOB option is set, in which case the word is deleted; or unless the NOMATCH option is unset, in which case the word is left unchanged.

Because if I unset nomatch, zsh behaves like bash:

unsetopt nomatch
for f in /not_a_dir/*; do
  echo 'in the loop'
done
echo 'after the loop'

Now I understand the difference of behaviors between bash and zsh, and why the script raises an error in zsh, but I want to understand why a failed filename generation makes zsh immediately stop processing a script.
So, I tried to reproduce the same issue by replacing the failed filename generation with a failed command (by executing not_a_cmd):

for f in ~/*; do
  not_a_cmd
done
echo 'after the loop'

But the output of this script is almost identical in both shells (apart from the error messages due to not_a_cmd). In particular, both shells print:

after the loop

And both shells exit with the code 0.

Why does a failed filename generation (like for f in /not_a_dir/*) make zsh stop processing a script, but not a failed command (like not_a_cmd)?

I'm using zsh 5.6.2-dev-0 (x86_64-pc-linux-gnu).

Best Answer

It's a feature, not a bug. ?

As it says in Zsh's manual, section "Errors":

Certain errors are treated as fatal by the shell: in an interactive shell, they cause control to return to the command line, and in a non-interactive shell they cause the shell to be aborted. […]

Fatal errors found in non-interactive shells include:

  • […]
  • File generation errors where not caught by the option BAD_PATTERN
  • […]
  • File generation failures where not caused by NO_MATCH or similar options.

As for the reasoning behind it, I found this email exchange in the Zsh mailing list archives:

In other shells this is implemented in a very simple way, if the wildcard can be expanded, then it is expanded, otherwise an asterisk is sent as an argument to the application.

Yes, other shells do it that way, but that has its downsides. For instance, if you run "touch *,c" it's better to get "no match" rather than creating a useless *,c file. Or if you type a command like this: "rsync -avP *.html *.shmtl *.php host:/dest/" (note the misspelling), it is better to get a "no match" error right away than to get a single open-error embedded in all the output that you then have to figure out at the end of the transfer. I much prefer to simply put single quotes around any wild-carded file names that should not be expanded rather than to configure the shell to include a wildcard that didn't match anything. YMMV, of course, but if you give it a try, you may find that you come to prefer it as well.

Related Question