If This, Then That: if and [[ ]]

if lets a script choose: the commands after then run only when a test passes, [[ ]] compares text with == and whole numbers with -lt and its friends, and if can also ask whether any command worked.

Apprentice · Unix · Linux · Updated

After this lesson you can

  • Write an if statement with elif and else, comparing text with == and != inside [[ ]]
  • Compare whole numbers in [[ ]] with -eq, -ne, -lt, -le, -gt and -ge, and explain why < compares them as text
  • Run commands only when another command worked, by putting that command itself after if

Key takeaways

  • if, then, else and fi let a script choose: the lines after then run only when the test passes, and the lines after else run when it doesn't.
  • elif adds another test, tried only when every test before it failed.
  • Inside [[ ]], == and != compare text, and -eq, -lt, -gt and their friends compare whole numbers. The spaces inside the brackets are required.
  • < inside [[ ]] compares text the way a dictionary does, so [[ 10 < 9 ]] passes. Use -lt for numbers.
  • if checks an exit code, where 0 means yes. [[ ]] is a command that answers 0 or 1, and any command, such as cp, can follow if.

Every evening you copy your essay into a backup folder, and a script could do it for you. But a script that runs the same lines every time is like a vending machine that drops a snack whether you paid or not: it copies when you didn't mean it to, and says "Copied" even when the copy failed. Real applications decide: an installer asks "Continue?", and a backup tool checks before it copies. In bash, deciding starts with if, and administrators' scripts are full of it: "if the disk is nearly full, send a warning".

These lines make the chapter's practice folder of school work. They're safe to run again:

mkdir -p ~/terminal-practice/smart-backup && cd ~/terminal-practice/smart-backup
printf 'Why the Town Library Matters\nFirst draft, two pages.\n' > essay.txt
printf 'Chapter 4: read by Friday\nLab report due Monday\n' > notes.txt
printf 'Hatchet\nThe Hobbit\nA Wrinkle in Time\n' > reading-list.txt
touch empty-draft.txt
mkdir -p photos && touch photos/science-fair.jpg

if, elif, else, fi

You already decide this way: if it's raining, take the umbrella; otherwise, your sunglasses. Bash writes it as if TEST; then … else … fi, read the way it's written: "if the test passes, then do these, else do those". fi is if backwards and marks the end. elif (read "else if") adds another test, tried only when the ones before it failed; elif and else are both optional. The usual test, [[ ]] (read "test"), checks one thing. Open nano backup.sh and write a backup that asks first, with read from Asking Questions: read:

#!/usr/bin/env bash
# backup.sh: copies essay.txt into backup/, if you say yes.
read -r -p "Back up essay.txt? (yes/no) " answer
if [[ "$answer" == "yes" ]]; then
  mkdir -p backup
  cp essay.txt backup/
  echo "Copied essay.txt."
elif [[ "$answer" == "no" ]]; then
  echo "Nothing copied."
else
  echo "Please answer yes or no."
fi

Save, chmod +x backup.sh, and run ./backup.sh twice, answering yes, then maybe:

Back up essay.txt? (yes/no) yes
Copied essay.txt.
Back up essay.txt? (yes/no) maybe
Please answer yes or no.

Bash tries the tests in order and runs only the first that passes.

Comparing text: == and !=

== (read "is the same as") passes when two pieces of text match exactly, and != (read "is not") when they differ. Every capital counts: answer Yes, and the script says "Please answer yes or no." Spaces count too: without the one after [[, bash reads [[maybe as a command's name:

answer=maybe
[[$answer == yes]]
bash: [[maybe: command not found

Keep the double quotes around "$answer" as well, the habit from Quotes: Double, Single and None.

Comparing numbers: -lt and its friends

A script should check it was given a file to copy. $# is how many arguments were typed (Arguments: $1, $# and "$@"), and numbers have their own comparisons:

Write Read it Passes when the left number is
-eq "equal" the same as the right one
-ne "not equal" different
-lt "less than" smaller
-le "less than or equal" smaller or the same
-gt "greater than" bigger
-ge "greater than or equal" bigger or the same

Why not <? Inside [[ ]], < compares text the way a dictionary sorts words, and 1 comes before 9. At the prompt, a ; stands in for each line break:

if [[ 10 < 9 ]]; then echo "less"; else echo "not less"; fi
if [[ 10 -lt 9 ]]; then echo "less"; else echo "not less"; fi
less
not less

if on a command: did it work?

Here's the secret of if: it runs a command and looks at its exit code (Did It Work? Exit Codes, && and ||), where 0 means yes. [[ ]] is a command that answers 0 when its test passes and 1 when it doesn't, so any command can follow if, cp included. Here's backup.sh grown up, copying whichever file you name:

#!/usr/bin/env bash
# backup.sh: copies the file you name into backup/.
if [[ $# -lt 1 ]]; then
  echo "Usage: ./backup.sh FILE"
  exit 1
fi
mkdir -p backup
if cp "$1" backup/; then
  echo "Copied $1."
else
  echo "The copy failed. Nothing was backed up."
  exit 1
fi

exit 1 stops the script and hands back exit code 1, "it didn't work". Run it with no name, a name, then a typo:

./backup.sh
./backup.sh essay.txt
./backup.sh essay.txs
Usage: ./backup.sh FILE
Copied essay.txt.
cp: cannot stat 'essay.txs': No such file or directory
The copy failed. Nothing was backed up.

cp's own message differs between systems; your script's line doesn't. Tutorials also write single brackets, [ "$temp" -gt 90 ]: the older test command, much the same, but [[ ]] forgives more. if and [[ ]] work in zsh too.

Check yourself

2 questions to try on the lesson page.

So what

With if, [[ ]] and exit codes, a script stops doing the same thing every time and starts doing the right thing: asking first, explaining how to run it, and owning up when something failed.

Key terms

Cheat sheets

Related