An unquoted [:lower:] is a glob¶
Level: 101 · you copied tr [:lower:] [:upper:] from a book, and it worked
One line: Without quotes, bash and zsh read [:lower:] as a filename pattern that matches any one-character name among :, l, o, w, e and r. So tr [:lower:] [:upper:] works in a directory with no such file. After touch e it does nothing, because the shell runs tr e e. In zsh it does not run at all when nothing matches. fish does not treat square brackets as a wildcard, so there it always works. Quote the class.
Measured¶
In bash, first in a fresh empty directory and then in the same directory with one file named e:
Verified output of tr_unquoted_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ ls
$ echo [:lower:] [:upper:]
[:lower:] [:upper:]
$ echo hello | tr [:lower:] [:upper:]
HELLO
$ shopt -s failglob; echo hello | tr [:lower:] [:upper:]; echo "status $?"; shopt -u failglob
tr_unquoted_sh.sh: line 7: no match: [:lower:]
status 1
$ touch e
$ echo [:lower:] [:upper:]
e e
$ echo hello | tr [:lower:] [:upper:]; echo "status $?"
hello
status 0
$ echo hello | tr '[:lower:]' '[:upper:]'
HELLO
- With nothing to match, bash leaves the word alone.
echo [:lower:] [:upper:]prints both words as typed, andtrgets the classes it expected. That is the book's situation: a home directory with no one-letter file names in it. [...]is a bracket expression, and this one is a set of characters.[:lower:]is:,l,o,w,e,r, and[:upper:]is:,u,p,e,r. A file namedematches both, andechoshows each word replaced bye.- So
trran astr e e, which translateseintoe.hellocame out still lowercase, with no message and status 0. Nothing points at quoting; it looks as iftrignored you. shopt -s failglobmakes a pattern with no match an error, so in the empty directory the command fails with a message and does not just happen to work. Theline 7is theevalinside the script'ssayhelper.failglobcannot catch thetouch ecase, because there the pattern did match.- The fix is quotes.
'[:lower:]'is not a pattern in any directory, in any of the three shells.
On a Mac¶
Nothing changes: the key above is shared. /bin/bash 3.2 matches the pattern, passes an unmatched word through and supports failglob just as bash 5.2 does. This chapter's Mac difference is inside tr itself, on the previous page. A Mac user is probably not typing into bash anyway, which is the next section.
In zsh and fish¶
zsh is the Mac's default shell, and it treats a pattern with no match as an error:
Verified output of tr_unquoted_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ echo hello | tr [:lower:] [:upper:]; echo "status $?"
(eval):1: no matches found: [:lower:]
status 1
$ setopt no_nomatch; echo hello | tr [:lower:] [:upper:]; unsetopt no_nomatch
HELLO
$ touch e
$ echo [:lower:] [:upper:]
e e
$ echo hello | tr [:lower:] [:upper:]; echo "status $?"
hello
status 0
$ echo hello | tr '[:lower:]' '[:upper:]'
HELLO
no matches found, status 1, andtrnever started. zsh'sNOMATCHoption is on by default. So the book's command, typed into a new Terminal window in a directory with no matching file, fails. bash behaves this way only withfailglob.setopt no_nomatchgives back bash's pass-through. Use quotes instead, because the option changes every pattern in the shell.- After
touch e, zsh does exactly what bash does: it runstr e e, the output stays lowercase, and the status is 0.
fish has no bracket wildcard. Its wildcards are * and **, and [ is an ordinary character:
Verified output of tr_unquoted_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ echo hello | tr [:lower:] [:upper:]
HELLO
$ touch e
$ echo [:lower:] [:upper:]
[:lower:] [:upper:]
$ echo hello | tr [:lower:] [:upper:]
HELLO
$ echo hello | tr '[:lower:]' '[:upper:]'
HELLO
The unquoted command works in both directories, and echo shows the words arriving as typed. Quoting still costs nothing in fish, and the quoted line works in the other two shells as well.
| bash | zsh | fish | |
|---|---|---|---|
| no file matches | tr gets [:lower:] and works |
error; tr does not run |
tr gets [:lower:] and works |
a file e exists |
runs tr e e: no change, status 0 |
runs tr e e: no change, status 0 |
tr gets [:lower:] and works |
| make no match an error | shopt -s failglob |
the default (NOMATCH) |
not a pattern |
| always right | tr '[:lower:]' '[:upper:]' |
the same | the same |
If you are coming from another library¶
- Encodings. The shell has no string type ↗: a shell word is only bytes, and the shell rewrites it before any program sees it. Pattern expansion, as on this page, is one of those rewrites.
- Perl. The one-liners in
-nand-pare a loop ↗ single-quote the program for the same reason:$,*and[belong to the shell before they reachperl.