Shell Scripting Basics
Writing a real Bash script with variables, conditionals, loops, and command substitution.
Why shell scripting matters
Anything you'd type into the terminal more than once is a candidate for a script — a plain text file the shell reads and executes line by line. Deployment steps, backup routines, and health checks are very often just a .sh file with a handful of commands, some variables, and basic logic. You don't need to learn a whole new language: it's the same commands you already run interactively, saved to a file.
A real example
Here's a small but realistic deploy-check script that checks disk space, backs up a directory if space allows, and reports its result:
#!/bin/bash
# backup-check.sh — backs up a directory if there's enough free disk space
# --- variables ---
SOURCE_DIR="/var/www/app/storage"
BACKUP_DIR="/var/backups/app"
MIN_FREE_MB=500
DATE=$(date +%Y-%m-%d_%H-%M-%S) # command substitution
# --- ensure the backup directory exists ---
mkdir -p "$BACKUP_DIR"
# --- check available disk space (in MB) on the filesystem holding SOURCE_DIR ---
FREE_MB=$(df --output=avail -m "$SOURCE_DIR" | tail -n 1 | tr -d ' ')
# --- conditional ---
if [ "$FREE_MB" -lt "$MIN_FREE_MB" ]; then
echo "Not enough disk space: ${FREE_MB}MB free, need ${MIN_FREE_MB}MB. Aborting."
exit 1
fi
echo "Disk space OK (${FREE_MB}MB free). Starting backup..."
# --- loop over a fixed list of subdirectories to back up individually ---
for dir in logs cache uploads; do
if [ -d "$SOURCE_DIR/$dir" ]; then
tar -czf "$BACKUP_DIR/${dir}-${DATE}.tar.gz" -C "$SOURCE_DIR" "$dir"
echo "Backed up: $dir -> ${dir}-${DATE}.tar.gz"
else
echo "Skipping missing directory: $dir"
fi
done
echo "Backup complete: $BACKUP_DIR"
exit 0
Making it executable and running it:
$ chmod +x backup-check.sh
$ ./backup-check.sh
Disk space OK (14203MB free). Starting backup...
Backed up: logs -> logs-2026-08-20_09-30-00.tar.gz
Backed up: cache -> cache-2026-08-20_09-30-00.tar.gz
Skipping missing directory: uploads
Backup complete: /var/backups/app
Breaking down the key pieces
The shebang
#!/bin/bash
This must be the very first line. It tells the kernel which interpreter should run the rest of the file — here, Bash specifically. Without it, running the script directly (./script.sh) falls back on assumptions that can silently behave differently than you intended.
Variables
SOURCE_DIR="/var/www/app/storage"
MIN_FREE_MB=500
No spaces around = — SOURCE_DIR = "..." (with spaces) is a syntax error in Bash, because it looks like a command named SOURCE_DIR being passed arguments. Reference a variable's value with a $ prefix, and prefer wrapping it in quotes ("$SOURCE_DIR") so paths containing spaces don't get split into multiple arguments.
Command substitution
DATE=$(date +%Y-%m-%d_%H-%M-%S)
$(...) runs the command inside it and substitutes its output as a string — here, capturing the current timestamp into a variable. The older backtick syntax (`date`) does the same thing but doesn't nest cleanly, so $(...) is the modern standard.
The if conditional
if [ "$FREE_MB" -lt "$MIN_FREE_MB" ]; then
echo "..."
exit 1
fi
[ ... ] is actually a command (an alias for test) — the spaces around the brackets are required. Common comparison operators:
| Operator | Meaning |
|---|---|
-eq, -ne |
numeric equal / not equal |
-lt, -gt, -le, -ge |
numeric less-than, greater-than, etc. |
=, != |
string equal / not equal |
-f path |
true if path exists and is a regular file |
-d path |
true if path exists and is a directory |
-z string |
true if the string is empty |
exit 1 ends the script immediately with a non-zero status code, signaling failure to whatever called the script (a CI pipeline, a cron job, another script checking $?). exit 0 at the end signals success explicitly.
The for loop
for dir in logs cache uploads; do
if [ -d "$SOURCE_DIR/$dir" ]; then
# ...
fi
done
This iterates over a fixed word list. for loops are just as commonly used over command output or file globs:
for file in *.log; do
echo "Found log file: $file"
done
for line in $(cat hosts.txt); do
ping -c 1 "$line"
done
Making a script runnable
A script isn't executable by default — you have to explicitly grant it the execute permission bit (see File System and Permissions):
$ chmod +x backup-check.sh
$ ./backup-check.sh # ./ is required unless the script's directory is on $PATH
Without +x, attempting to run it directly fails with "Permission denied" — though you could still run it explicitly through the interpreter (bash backup-check.sh), which doesn't require the execute bit at all.
Common mistakes
- Spaces around
=in a variable assignment (NAME = "value") — Bash parses this as a command, not an assignment, and errors. - Forgetting to quote variables (
if [ -d $SOURCE_DIR/$dir ]instead of"$SOURCE_DIR/$dir") — a path containing a space breaks the comparison in ways that are hard to debug. - Forgetting
chmod +xand being confused by "Permission denied" when trying to run the script. - Not checking exit codes of commands that can fail (e.g., assuming
tarsucceeded) in a script that other automation depends on.
Interview questions
Q: What does the #!/bin/bash line at the top of a script do?
It's the "shebang" — it tells the kernel which interpreter to use to execute the rest of the file when the script is run directly (e.g., ./script.sh). Without it, the script may be interpreted incorrectly or fail to run as expected.
Q: How do you capture the output of a command into a variable in Bash?
Command substitution: VAR=$(command), which runs the command and assigns its standard output (with a trailing newline stripped) to VAR. The older backtick syntax does the same thing but is harder to nest.
Q: Why should you quote variables like "$FILE" inside conditionals and commands?
Without quotes, the shell performs word-splitting on the variable's value — a path containing a space would be treated as multiple separate arguments, breaking commands like [ -d $FILE ] or cp $FILE dest/ in subtle, hard-to-debug ways. Quoting preserves the value as a single argument.