Skip to content

09 - Shell Scripting Basics

What this session is

About an hour. You'll learn to write shell scripts - files containing a sequence of commands. Variables, conditionals, loops, command-line arguments. Enough to automate routine tasks.

Your first script

Create a file greet.sh:

#!/bin/bash
echo "Hello, $USER!"
echo "Today is $(date)"
echo "You are in $(pwd)"

Make it executable and run:

chmod +x greet.sh
./greet.sh

You should see something like:

Hello, alice!
Today is Sat May 17 14:23:45 BST 2026
You are in /home/alice/practice

What's new:

  • #!/bin/bash - the shebang line. Tells the OS this file should be run by bash. Always the first line of a shell script.
  • $USER - a variable ("interpolation" - bash substitutes the value).
  • $(date) - command substitution. The output of date is inserted here.

Variables

name="Alice"
greeting="Hello, $name"
echo "$greeting"            # Hello, Alice
echo "$greeting!"           # Hello, Alice!

Rules: - name=value - assignment. No spaces around = (it's not a comparison). - $name - read the variable. - Quote variables when they might contain spaces: echo "$path" not echo $path.

You can do command substitution:

files=$(ls)
count=$(ls | wc -l)
echo "There are $count files: $files"

Math (integer only in plain bash):

a=5
b=3
sum=$((a + b))
echo "$sum"                 # 8

The $(( ... )) is arithmetic expansion.

Command-line arguments

When you run ./script.sh foo bar, the script can access the arguments:

  • $1, $2, ... - first, second, ... argument.
  • $0 - script name.
  • $# - number of arguments.
  • $@ - all arguments (each as a separate word).
#!/bin/bash
echo "Script: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "Argument count: $#"
echo "All args: $@"

Run as ./script.sh alice bob:

Script: ./script.sh
First arg: alice
Second arg: bob
Argument count: 2
All args: alice bob

Conditionals

if [ "$1" = "hello" ]; then
    echo "you said hello"
else
    echo "you didn't say hello"
fi

Notes: - if condition; then ... fi - fi ends the block. (Yes, "fi" - "if" backward. Bash quirk.) - [ ... ] is the test command (literally a command named [). Spaces matter: [ "$x" = "y" ] works; ["$x"="y"] doesn't. - For strings: = (or ==), !=. - For integers: -eq, -ne, -lt, -le, -gt, -ge. - Negation: if ! [ ... ]; then ....

There's a modern [[ ... ]] form:

if [[ "$x" == "hello" ]]; then ...

[[ is bash-specific; supports more (regex, glob patterns). Use it for new scripts; [ for maximum portability.

File tests:

if [ -f "$file" ]; then echo "exists and is a regular file"; fi
if [ -d "$dir" ]; then echo "exists and is a directory"; fi
if [ -e "$path" ]; then echo "exists (any type)"; fi
if [ -r "$file" ]; then echo "readable"; fi
if [ -w "$file" ]; then echo "writable"; fi
if [ -x "$file" ]; then echo "executable"; fi

Loops

for name in alice bob chioma; do
    echo "Hello, $name"
done

Loop over a glob:

for file in *.txt; do
    echo "Processing $file"
done

C-style:

for ((i=1; i<=5; i++)); do
    echo "$i"
done

While loop:

n=10
while [ $n -gt 0 ]; do
    echo "$n"
    n=$((n - 1))
done

Reading user input

echo "What's your name?"
read name
echo "Hello, $name"

read reads one line from stdin.

Functions

greet() {
    echo "Hello, $1"
}

greet "Alice"
greet "Bob"

Inside the function: - $1, $2, ... - function arguments (not script arguments). - return N - exit code 0-255 (not a value). For returning a string, echo it and capture with $(...).

add() {
    echo $(($1 + $2))
}

sum=$(add 3 4)
echo "$sum"        # 7

A real script: clean temp files older than 7 days

#!/bin/bash
# clean-temp.sh - delete /tmp/myapp/*.log files older than 7 days

TARGET_DIR="/tmp/myapp"

if [ ! -d "$TARGET_DIR" ]; then
    echo "Directory $TARGET_DIR doesn't exist"
    exit 1
fi

count=$(find "$TARGET_DIR" -name "*.log" -mtime +7 | wc -l)
echo "Found $count old log files in $TARGET_DIR"

if [ "$count" -gt 0 ]; then
    find "$TARGET_DIR" -name "*.log" -mtime +7 -delete
    echo "Deleted $count files."
fi

New things: - # comment - anything after # is a comment. - exit 1 - exit the script with status code 1 (nonzero = failure). 0 is success.

Robust shell scripts: best practices

Two lines you should put at the top of every script you write:

#!/bin/bash
set -euo pipefail
  • set -e - exit immediately if any command fails.
  • set -u - error if you use an undefined variable.
  • set -o pipefail - pipelines fail if any command in them fails (not just the last).

Together: "fail fast, fail loud" - exactly what you want in shell scripts.

Also: always quote variables. "$file" not $file. Spaces and special characters in filenames break unquoted variables in surprising ways.

#!/bin/bash
set -euo pipefail

# Always quote
for file in "$@"; do
    if [ -f "$file" ]; then
        echo "Processing: $file"
    fi
done

Exercise

Write a script ~/practice/info.sh:

#!/bin/bash
set -euo pipefail

echo "User: $USER"
echo "Host: $(hostname)"
echo "Date: $(date)"
echo "Uptime: $(uptime)"
echo "Disk free:"
df -h | head -n 2

Make it executable: chmod +x info.sh. Run: ./info.sh. Read the output.

Stretch 1: modify it to accept an optional argument - a username - and report whether that user exists on the system (id "$1" >/dev/null 2>&1 returns 0 if exists, nonzero if not).

Stretch 2: write backup.sh PATH that copies the given file/directory to a timestamped backup:

#!/bin/bash
set -euo pipefail

src="$1"
dest="${src}.backup.$(date +%Y%m%d-%H%M%S)"
cp -r "$src" "$dest"
echo "Backed up to $dest"
Run as ./backup.sh ~/practice/info.sh. Check the backup file appears.

What you might wonder

"Should I learn bash or sh (POSIX shell)?" Bash for daily use. POSIX sh (run via /bin/sh) is more portable (works on systems without bash) but more limited. For shipping scripts: shebang #!/usr/bin/env bash and target bash.

"What about Python instead of bash?" For anything more than ~50 lines, use Python (or Go, or any "real" language). Bash is great for "string commands together"; it's a poor general-purpose language. The rule of thumb: if you need an array, switch to Python.

"What's ${var} vs $var?" Same thing. ${var} is the unambiguous form; useful when the next character would confuse the parser: ${name}_id (vs $name_id which would look up name_id).

"How do I debug a script?" Run with bash -x script.sh - prints each command before executing. Or add set -x inside the script to enable tracing partway. Disable with set +x.

Done

  • Write a shebang script.
  • Use variables and command substitution.
  • Read command-line arguments.
  • Use if, for, while.
  • Define functions.
  • Apply set -euo pipefail for safer scripts.
  • Always quote variables.

Next: Editing in the terminal →

Comments