7

I'm transitioning from being a long time tcsh user to a new bash user (it's way overdue). I wrote a lot of foreach loops in tcsh on the fly on a regular basis, so I learned the syntax for bash's for loops as a substitute, but was surprised when non-matching glob patterns got passed through the loop as literal strings. I searched for a way to change this behavior so that literal strings would get skipped and found shopt -s nullglob. My understanding was that this theoretically should be equivalent to the way tcsh behaves, but today I discovered a difference. When I do ls ../*.doesnotmatch, the result was a list of the current directory's contents. Specifically, I did this:

bash:

$ shopt -s nullglob
$ ls ../*.sam
extractSplitReads_BwaMem    extractSplitReads_BwaMem.xml
$ shopt -u nullglob
$ ls ../*.sam
ls: ../*.sam: No such file or directory

There's nothing in the parent directory that matches *.sam, particularly not the current directory. I was really confused at first, but then I realized that the glob pattern is disappearing and that the command was executing as if I had not supplied any arguments, e.g.:

$ ls

So I tried setting failglob both by itself and with nullglob, but as long as failglob is set, any non-matching glob pattern kills the command, whether or not a matching pattern is present:

bash:

$ shopt -s failglob
$ shopt -s nullglob
$ ls ../vis*/*.xml
../visualization/LAJ.xml
$ ls ../vis*/*.xml ../*.sam
bash: no match: ../*.sam
$ ls ../{vis*/*.xml,*.sam}
bash: no match: ../*.sam

When I am using tcsh, all globs are boiled down to just those things that matched and if nothing matches, you get a glob error:

tcsh:

$ ls ../vis*/*.xml ../*.sam
../visualization/LAJ.xml
$ ls ../{vis*/*.xml,*.sam}
../visualization/LAJ.xml
$ ls ../*.sam
ls: No match.

I looked through the shopt settings, but I don't see a way to get this behavior. Am I missing something? Is there another modern shell besides bash or tcsh that treats globs the way tcsh does? I want the behavior of nullglob when something matches, but the behavior of failglob when nothing matches, which seems to be how tcsh works.

  • Going straight to zsh instead of bash would be a much more natural transition. zsh globs work like bash -O failglob by default, but you can use the cshnullglob option for it to behave like tcsh. See also Why is nullglob not default? – Stéphane Chazelas Aug 7 '18 at 5:40
4

The only shopt options related to filename expansion are dotglob, failglob, nocaseglob and nullglob, and none of them (alone or combined) seem to do exactly what you want. It's a shame because that sounds like a really good idea.

My recommendation is to have failglob set in interactive sessions, that way you can avoid potentially unwanted commands such as:

mv -r file1 file2 dir1 dir2 destination-*-dir

where file1, file2 and dir1 would be moved into dir2 if destination-*-dir matches nothing and nullglob is set.

On the other hand, it's not a good idea to entirely rely on filename expansions when shell-scripting. It's recommendable to always validate if such expansions exist and are what they are supposed to be.

I mean, instead of doing this:

rm -- *.jpg *.txt

It's better to do something like this:

for file in *.jpg *.txt; do
  if [ -f "${file}" ]; then
    rm -- "${file}"
  fi
done

# Or this (non-POSIX, as it uses an array)

for file in *.jpg *.txt; do
  if [ -f "${file}" ]; then
    files_to_delete+=( "${file}" )
  fi
done

if [ "${#files_to_delete[@]}" -gt 0 ]; then
  rm -- "${files_to_delete[@]}"
fi

That way you'll be safe even if, for example, some file matches *.txt but it's actually a directory.

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.