Computing FoundationsThe Command Line › Day 12

Hands-on lab — Day 12: 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

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 the extension_of function and sort | uniq -c to 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

  1. Run bash starter/backup_notes.sh examples/sample-notes — it must exit without errors.
  2. Once you have completed all five exercises, its output must match expected-output/sample-run.txt exactly.
  3. Confirm the script handles a bad path: bash starter/backup_notes.sh /no/such/dir prints an error to standard error and exits non-zero (check with echo $?).
  4. 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

  1. Add a total-size line: inside the loop, sum each file's size (macOS stat -f%z "${path}", Linux stat -c%s "${path}") using command substitution, and print Total size: N bytes.
  2. Convert the byte total to mebibytes with integer division ($((bytes / 1048576))) and print it alongside the byte count.
  3. 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.
  4. If you installed ShellCheck, run shellcheck examples/backup_notes.sh and read any suggestions; then deliberately remove a pair of quotes and watch it flag the bug.
  • 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}".
  • .gitignore and 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.sh reads 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 with mktemp -d and 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 to sudo a 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.