Hands-on guide to writing and running shell scripts (bash). You can try everything in WSL/VirtualBox.
1) What’s a shell script?
A text file with commands you could type in the terminal—saved so you can run them again and again.
Typical first line (shebang) tells the OS which shell to use:
#!/usr/bin/env bash(Portable and recommended.)
2) Your first script (hello)
1.
Create a file hello.sh:
#!/usr/bin/env bashecho "Hello, $USER! Today is $(date '+%A, %d %b %Y')."2. Make it executable and run:
chmod +x hello.sh./hello.sh# or: bash hello.sh # runs with bash even if not executableIf you see ^M or
“bad interpreter”: file has Windows line endings. Fix with:
sudo apt install dos2unixdos2unix hello.sh3) Script structure you’ll reuse
#!/usr/bin/env bashset -Eeuo pipefail# -e: exit on error -u: error on unset vars# -o pipefail: catch errors in pipelines# -E: keep ERR traps working in functions # ------------- config / defaults -------------LOG_DIR="${HOME}/logs"mkdir -p "$LOG_DIR" # ------------- functions ---------------------log() { printf '[%(%F %T)T] %s\n' -1 "$*"; } # ------------- main logic --------------------log "Starting job…"# your commands herelog "All done."4) Variables & quoting (super important)
name="Aisha"echo "$name" # good: quoted (safe with spaces)echo $name # risky: word splittingn=$((5+7)) # arithmetic → 12PATH="$HOME/bin:$PATH" # prepend to PATH·
Always
quote "${var}" in scripts.
·
Use $(command) for command substitution (not backticks).
5) Arguments & exit status
·
Positional args: $0 (script name), $1, $2…; $# count; $@ all args (preserve
spaces when quoted).
·
Last command’s
exit code: $? (0 = success).
#!/usr/bin/env bashset -Eeuo pipefailname="${1:-Student}" # default if $1 missingecho "Hello, $name"echo "Previous exit code was: $?"exit 06) Conditions, tests, and case
file="notes.txt"if [[ -f "$file" ]]; then echo "File exists"elif [[ -d "$file" ]]; then echo "It's a directory"else echo "Not found"fi ext="png"case "$ext" in jpg|jpeg) echo "JPEG image" ;; png) echo "PNG image" ;; *) echo "Unknown" ;;esacCommon test flags: -f file, -d dir, -e exists, -s non-empty, -x
executable, -r readable, -w
writable, -nt newer than, -ot older
than.
7) Loops & arrays
# for loopfor f in *.txt; do echo "Found: $f"done # while loop (read lines)while IFS= read -r line; do echo ">$line"done < file.txt # arrays (bash)arr=(alpha beta gamma)echo "${arr[1]}" # betafor x in "${arr[@]}"; do echo "$x"; done8) Functions & return codes
greet() { local who="${1:-World}" echo "Hello, $who"} greet "Linux"·
return sets function exit code (0/1/…); echo prints output.
·
Use local for function-local variables.
9) Input/Output, pipes, here-docs
echo "Log line" >> app.log # appendwc -l < data.txt # read from filegrep -i "error" app.log | tee errs.txt # see + save # here-doc (write multi-line text)cat > config.ini <<'EOF'[app]env=prodEOF10) Parsing options with getopts (idiomatic)
#!/usr/bin/env bashset -Eeuo pipefail usage(){ echo "Usage: $0 [-n name] [-v] file"; } name="Student"; verbose=0while getopts ":n:v" opt; do case "$opt" in n) name="$OPTARG" ;; v) verbose=1 ;; \?) usage; exit 2 ;; esacdoneshift $((OPTIND-1)) # move past parsed options file="${1:-}"[[ -z "$file" ]] && { usage; exit 2; } (( verbose )) && echo "Name: $name | File: $file"echo "Hello, $name. File has $(wc -l < "$file") lines."Run:
./tool.sh -n Aisha -v notes.txt11) Traps & cleanup (robust scripts)
Clean temporary files even on Ctrl+C or errors:
tmp="$(mktemp -d)"cleanup(){ rm -rf "$tmp"; }trap cleanup EXIT INT TERM # …use "$tmp"…12) Running scripts (5 common ways)
./run.sh # needs +x and executable shebangbash run.sh # run with bash explicitlybash -x run.sh # debug: show commands as they runENV=prod ./run.sh # set an env var for this runPATH="$HOME/bin:$PATH" ./run.shWhere to place scripts
·
Personal: ~/bin (add to PATH in ~/.bashrc)
·
System-wide: /usr/local/bin (needs sudo)
13) Cron: run on a schedule (intro)
crontab -e# Every day at 01:3030 1 * * * /home/you/bin/backup.sh >> /home/you/logs/backup.log 2>&1Cron has a minimal environment—use absolute paths and export what you need inside the script.
14) Debugging checklist
·
bash
-x script.sh (trace), or add set -x temporarily.
·
Print variables: declare -p var arr.
·
Check exit codes:
cmd || echo "failed:
$?".
·
Validate shell: #!/usr/bin/env bash and run with bash, not
/bin/sh.
·
Static analysis: sudo apt install shellcheck &&
shellcheck script.sh.
15) Three mini projects (copy-paste ready)
A) Safer backup (with timestamp, logging)
#!/usr/bin/env bashset -Eeuo pipefailSRC="${1:-$HOME/Documents}"DST="${2:-$HOME/backups/$(date +%F_%H-%M-%S)}"mkdir -p "$DST"rsync -a --delete "$SRC"/ "$DST"/echo "Backup completed to $DST"Run: ./backup.sh ~/lab ~/backup_dir
B) Log scanner (summaries)
#!/usr/bin/env bashset -Eeuo pipefaillog="${1:?usage: $0 LOGFILE}"errs=$(grep -c -i 'error' "$log" || true)warns=$(grep -c -i 'warn' "$log" || true)echo "Errors: $errs Warnings: $warns"C) Tiny CLI with subcommands
#!/usr/bin/env bashset -Eeuo pipefailcmd="${1:-help}"; shift || truecase "$cmd" in greet) echo "Hello, ${1:-Student}";; sum) awk '{s+=$1} END{print s}' "${1:-/dev/stdin}";; help|*) echo "Usage: $0 {greet [name]|sum file}";;esac16) Safety rules (student edition)
·
Always quote variables: "$var".
·
Prefer set -Eeuo pipefail.
·
Use -- to stop option parsing when forwarding args: cmd -- "$file".
·
Avoid sudo in scripts unless required; document why.
· Don’t rely on current directory; use absolute paths or:
·cd "$(dirname "$0")" # run relative to script’s location
Exam-ready bullets
·
Shebang selects interpreter; make script executable with chmod +x.
·
Use set -euo pipefail for robustness; quote variables.
·
Arguments: $1..$9, $#, $@; parse options with getopts.
·
Tests: [[ … ]] with -f/-d/-e, etc.; loops: for/while; functions with local.
·
Exit
status: 0
success; check with $?.
·
Traps clean up temp files; use mktemp.
·
Debug with bash -x, lint with shellcheck.
· Cron needs absolute paths and a minimal environment.
If you want, I can turn these into a step-by-step lab sheet (with checkboxes) or a print-ready cheat sheet for your class.