find only the files that ended with

As @StephenKitt said said, in bash, brace expansion is done before globbing (even before all other other forms of expansions including parameter expansions, see how bash is the only shell where echo $P{S1,ATH} outputs the contents of both $PS1 and $PATH), so there,

ls -l /python/*.{whl,}

is the same as:

ls -l /python/*.whl /python/*.

And bash has that misfeature inherited from the Bourne shell that globs that don’t match are passed literally to the command, so if there’s no non-hidden file in /python whose name ends in ., the /python/*. filename will be passed to ls and ls complains that that file (yes, *. is a perfectly valid file name in Unix) doesn’t exist. Note that the failglob and nullglob options change that behaviour.

Now, the brace expansion feature comes from csh in the late 70s. And you’ll notice that you don’t see that error in csh.

csh doesn’t have that misfeature of the Bourne shell and behaves like the /etc/glob of early Unices (which gave their name to globs) in that globs that don’t match are removed (instead of being passed as-is), and if all globs in a command line fail to match, then the command is cancelled (the sensible thing to do as obviously something is wrong).

So in csh:

ls -l /python/*.{whl,}

is first expanded to

ls -l /python/*.whl /python/*.

like in bash, but since /python/*. doesn’t match any file but /python/*.whl matches some files, /python/*. is removed and ls is called with the list of .whl files.

If there hadn’t been any .whl file either, you would have had a No match error by csh, instead of an error by ls about non-existing files.

zsh is another shell that copied brace expansion from csh. The behaviour for unmatched globs is different.

zsh doesn’t have that misfeature of the Bourne shell (at least not by default) either, but it cancels commands as long as any (as opposed to all in csh//etc/glob) glob fails to match so ls -l /python/*.{whl,} or ls -l /python/*.whl /python/*. would not run ls even though there are .whl files, because the /python/*. glob doesn’t match. In zsh, you can get the csh behaviour instead with set -o cshnullglob or the Bourne behaviour with set +o nomatch (not recommended). In bash, you can get a behaviour similar to that of zsh with shopt -s failglob.

The fish shell behaviour is yet different.

fish (a much newer shell) behaves like zsh in that failing globs cancel the command. Like in zsh, you’ll see that

ls -l /python/*.whl /python/*.

doesn’t run ls and returns a:

fish: No matches for wildcard “/python/*.”. See `help expand`. ls -l /python/*.whl /python/*. ^

error. But

ls -l /python/*.{whl,}

does not return an error, lists the .whl files and does not pass a /python/*. argument to ls behaving in this case like csh.

But that’s because even though {x,y} is not strictly speaking a globbing operator there either, it is done alongside globbing, and only if globbing done upon the whole brace doesn’t match any file is the command cancelled. You’ll see that if there are neither *.whl nor *. files, the error becomes:

fish: No matches for wildcard “/python/*.{whl,}”. See `help expand`. ls -l /python/*.{whl,} ^

I.e., the whole /python/*.{whl,} is considered the wildcard here, not either wildcard resulting from the brace expansion.

To list the files that end in either .whl or ., I’d do:

ls -ld /python/*(.|.whl) # zsh ls -ld /python/*.(|whl) # zsh ls -ld /python/*@(.|.whl) # ksh / bash -O extglob ls -ld /python/*.?(whl) # ksh / bash -O extglob

That is, use a glob operator within one glob that matches either as opposed to two globs, one for each case (unless you want the .whl files to be listed first, but in the case of ls as the command, that makes no difference as ls sorts the list anyway).

Beware however that the ksh variants, unless you use failglob in bash could end-up listing a file literally called *.?(whl) if there was no file matching that pattern because of that Bourne shell misfeature.

in zsh, if you wanted to list the *.whl files before the *. without treating it as an error if either list is empty like in csh/fish, you could either use cshnullglob locally:

(){ set -o localoptions -o cshnullglob; ls -ldU /python/*.{whl,}; }

(here assuming GNU ls and using its -U option to disable its sorting).

Or use the N glob qualifier (to get a nullglob behaviour) and do the error handling (for the case where neither glob matches) by hand:

() { if (($#)); then ls -ldU — “[email protected]” else echo>&2 No match return 1 fi } /python/*.{whl,}

Something similar could be done in bash using a subshell (as bash doesn’t have anonymous functions) for limiting the scope of options and positional parameters:

( shopt -s nullglob shopt -u failglob # failglob takes precedence over nullglob when set! set — /python/*.{whl,} if (($#)); then ls -ldU — “[email protected]” else echo>&2 No match exit 1 fi )


Leave a Reply