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:

Bash
#!/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:

Bash
$ 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

Bash
#!/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

Bash
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

Bash
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

Bash
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

Bash
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:

Bash
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):

Bash
$ 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 +x and being confused by "Permission denied" when trying to run the script.
  • Not checking exit codes of commands that can fail (e.g., assuming tar succeeded) 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.