3

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

  • 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

Your Answer

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

Browse other questions tagged or ask your own question.