2019-05-30 19:05:33 -0500, Steven Penny:
[...]
> > Of all three designs zsh's is probably the best, but that's
> > mostly because it did fix that split+glob upon parameter
> > expansion issue of the Bourne shell (when not invoked as sh).
> 
> This is where we differ (I think). I have had limited use of Zsh but the use I
> have had of it has been painful. For example with Zsh you cannot use this
> "portable" method to check for empty directory:
> 
>     % set -- *
>     zsh: no matches found: *
> 
>     % echo $?
>     1
> 
> https://unix.stackexchange.com/questions/492912
> 
> Granted the method is crude, but its the fact that Zsh fails so harshly, with
> "set -e" or similar execution would halt. Compare this with POSIX shell:
> 
>     $ set -- *
>     echo $?
>     0

No need for "set -e", in non-interactive shells, a failed glob
exits the shell like other syntax errors which is the right
thing to do, see below.

In interactive shells, it returns to the prompt like for other
syntax errors.

> Or for example Python:
> 
>     >>> import glob
>     >>> glob.glob('*')
>     []
> 
> in both cases, youre essentially getting an empty array, or something that 
> could
> easily be tested to determine if the directory is empty. Sorry for going on a
> tangent, but I wanted to explain why I would be wary of any Zsh suggestions as
> I disagree with some of their decisions.
[...]

See
https://unix.stackexchange.com/questions/204803/why-is-nullglob-not-default
about that.

The pre-Bourne sh, csh, zsh, fish, bash -O failglob behaviour is
the correct one IMO, and the POSIX sh behaviour is bogus and
again could be fixed by adding bash's failglob or zsh's nomatch
option to the standard.

Here set dir/* or files=(dir/*) is one of the very few commands
where you would want a nullglob effect as opposed to a failglob
one when the glob doesn't match, but in any case, still not the
Bourne/POSIX behaviour (leave the glob unexpanded asis).

In zsh, here, you'd write it:

  set dir/*(N)

In ksh93:

  set dir/~(N)*

In bash:

  saved=$(shopt -p nullglob failglob) || true
  shopt -s nullglob
  shopt -u failglob
  set dir/*
  eval "$saved"

Not much of an improvement over what you need to do in POSIX
shells in this case:

  set -- dir/[*] dir/*
  case $1$2 in
    ('dir/[*]dir/*) set --;;
    (*) shift;;
  esac

(and no, doing [ -e "$1" ] or even [ -e "$1" ] || [ -L "$1" ] to
check whether the glob matched is not correct).


In fish, which also fails upon non-matching globs, the "set" and
"count" commands (and for loops) are actually treated specially.

   echo *.none # fails if there's no none file
   set files *.none # doesn't and sets $files to an empty list

You don't want nullglob to be the default (like in your python
glob()) for all the reasons listed at
https://unix.stackexchange.com/questions/204803/why-is-nullglob-not-default

You don't want the POSIX behaviour either as passing the glob
literally is never what you want, and is even dangerous like in

  rm -f -- *.[ch]

That would remove the file called '*.[ch]' if there was no
non-hidden file with a name ending in .c or .h

One difference between fish/bash -O failglob and zsh is the
scope of the failure upon non-matching globs.

In zsh, a failing glob exits the current subshell or shell
(unless there's an "always" block to catch the error).
(interactively, it returns to the prompt).

In fish, it only cancels the command with the failing glob
and sets $status to 124. That's similar to what was done in
pre-Bourne shells where globs were handled by a separate utility
(/etc/glob which gave its name to globs)

In bash -O failglob, interactively, like in zsh, it cancels
everything and returns to the prompt (or exits a subshell).

But in non-interactive shells, it still does the same! That
makes it not very usable in scripts.

For instance, neither

   echo *.none; echo done

nor

   {
     echo *.none
     echo done
   }

Would output "done" if there was no *.none file, but

   echo *.none
   echo done

would because upon the failing glob, bash aborts and resumes
execution at the next point where it left reading and parsing
code like it would in interactive shells.

So, in scripts, that means you can only safely use failglob if
you put your whole script inside a command group (or a main()
function) and then you get the zsh behaviour, or you only use
globs inside eval statements (eval 'cmd -- *.txt') and then you
get the fish behaviour.

In zsh, you can catch the failing glob with a subshell or
"always" block:

{
  cmd -- *failing* glob
} always {
  if ((TRY_BLOCK_ERROR)); then
    echo handle the nomatch error
    TRY_BLOCK_ERROR=0 # reset the error condition if need be
                      # or let it propagate otherwise
  fi
}

or:

args=(*failing*(N))
if (($#args)); then
  cmd -- $args glob
else
  echo handle the no match case
fi
  

In bash -O failglob:

if eval 'args=(*failing*)'; then
  cmd -- "${args[@]}"
else
  # beware $args has been left untouched here
  echo handle the no match error
fi

You might as well use nullglob instead of failglob and only use
globs in array assignments:

shopt -s nullglob
files=(*)
if ((${#files[@]})); then
  cmd -- "${args[@]}"
else
  echo handle the no match case
fi

(also beware that in bash, nullglob doesn't cancel failglob, so
is ineffective when failglob is on).

To me, it makes sense to treat a failing glob the same way as a
syntax error. There's no general safe way to handle it. If the
script author has not taken steps to handle the case (like by
using (N)/nullglob, or "always" block), the safest thing to do
is probably to exit.

In any case, the Bourne/POSIX sh behaviour is certainly among
the worst.

-- 
Stephane

Reply via email to