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:
Make it executable and run:
You should see something like:
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 ofdateis 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:
Math (integer only in plain bash):
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:
Conditionals¶
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:
[[ 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¶
Loop over a glob:
C-style:
While loop:
Reading user input¶
read reads one line from stdin.
Functions¶
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 $(...).
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:
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"
./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 pipefailfor safer scripts. - Always quote variables.
Next: Editing in the terminal →