5

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

2
  • 1
    This is a genuine question -- why printf "shopt -s failglob \n echo *.foo \n echo afterwards" | bash will print "afterwards", but printf "echo *.foo \n echo afterwards" | zsh and bash -c 'shopt -s failglob; echo *.foo; echo afterwards' won't? I'm too lazy to search if this has been discussed to depth on this site -- but please change the title of your question; it's begging for kneejerk deletion; there are a hundred thousands cases where zsh may stop processing a script ;-)
    – mosvy
    Oct 31 '18 at 9:11
  • @mosvy I don't know the answer, but in your first command, if I replace the second newline with a semicolon, afterwards is not printed. Out of the 3 bash commands (the two you provided plus the one I just mentioned), two fail to print afterwards. I'm not sure, but it makes me think that the one which does print afterwards is the exception: printf "shopt -s failglob \n echo *.foo \n echo afterwards" | bash. For some reason, the second newline is able to prevent bash from interrupting the script.
    – user938271
    Oct 31 '18 at 17:58
1

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.