Why this lesson
Scripts constantly ask questions: does this file exist? Is this variable empty? Did the user type prod or something else? In bash, a question is a command that exits 0 for "yes" and non-zero for "no" - so if (6.3) can use it. There are two such commands, [ ] and [[ ]], and the older one breaks on exactly the values quoting (6.6) warned you about.
What you need to know already: if runs a command and checks its exit status (6.3, 6.5); word splitting and globbing (6.6).
Use [[ ]]
[ is an ordinary command (there is even a program at /usr/bin/[), whose last argument must be ]. Because it is a command, its arguments go through word splitting and globbing first:
name="two words"
[ $name = "two words" ] # bash: [: too many arguments
[[ $name = "two words" ]] # fine - no splitting inside [[ ]]
The first line became [ two words = "two words" ] - too many words for [. [[ ]] is bash syntax (bash reads the whole expression itself), so no splitting happens inside it. That gives you:
[[ -n $var ]] true if var is not empty (quote anyway, from habit)
[[ $a == foo* ]] glob matching: does $a start with "foo"?
[[ $env =~ ^(dev|prod)$ ]] REGEX matching (below)
[[ -f $f && -r $f ]] && and || inside the test
[[ ! -d $dir ]] ! = not
Use [ ] only when the script must run under /bin/sh (dash, 6.1).
The tests worth knowing
-e PATH exists -z STR empty string
-f PATH regular file -n STR non-empty string
-d PATH directory STR = STR / != string comparison
-r -w -x readable/writable/executable
-s PATH exists and is non-empty
-L PATH is a symlink
N -eq -ne -lt -le -gt -ge N integer comparison
(-eq = equal, -ne not equal, -lt less than, -le less or equal, -gt greater than, -ge greater or equal.)
Note = compares strings (text), -eq compares integers. [[ "10" > "9" ]] is false: as text, "10" comes before "9" (it compares the first character, 1 vs 9). Use -gt for numbers, or (( 10 > 9 )) (below).
=~ and regular expressions
A regular expression (regex) is a pattern that describes text - more powerful than a glob. The few pieces you need now:
^ start of the text $ end of the text
( | ) one of the alternatives [0-9] any one digit
+ one or more of the thing before it
\. a real dot (a bare . means "any character")
So ^(dev|prod)$ means "the whole text is exactly dev or prod". Chapter 7 (7.3) covers regex fully.
[[ $x =~ REGEX ]] is true when $x matches. Parts in ( ) are capture groups: bash saves what each one matched in the array BASH_REMATCH:
version=2.14.1
if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
major=${BASH_REMATCH[1]} # 2
minor=${BASH_REMATCH[2]} # 14
fi
Do not quote the regex - a quoted right-hand side is matched literally:
[[ $env =~ "^(dev|prod)$" ]] # literal string match. Always false.
[[ $env =~ ^(dev|prod)$ ]] # regex. Correct.
If the pattern is in a variable, that variable must be unquoted too - which is one of the few places you deliberately leave off the quotes.
(( )) for arithmetic
count=0
(( count++ ))
(( count > 5 )) && echo "plenty"
if (( $# < 1 )); then usage; fi
(( )) does arithmetic (integer maths) and comparisons; count++ adds 1. $(( )) does the same but gives you the result as text: n=$((n + 1)). No $ is needed on variables inside. $# is the number of arguments (6.6).
Careful with set -e: (( )) exits non-zero when its result is 0. So (( count++ )) when count is 0 counts as a failure, and kills the script. Use count=$((count + 1)) instead.
What you can now do
- Write file, string and number tests with
[[ ]], and know why[ $x = y ]breaks. - Check a value against a regex with
=~, without quoting the regex.