>_ Operating systems & Linux

Bash scripting basics: variables, conditions and loops

How to write your first useful Bash scripts: shebang, variables, quoting, arguments, if, for and while loops, exit codes and safer script options.

When you notice you are typing the same sequence of commands for the third time, it is time to write a script. A Bash script is nothing more than a text file containing the commands you would type by hand, plus a few constructs for making decisions and repeating operations. You do not need to be a programmer: a few rules are enough, and above all a few prudent habits.

The first script

#!/usr/bin/env bash
echo "Free space on the main disk:"
df -h /

The first line, called the shebang, tells the system which interpreter to use. Once the file is saved as space.sh, you make it executable with chmod +x space.sh and run it with ./space.sh. The ./ is needed because, for safety, the current folder is not among the paths where the shell looks for commands.

Variables and quoting

folder="/home/anna/Documents"
date=$(date +%Y-%m-%d)
echo "Backup of $folder on $date"

Two rules prevent many errors. First: no spaces around the equals sign; folder = "..." is read as a command called folder. Second: always put variables in double quotes when you use them. If a variable contains Holiday photos, without quotes rm $file becomes rm Holiday photos and tries to delete two separate files. With rm "$file" the name stays whole.

The $(command) syntax puts a command's output into a variable. Single quotes, on the other hand, prevent any substitution: echo '$date' prints $date literally.

Script arguments

Arguments passed on the command line are available as $1, $2 and so on; $# gives their number and "$@" holds them all, each correctly separated. A check at the start makes the script friendlier:

if [ "$#" -ne 1 ]; then
    echo "Usage: $0 folder" >&2
    exit 1
fi

Conditions with if

The structure is if ... then ... else ... fi. The most common tests:

Test True if
[ -f "$x" ] $x is an existing file
[ -d "$x" ] $x is an existing directory
[ -z "$x" ] the string is empty
[ "$a" = "$b" ] the strings are equal
[ "$n" -gt 10 ] the number is greater than 10

For numeric comparisons use -eq, -ne, -lt, -le, -gt, -ge, not the < and > symbols, which inside square brackets would be read as redirections. Bash also offers double brackets [[ ... ]], which are more forgiving with empty variables.

for and while loops

The for loop repeats a block for each item in a list:

for photo in *.jpg; do
    echo "Processing $photo"
    cp -- "$photo" "archive/$photo"
done

The double dash -- tells the command that the following arguments are not options: it protects against files whose name starts with a hyphen.

The while loop repeats as long as a condition is true. Its most typical use is reading a file line by line:

while IFS= read -r line; do
    echo "Server: $line"
done < servers.txt

IFS= and -r stop leading spaces and backslashes from being altered: it is the form to use almost always.

Exit codes

Every command ends with a number: 0 means success, any other value an error. The $? variable holds the last command's code. Your script should also exit with exit 0 when all goes well and with a different value when something fails: that way it can be used in chains with && or in a scheduler that reports errors.

The three safety options

Many scripts start with this line after the shebang:

set -euo pipefail
  • -e stops the script at the first failing command, instead of carrying on in an inconsistent state;
  • -u treats the use of an undefined variable as an error, so a typo does not produce an empty path;
  • -o pipefail makes a pipe fail if any of its commands fails, not just the last.

The value of -u is obvious with an example: if you mistakenly write rm -rf "$FOLDR/" instead of $FOLDER, without -u the variable is empty and the command becomes rm -rf "/". With -u the script stops first.

Functions and readability

Functions group repeated operations:

log() {
    echo "$(date '+%H:%M:%S') $*"
}
log "Starting copy"

Comments with #, clear variable names and short functions keep the script understandable even months later. Before putting it into use, the ShellCheck tool analyses the code and flags missing quotes, badly written tests and other common mistakes: it is one of the best ways to learn.

When to switch to another language

Bash is ideal for orchestrating commands and handling files. When a script grows beyond a few hundred lines, needs complex data structures or non-trivial calculations, it is better to move to a language such as Python, which offers better error handling and more suitable libraries.

In short

  • A script starts with the shebang, is made executable with chmod +x and run with ./.
  • No spaces around =, and variables always in double quotes.
  • Use -eq, -gt and the like for numbers; while IFS= read -r to read files line by line.
  • set -euo pipefail stops the script on errors and undefined variables.
  • Check scripts with ShellCheck and move to Python when the logic gets complicated.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. With degrees in computer science and psychology, he combines technical analysis with an understanding of the human factor.

Read also