8

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.

1
  • 1
    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? Commented Aug 7, 2018 at 5:40

1 Answer 1

5

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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .