Computing Foundations › The Command Line › Day 12
Hands-on lab — Day 12: Shell Scripting: Variables, Loops, and Conditionals
- ← Back to the Day 12 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-012-shell-scripting-variables-loops-and-conditionals/
Commands
Setup
cd labs/sections/computing-foundations/day-012-shell-scripting-variables-loops-and-conditionals Run
bash examples/backup_notes.sh examples/sample-notes
bash starter/backup_notes.sh examples/sample-notes Test
bash tests/run_tests.sh File tree
examples/backup_notes.sh examples/sample-notes/budget.csv examples/sample-notes/ideas.txt examples/sample-notes/meeting.md examples/sample-notes/README examples/sample-notes/recipe.md examples/sample-notes/shopping.txt expected-output/FIELDS.md expected-output/sample-run.txt metadata.yml README.md requirements/README.md security.md starter/backup_notes.sh starter/scripting-worksheet.md tests/run_tests.sh troubleshooting.md
Lab README
Day 012 lab — Write Your First Real Script
Lesson
- Lesson title: Shell Scripting: Variables, Loops, and Conditionals
- Day number: 12 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-012-shell-scripting-variables-loops-and-conditionals
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-012-shell-scripting-variables-loops-and-conditionalswhen the site is running.
Purpose
Day 12's lesson turns you from someone who types commands into someone who
writes them down as reliable scripts. This lab makes that concrete: you build
backup_notes.sh — a genuinely useful tool that takes a directory, counts its
files by extension, and prints a summary — completing it through five numbered
exercises that each add one real concept (an argument with a default, a
validation conditional, a function, a counting loop, and a report). By the end
you will have written a real script using variables, quoting, a function, a
loop, a conditional, and set -euo pipefail, and confirmed it with an
automated test suite.
Learning objectives
- Turn a sequence of commands into an executable, strict-mode shell script.
- Read a command-line argument with a sensible default (
${1:-.}). - Validate input with a conditional and signal failure with an exit code.
- Write a function and call it from a loop that counts files by extension.
- Quote every variable expansion so filenames with spaces are handled safely.
- Run an automated test that checks real behavior and interpret its output.
Prerequisites
- The Day 12 lesson (read it first — it explains every construct this lab uses).
- Days 8–11: comfort in the terminal, moving around the filesystem, and reading text.
- A terminal and any text editor. No programming experience beyond the course so far.
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon, bash 3.2.57).
- Linux — fully supported (any distribution with bash; uses only portable commands).
- Windows — run the scripts unmodified inside WSL (Windows Subsystem for Linux). Native PowerShell is a different language and is not covered here.
Hardware requirements
Any computer made in roughly the last 15 years. The lab only reads a directory listing and prints a report; it needs no particular RAM, disk, or GPU.
Required software
bash(3.2 or newer — preinstalled on macOS and Linux).- Standard OS utilities only:
basename,sort,uniq,printf— all preinstalled. - Optional: ShellCheck (a free, open-source linter) if you want to check your script for common bugs. It is not required to complete the lab.
Free and open-source options
Everything in this lab is free and open source: bash and every command used
ship with your OS. The optional ShellCheck linter is free and open source too.
No account, API key, network access, or purchase is needed.
Installation
None required. Copy this directory (or clone the repository) and change into it:
cd labs/sections/computing-foundations/day-012-shell-scripting-variables-loops-and-conditionals
To optionally install ShellCheck: brew install shellcheck (macOS) or
sudo apt install shellcheck (Debian/Ubuntu).
File structure
day-012-shell-scripting-variables-loops-and-conditionals/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── backup_notes.sh ← YOUR working file (5 exercises)
│ └── scripting-worksheet.md ← predict-then-run worksheet
├── examples/
│ ├── backup_notes.sh ← completed reference implementation
│ └── sample-notes/ ← a known directory to run against
├── tests/
│ └── run_tests.sh ← automated checks (fixture-based)
├── expected-output/
│ ├── sample-run.txt ← real captured run on sample-notes
│ └── FIELDS.md ← the fields every correct run prints
├── requirements/
│ └── README.md ← dependency statement
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the finished result first
bash examples/backup_notes.sh examples/sample-notes
## 2. Your task: complete the five exercises in the starter, then run it
bash starter/backup_notes.sh examples/sample-notes
## 3. Check your work
bash tests/run_tests.sh
What the commands do
bash examples/backup_notes.sh examples/sample-notes— runs the reference script against the sample directory: it takes the directory argument, validates it, loops over the files, uses theextension_offunction andsort | uniq -cto count each extension, and prints the summary.bash starter/backup_notes.sh examples/sample-notes— runs your in-progress script. The skeleton runs from the start; each exercise you complete makes the output more correct until it matches the reference.bash tests/run_tests.sh— builds a temporary directory with a known set of files (3.txt, 2.md, 1.csv, 1 file with no extension, plus a subdirectory), runs the reference script against it, and asserts the counts are exactly right; also checks the empty-directory and "not a directory" cases. Exits 0 on success, non-zero on any failure.
Expected output
Running the reference script on the bundled sample directory prints (see
expected-output/sample-run.txt):
=== Notes summary ===
Directory: sample-notes
Total files: 6
By extension:
(no extension): 1
csv: 1
md: 2
txt: 2
=== End of summary ===
Extensions are printed in sorted order, so (no extension) (which begins with
() sorts before the lettered extensions. expected-output/FIELDS.md
lists exactly which lines a correct run must print.
Validation steps
- Run
bash starter/backup_notes.sh examples/sample-notes— it must exit without errors. - Once you have completed all five exercises, its output must match
expected-output/sample-run.txtexactly. - Confirm the script handles a bad path:
bash starter/backup_notes.sh /no/such/dirprints an error to standard error and exits non-zero (check withecho $?). - Run the tests (next section) — all checks must pass.
Tests
bash tests/run_tests.sh
Expected final line: 11 checks, 0 failure(s). The command exits 0 on success
and non-zero on any failure, so it can run in CI. The tests target the
reference script in examples/; complete the starter and compare its output
against the same sample to check your own version.
Cleanup
Nothing to clean up: the scripts only read a directory and print a report —
they create, move, and delete nothing. The test suite writes only to a
temporary directory (mktemp -d) and removes it automatically on exit. To
reset your work, restore the starter from git:
git checkout -- starter/backup_notes.sh.
Troubleshooting
See troubleshooting.md for the full list (syntax errors,
unbound variable, spaces around =, wrong test operators, permission notes).
Security notes
See security.md. Short version: the scripts make no network calls, need no elevated privileges, and only read a directory listing to print a report — they never execute the files they find or accept untrusted input as commands.
Extension exercises
- Add a total-size line: inside the loop, sum each file's size (macOS
stat -f%z "${path}", Linuxstat -c%s "${path}") using command substitution, and printTotal size: N bytes. - Convert the byte total to mebibytes with integer division (
$((bytes / 1048576))) and print it alongside the byte count. - Add a guardrail: print a warning when the directory holds more than 100 files, the kind of check a real backup script uses to catch a mistakenly enormous folder.
- If you installed ShellCheck, run
shellcheck examples/backup_notes.shand read any suggestions; then deliberately remove a pair of quotes and watch it flag the bug.
Navigation
- Previous day: Day 11 — Environment Variables and Shell Configuration (
labs/sections/computing-foundations/day-011-environment-variables-and-shell-configuration/). - Next day: Day 13 — Package Managers: Homebrew, apt, and winget (
labs/sections/computing-foundations/day-013-package-managers-homebrew-apt-and-winget/).
Expected output
FIELDS.md
# Required output lines (all platforms)
A correct run of `backup_notes.sh DIRECTORY` prints, in order:
1. `=== Notes summary ===`
2. `Directory: <the directory you passed, or . for the default>`
3. `Total files: <count of regular files at the top level>`
4. `By extension:`
5. One indented line per extension, ` <ext>: <count>`, in sorted order —
or ` (none)` when the directory holds no files.
6. `=== End of summary ===`
Rules the counts must obey:
- Only **regular files** at the top level are counted; subdirectories are
skipped, and the script does not descend into them.
- Files with no dot in the name (such as `README`) are grouped under
`(no extension)`, which sorts before lettered extensions because it begins
with `(`.
- Extensions keep their original case (`photo.JPG` counts as `JPG`, not `jpg`).
- Hidden files whose names begin with a dot (such as `.gitignore`) are not
matched by the `*` glob and are therefore not counted.
Error and edge cases:
- Passing a path that is not a directory prints `Error: '<path>' is not a
directory` to **standard error** and exits with code `1`.
- An empty directory prints `Total files: 0` and ` (none)`, and exits `0`.
`sample-run.txt` in this directory is a real captured run against
`examples/sample-notes/` on macOS (Apple Silicon, bash 3.2.57, 2026-07-12).
Linux output is identical for the same inputs, since the script uses only
portable commands.
sample-run.txt
=== Notes summary ===
Directory: sample-notes
Total files: 6
By extension:
(no extension): 1
csv: 1
md: 2
txt: 2
=== End of summary ===
Source files
examples/backup_notes.sh (2324 bytes)
#!/usr/bin/env bash
# Day 012 lab — completed reference implementation.
#
# backup_notes.sh — summarize a directory of notes before you archive it.
# It takes a directory as its first argument (defaulting to the current
# directory), counts the regular files in it, and prints a breakdown by file
# extension. This is the kind of inventory you would generate before backing
# up or cleaning out a folder.
#
# Usage:
# bash backup_notes.sh [DIRECTORY]
# bash backup_notes.sh # summarizes the current directory
# bash backup_notes.sh ~/notes # summarizes ~/notes
set -euo pipefail
# --- Argument with a sensible default -------------------------------------
# Take the first argument, or fall back to "." (the current directory).
target="${1:-.}"
# --- Validate the input ---------------------------------------------------
# A tool that silently accepts nonsense is a trap: fail loudly instead.
if [ ! -d "${target}" ]; then
echo "Error: '${target}' is not a directory" >&2
exit 1
fi
# --- A small, reusable function -------------------------------------------
# Print the extension of a filename, or "(no extension)" when it has none.
extension_of() {
local name="$1"
if [ "${name}" = "${name%.*}" ]; then
# No dot in the name at all (e.g. README).
echo "(no extension)"
else
# Strip everything up to and including the last dot.
echo "${name##*.}"
fi
}
# --- Loop over the files, counting as we go -------------------------------
total=0
extensions=""
for path in "${target}"/*; do
# Skip anything that is not a regular file (directories, or an empty dir
# where the glob did not expand to a real path).
[ -f "${path}" ] || continue
total=$((total + 1))
extensions="${extensions}$(extension_of "$(basename "${path}")")
"
done
# --- Print the report -----------------------------------------------------
echo "=== Notes summary ==="
echo "Directory: ${target}"
echo "Total files: ${total}"
echo "By extension:"
if [ "${total}" -eq 0 ]; then
echo " (none)"
else
# sort groups identical extension lines together; uniq -c collapses each
# group into "count extension"; the loop reformats it for the report.
printf '%s' "${extensions}" | sort | uniq -c | while read -r count ext; do
echo " ${ext}: ${count}"
done
fi
echo "=== End of summary ==="
examples/sample-notes/budget.csv (23 bytes)
month,amount
July,1200
examples/sample-notes/ideas.txt (18 bytes)
Try the loop idea
examples/sample-notes/meeting.md (20 bytes)
# Weekly sync notes
examples/sample-notes/README (14 bytes)
Read me first
examples/sample-notes/recipe.md (23 bytes)
# Roux: butter + flour
examples/sample-notes/shopping.txt (18 bytes)
Buy milk and eggs
metadata.yml (642 bytes)
lesson_id: D012
day: 12
kind: shell-scripting
languages: [bash]
setup_commands:
- cd labs/sections/computing-foundations/day-012-shell-scripting-variables-loops-and-conditionals
run_commands:
- bash examples/backup_notes.sh examples/sample-notes
- bash starter/backup_notes.sh examples/sample-notes
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter/backup_notes.sh # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), bash 3.2.57; bash tests/run_tests.sh -> 11 checks, 0 failure(s).'
requirements/README.md (1138 bytes)
# Dependencies — Day 012 lab
**None beyond a POSIX shell.** This lab has no installable dependencies:
- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux distribution)
- Standard OS utilities: `basename`, `sort`, `uniq`, `printf`, `mktemp` — all
part of the base system on macOS and Linux.
## Optional: ShellCheck
[ShellCheck](https://www.shellcheck.net/) is a free, open-source static
analyzer for shell scripts. It is **entirely optional** — the lab is fully
completable without it — but it is an excellent habit to adopt. If you want to
try it:
- macOS: `brew install shellcheck`
- Debian/Ubuntu: `sudo apt install shellcheck`
- Fedora: `sudo dnf install ShellCheck`
Then run `shellcheck examples/backup_notes.sh` to see it in action. There is no
paid version; ShellCheck is free.
The companion formatter **shfmt** (also free and open source) is likewise
optional — it rewrites a script into a consistent style — and is not needed to
finish the lab.
There is deliberately no `requirements.txt` or `package.json` here: this lab
runs on a factory-fresh macOS or Linux machine with nothing installed.
starter/backup_notes.sh (3801 bytes)
#!/usr/bin/env bash
# Day 012 lab — YOUR working file. Build backup_notes.sh step by step.
#
# This skeleton already runs, but it is deliberately incomplete: five numbered
# exercises below add one real feature each. Complete them in order. After each
# one, run the script to see your progress:
#
# bash starter/backup_notes.sh examples/sample-notes
#
# The finished result should match examples/backup_notes.sh and pass the tests
# (bash tests/run_tests.sh). Try each exercise yourself before peeking.
set -euo pipefail
# ===========================================================================
# Exercise 1 — Accept a directory argument, with a sensible default.
# Replace the line below so that `target` is the first argument ($1), or "."
# (the current directory) when no argument is given. Use the ${1:-default}
# form: target="${1:-.}"
# ---------------------------------------------------------------------------
target="."
# ===========================================================================
# ===========================================================================
# Exercise 2 — Validate the input.
# Right now the script trusts whatever it is given. Add a conditional that
# checks the target is a directory, and if not, prints an error to standard
# error and exits with a non-zero code. Uncomment and complete:
#
# if [ ! -d "${target}" ]; then
# echo "Error: '${target}' is not a directory" >&2
# exit 1
# fi
# ===========================================================================
# ===========================================================================
# Exercise 3 — Write the extension_of function.
# This helper should print the extension of a filename, or "(no extension)"
# when there is no dot. Replace the stub body below with:
#
# local name="$1"
# if [ "${name}" = "${name%.*}" ]; then
# echo "(no extension)"
# else
# echo "${name##*.}"
# fi
# ---------------------------------------------------------------------------
extension_of() {
echo "file" # <-- replace this stub with the real logic (Exercise 3)
}
# ===========================================================================
# ===========================================================================
# Exercise 4 — Loop over the files and count them.
# Fill in the loop body so that, for each REGULAR file in "${target}", it:
# (a) skips non-files with: [ -f "${path}" ] || continue
# (b) adds one to total: total=$((total + 1))
# (c) records the extension: append extension_of "$(basename "${path}")"
# to the `extensions` variable, one per line (see examples/ for the
# exact idiom).
# ---------------------------------------------------------------------------
total=0
extensions=""
for path in "${target}"/*; do
: # <-- replace this no-op with the loop body (Exercise 4)
done
# ===========================================================================
# --- Print the report -----------------------------------------------------
echo "=== Notes summary ==="
echo "Directory: ${target}"
echo "Total files: ${total}"
echo "By extension:"
if [ "${total}" -eq 0 ]; then
echo " (none)"
else
# =========================================================================
# Exercise 5 — Print the by-extension breakdown.
# Turn the collected `extensions` into sorted counts. Replace the echo
# stub below with the pipeline:
#
# printf '%s' "${extensions}" | sort | uniq -c | while read -r count ext; do
# echo " ${ext}: ${count}"
# done
# -------------------------------------------------------------------------
echo " (fill in Exercise 5 to see the breakdown)"
# =========================================================================
fi
echo "=== End of summary ==="
starter/scripting-worksheet.md (2692 bytes)
# Scripting worksheet — predict, then run
The best way to know you understand a loop is to predict what it will do
*before* you run it. Do the prediction below by hand first; only then run the
script and compare. A wrong prediction you understand afterwards teaches more
than a lucky right one.
## The input directory
The lab ships a sample directory at `examples/sample-notes/`. It contains
exactly these files:
```text
sample-notes/
├── shopping.txt
├── ideas.txt
├── meeting.md
├── recipe.md
├── budget.csv
└── README
```
## Step 1 — Predict (fill this in before running anything)
Using your understanding of how `backup_notes.sh` counts files by extension,
predict its report for `examples/sample-notes/`:
- Total files: __________
- `txt`: __________
- `md`: __________
- `csv`: __________
- `(no extension)`: __________
Then predict the order the extensions will be printed in. (Hint: the script
pipes the extensions through `sort` before counting — so what determines the
order?)
Predicted order (top to bottom): ________________________________________
## Step 2 — Run and compare
From the lab directory, run your completed script against the sample:
```bash
bash examples/backup_notes.sh examples/sample-notes
```
(Use `examples/backup_notes.sh` to check against the reference, or
`starter/backup_notes.sh` once you have completed all five exercises — they
should produce identical output.)
Write down the actual result:
- Total files: __________
- `txt`: __________
- `md`: __________
- `csv`: __________
- `(no extension)`: __________
## Step 3 — Reconcile
- Did your totals match? If not, which file did you miscount, and why?
- Did the *order* match your prediction? Explain in one sentence what `sort`
did to the extension list, and where `(no extension)` landed relative to the
letters — and why.
- The `README` file has no dot in its name. Trace how the `extension_of`
function decides it has "(no extension)": what does `"${name%.*}"` produce
for `README`, and why does that equal `README` itself?
## Step 4 — A second prediction (harder)
Suppose you add two more files to the directory: `photo.JPG` (uppercase
extension) and `.gitignore` (a dotfile whose name begins with a dot). Predict:
- Will `photo.JPG` be counted under `JPG` or `jpg`? (The script does not change
case — so which?)
- Will `.gitignore` be counted, and if so, under what extension? Think about
what `"${name%.*}"` does to a name that starts with a dot, and check your
answer by creating the files and re-running. Note anything that surprised
you — this is exactly the kind of edge case real scripts must handle.
tests/run_tests.sh (3376 bytes)
#!/usr/bin/env bash
# Tests for the Day 012 lab. Run from anywhere:
# bash tests/run_tests.sh
#
# Builds a temporary directory with a KNOWN set of files, runs the completed
# reference script (examples/backup_notes.sh) against it, and checks that the
# reported counts are exactly right. Also checks the empty-directory case and
# the "not a directory" error path. Cleans up the temp directory on exit.
set -u
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
script="${lab_dir}/examples/backup_notes.sh"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# A temp workspace we always clean up, even if a check fails.
work="$(mktemp -d)"
cleanup() { rm -rf "${work}"; }
trap cleanup EXIT
# --- Fixture: a known set of files ----------------------------------------
# 3 x .txt, 2 x .md, 1 x .csv, 1 file with no extension => 7 files total.
fixture="${work}/notes"
mkdir -p "${fixture}"
printf 'a\n' >"${fixture}/one.txt"
printf 'b\n' >"${fixture}/two.txt"
printf 'c\n' >"${fixture}/three.txt"
printf 'd\n' >"${fixture}/alpha.md"
printf 'e\n' >"${fixture}/beta.md"
printf 'f\n' >"${fixture}/data.csv"
printf 'g\n' >"${fixture}/README"
# A subdirectory that must NOT be counted as a file.
mkdir -p "${fixture}/subfolder"
echo "Testing ${script} against a known fixture ..."
if ! out="$(bash "${script}" "${fixture}" 2>&1)"; then
check "script exits 0 on a valid directory" "no"
echo "${out}" | sed 's/^/ /'
else
check "script exits 0 on a valid directory" "yes"
fi
# Header and footer present.
echo "${out}" | grep -q '^=== Notes summary ===$' && check "prints header" "yes" || check "prints header" "no"
echo "${out}" | grep -q '^=== End of summary ===$' && check "prints footer" "yes" || check "prints footer" "no"
# Total files should be exactly 7 (the subfolder is excluded).
echo "${out}" | grep -q '^Total files: 7$' && check "counts 7 total files (excludes subdirectory)" "yes" || check "counts 7 total files (excludes subdirectory)" "no"
# Per-extension counts, exactly.
echo "${out}" | grep -q '^ txt: 3$' && check "counts 3 .txt files" "yes" || check "counts 3 .txt files" "no"
echo "${out}" | grep -q '^ md: 2$' && check "counts 2 .md files" "yes" || check "counts 2 .md files" "no"
echo "${out}" | grep -q '^ csv: 1$' && check "counts 1 .csv file" "yes" || check "counts 1 .csv file" "no"
echo "${out}" | grep -q '^ (no extension): 1$' && check "counts 1 file with no extension" "yes" || check "counts 1 file with no extension" "no"
# --- Empty directory: total 0, still exits 0 ------------------------------
empty="${work}/empty"
mkdir -p "${empty}"
if empty_out="$(bash "${script}" "${empty}" 2>&1)"; then
check "empty directory exits 0" "yes"
else
check "empty directory exits 0" "no"
fi
echo "${empty_out}" | grep -q '^Total files: 0$' && check "empty directory reports 0 files" "yes" || check "empty directory reports 0 files" "no"
# --- Error path: not a directory should exit non-zero ---------------------
if bash "${script}" "${work}/does-not-exist" >/dev/null 2>&1; then
check "missing directory exits non-zero" "no"
else
check "missing directory exits non-zero" "yes"
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 012 lab
syntax error near unexpected token 'done' or 'fi'
An if needs ; then after its condition, and a for/while needs do
after its header. A missing one of these confuses the shell until it reaches
the matching done/fi. Compare your file line by line against
examples/backup_notes.sh.
backup_notes.sh: line N: target: unbound variable
You are running under set -u (strict mode) and referenced a variable that was
never assigned — almost always a typo in the name (${targett} instead of
${target}). That is strict mode doing its job: fix the spelling.
command not found right after an assignment
You put spaces around the =. Shell assignment must have no spaces:
total=0, never total = 0. With spaces, the shell tries to run a command
called total.
The counts look wrong
- A subdirectory got counted. It should not — the loop uses
[ -f "${path}" ]to keep only regular files. Make sure you did not change that test to-e. - A file with a space in its name broke the count. You have an unquoted
variable. Every expansion must be quoted:
"${target}"/*,"${path}". .gitignoreand other dotfiles are missing. That is expected: the*glob does not match names beginning with a dot, so hidden files are not counted.
Comparing numbers behaves oddly
Use integer operators for numbers: [ "${total}" -eq 0 ], -gt, -lt.
Reserve = for string comparison. [ "${total}" = 0 ] compares text, which
can surprise you when the value is empty or has leading spaces.
Permission denied when running the script
You do not need to make the file executable — run it through bash explicitly:
bash starter/backup_notes.sh. If you prefer ./starter/backup_notes.sh,
first run chmod +x starter/backup_notes.sh.
The starter prints Total files: 0 even though the directory has files
That is the starting state: the loop body is a no-op (:) until you complete
Exercise 4, and target is hard-coded until you complete Exercise 1. Finish
the exercises in order and the counts appear.
Windows: bash is not recognized
Install WSL (wsl --install), open the Ubuntu terminal, and run the Linux
commands there. Native Windows PowerShell uses a different scripting language
not covered by this lab.
Security notes
Security notes — Day 012 lab
-
What the scripts do:
backup_notes.shreads a directory listing and prints a summary. It only reads file names and metadata — it never opens, executes, moves, or deletes the files it finds. It makes no network connections, writes no files, and changes no settings. The test suite writes only to a temporary directory created withmktemp -dand deletes it automatically. -
Never runs untrusted input as commands. The script treats its argument purely as a directory path and the file names purely as data. Because every variable expansion is quoted (
"${target}","${path}"), a file named with spaces or shell metacharacters cannot be turned into extra commands — quoting is the security boundary here, not just a style choice. -
Privileges: everything runs as your normal user. Nothing in this lab needs
sudo. If any tutorial ever tells you tosudoa shell script you have not read, that is your cue to stop and read it first. -
Reading before running: both scripts are short and commented — read them before you run them. Piping a script off the internet straight into the shell (
curl … | bash) hands a stranger the power to run any command as you; this course's habit is that every lab script is small enough to read and understand before executing. -
Optional ShellCheck: running the optional ShellCheck linter is itself a security practice — it flags unquoted variables and other patterns that turn data into unintended commands.