Computing FoundationsSystems Foundations: Storage, Observability, and Tooling › Day 38

Hands-on lab — Day 38: Regular Expressions

Commands

Setup

cd labs/sections/computing-foundations/day-038-regular-expressions

Run

bash examples/regex_drills.sh
bash starter/regex_drills.sh

Test

bash tests/run_tests.sh

File tree

examples/regex_drills.sh
examples/samples/data.txt
expected-output/drills-output.txt
expected-output/FIELDS.md
expected-output/test-output.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/regex_drills.sh
starter/regex-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 038 lab — Match Patterns with grep and sed

Lesson

  • Lesson title: Regular Expressions
  • Day number: 38 of 365
  • Lesson article: https://ai-roadmap-365.github.io/day-038-regular-expressions
  • 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-038-regular-expressions when the site is running.

Purpose

Day 38's lesson builds regex syntax from the ground up. This lab makes it real: you run grep -E and sed -E against a small, clearly synthetic sample file that mixes emails, dates, phone-ish numbers, and access-log lines with IP addresses and status codes. You extract, count, and reformat text — and confirm your patterns are right by matching known counts.

Learning objectives

  • Extract matches from text with grep -E -o.
  • Count matching lines with grep -E -c (and invert with -v).
  • Write patterns for emails, dates, IP addresses, and status codes, and reason about what they do and do not match.
  • Capture fields and reformat them with sed -E and backreferences (\1, \2).
  • Run an automated test script and read its pass/fail output.

Prerequisites

  • The Day 38 lesson (read it first — it teaches every metacharacter used here).
  • Comfort running basic shell commands (Days 8–14).
  • A terminal with grep and sed (preinstalled on macOS and Linux).

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon, BSD grep/sed).
  • Linux — fully supported (GNU grep/sed). Output is byte-for-byte identical.
  • Windows — use WSL or Git Bash and follow the Linux path.

Hardware requirements

Any computer made in roughly the last 15 years. The lab only reads a small text file and prints to the terminal; it needs no particular RAM, disk, or GPU.

Required software

  • bash (3.2 or newer — preinstalled on macOS and Linux).
  • grep and sed, both supporting the -E extended-regex flag. Preinstalled on macOS and Linux.

Free and open-source options

Everything in this lab is free and ships with your OS. No account, API key, or purchase is needed; nothing reaches the network. See the lesson for free regex testers (such as regex101) you can use in a browser to build patterns interactively.

Installation

None. Copy this directory (or clone the repository) and you are ready:

cd labs/sections/computing-foundations/day-038-regular-expressions

File structure

day-038-regular-expressions/
├── README.md                       ← you are here
├── metadata.yml                    ← machine-readable lab metadata
├── starter/
│   ├── regex_drills.sh             ← YOUR working file (4 exercises)
│   └── regex-worksheet.md          ← worksheet for the practice assignment
├── examples/
│   ├── regex_drills.sh             ← completed reference drills
│   └── samples/
│       └── data.txt                ← the synthetic sample you match against
├── tests/
│   └── run_tests.sh                ← automated checks against known counts
├── expected-output/
│   ├── drills-output.txt           ← real captured run of the example drills
│   ├── test-output.txt             ← real captured run of the tests
│   └── FIELDS.md                   ← the exact expected counts, all platforms
├── requirements/
│   └── README.md                   ← dependency statement (none beyond the OS)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished drills first
bash examples/regex_drills.sh

## 2. Your task: fill in the 4 exercises in the starter, then run it
bash starter/regex_drills.sh

## 3. Check your work against the known counts
bash tests/run_tests.sh

What the commands do

  • bash examples/regex_drills.sh — runs four worked drills against examples/samples/data.txt: extract emails (grep -E -o), count dated lines (grep -E -c), extract IP-ish addresses (grep -E -o), and reformat a date with sed -E backreferences. Each pattern is explained line by line in the script's comments.
  • bash starter/regex_drills.sh — the same idea with four numbered exercises to complete: match phone-ish numbers, extract status codes from the log lines, count non-comment lines, and reorder a day-first date with capture groups. Each has a placeholder (WRITE_YOUR_PATTERN_HERE) and a hint.
  • bash tests/run_tests.sh — runs the reference patterns against the sample and checks the counts (6 emails, 9 dated lines, 5 IPs, 5 status codes, 16 non-comment lines) and the sed reformat, then confirms the example script exits cleanly.

Expected output

See expected-output/drills-output.txt — a real captured run. The key numbers (exact, because the sample is fixed):

emails extracted ............ 6
lines with a YYYY-MM-DD date . 9
IP-ish addresses ............ 5
status codes (log lines) .... 5   (200, 401, 200, 404, 500)
non-comment lines ........... 16
sed reformat 2026/07/12 ..... 2026-07-12

expected-output/FIELDS.md lists the same counts and confirms they are identical on macOS and Linux.

Validation steps

  1. Run bash examples/regex_drills.sh — it must print six emails, the number 9, five IP addresses, and 2026-07-12.
  2. Complete the four exercises in starter/regex_drills.sh (replace every WRITE_YOUR_PATTERN_HERE) and run it — Exercise 1 should print 5 phone numbers, Exercise 2 the five status codes, Exercise 3 the count 16, and Exercise 4 2026-07-12.
  3. Run the tests (next section) — all checks must pass.

Tests

bash tests/run_tests.sh

Expected final line: 8 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure, so it can run in CI. It uses only the committed sample and makes no network calls.

Cleanup

Nothing to clean up: the scripts only read the sample and print to the terminal. To reset your work, restore the starter files from git: git checkout -- starter/regex_drills.sh starter/regex-worksheet.md.

Troubleshooting

See troubleshooting.md for the full list: missing -E, BSD-vs-GNU sed differences on macOS/Linux, \d not matching, and patterns that match too much.

Security notes

See security.md. Short version: the scripts read a synthetic local file, make no network calls, need no elevated privileges, and never execute matched text — a habit you should keep (never eval what a regex extracts).

Extension exercises

  1. Replace a broad .* with a narrow class. On a line like <a href="one"> and <b href="two">, compare grep -E -o '<.*>' (one greedy match) with grep -E -o '<[^>]*>' (two tag matches) and explain the difference in terms of backtracking.
  2. Write a single pattern that matches only valid-looking status codes in the 200–599 range ([2-5][0-9][0-9]) and confirm it still finds all five.
  3. Add a fifth drill that redacts every email address in the sample, replacing the local part with *** using sed -E and a capture group for the domain.
  • Previous day: Day 37 — Debuggers, Linters, and Formatters (labs/sections/computing-foundations/day-037-debuggers-linters-and-formatters/).
  • Next day: Day 39 — Data Storage: Files, Databases, Object Storage, and Caches (labs/sections/computing-foundations/day-039-data-storage-files-databases-object-storage/).

Expected output

FIELDS.md

# Expected results (all platforms)

The committed sample `examples/regex_drills.sh` runs against
`examples/samples/data.txt`, which is fixed, so these counts are exact and the
same on macOS and Linux:

| Drill / pattern | Result |
| --- | --- |
| Email addresses extracted | **6** |
| Lines containing a `YYYY-MM-DD` date | **9** |
| IP-ish addresses extracted | **5** |
| Status codes pulled from log lines | **5** (200, 401, 200, 404, 500) |
| Non-comment lines (do not start with `#`) | **16** |
| `sed` reformat of `2026/07/12` | **`2026-07-12`** |

Files in this directory are real captures from the authoring machine
(macOS, BSD grep/sed, 2026-07-12):

- `drills-output.txt` — the full output of `bash examples/regex_drills.sh`.
- `test-output.txt` — the full output of `bash tests/run_tests.sh`, ending in
  `8 checks, 0 failure(s).`

## Platform note (macOS vs Linux)

The patterns in this lab deliberately use POSIX character classes (`[0-9]`,
`[[:alnum:]]`, `[[:alpha:]]`) and explicit `{n}` counts, and both `grep -E` and
`sed -E`. These behave identically under BSD tools (macOS) and GNU tools
(Linux), so the output above is byte-for-byte the same on both. The only
differences you would hit are with GNU-only shorthands like `\d` or `\+`, which
this lab avoids on purpose — see `troubleshooting.md`.

drills-output.txt

=== Drill 1: extract every email address ===
ada.lovelace@example.com
grace.hopper@example.org
alan.turing@example.net
katherine.johnson@example.com
edsger.dijkstra@example.org
mission.control@example.net

=== Drill 2: count lines that contain a YYYY-MM-DD date ===
9

=== Drill 3: extract every IP-ish address ===
192.168.1.44
10.0.0.7
172.16.5.9
192.168.1.44
10.0.0.7

=== Drill 4: reformat a date field with capture groups (sed backreferences) ===
2026-07-12

=== Done. Compare these results with the counts in the README. ===

test-output.txt

Checking the committed sample exists ...
  ok: <repo>/labs/sections/computing-foundations/day-038-regular-expressions/examples/samples/data.txt
Checking known counts from the sample ...
  ok: email addresses extracted (6)
  ok: lines with a YYYY-MM-DD date (9)
  ok: IP-ish addresses extracted (5)
  ok: status codes from log lines (5)
  ok: non-comment lines (16)
  ok: sed backreference reformat (2026-07-12)
Checking the example drills script runs clean ...
  ok: examples/regex_drills.sh exits 0 (yes)
  ok: example output shows reformatted date (yes)

8 checks, 0 failure(s).

Source files

examples/regex_drills.sh (2948 bytes)
#!/usr/bin/env bash
# Day 038 lab — worked regex drills with grep -E and sed -E.
#
# This is the completed REFERENCE script. It runs entirely offline against the
# committed sample file examples/samples/data.txt and demonstrates the four
# everyday regex jobs: extract, count, extract-with-groups, and reformat.
# Read each pattern's explanation, run the script, then rebuild it yourself in
# starter/regex_drills.sh.
#
# We use grep -E (extended regex) and sed -E throughout, and POSIX character
# classes like [0-9] and [[:alnum:]] rather than the PCRE shorthands \d and \w,
# because the POSIX forms behave the same in BSD grep/sed (macOS) and GNU
# grep/sed (Linux). See troubleshooting.md for the dialect differences.
set -euo pipefail

# Resolve the sample file relative to this script, so the drills run from any
# working directory.
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
sample="${script_dir}/samples/data.txt"

echo "=== Drill 1: extract every email address ==="
# Pattern, piece by piece:
#   [[:alnum:]._%+-]+   one or more letters/digits/dot/underscore/percent/plus/hyphen  (the local part)
#   @                   a literal @ sign
#   [[:alnum:].-]+      one or more letters/digits/dot/hyphen                           (the domain name)
#   \.                  a LITERAL dot (escaped — an unescaped . matches any character)
#   [[:alpha:]]{2,}     two or more letters                                            (the top-level domain)
# -o prints only the matched text, not the whole line.
grep -E -o '[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}' "${sample}"
echo

echo "=== Drill 2: count lines that contain a YYYY-MM-DD date ==="
# [0-9]{4}-[0-9]{2}-[0-9]{2}  four digits, hyphen, two digits, hyphen, two digits.
# -c counts MATCHING LINES rather than printing them. Note: this checks the
# SHAPE of a date, not whether the month and day are real calendar values.
grep -E -c '[0-9]{4}-[0-9]{2}-[0-9]{2}' "${sample}"
echo

echo "=== Drill 3: extract every IP-ish address ==="
# ([0-9]{1,3}\.){3}   a group of "1-3 digits then a literal dot", repeated 3 times
#   [0-9]{1,3}        a final 1-3 digit number
# This matches the SHAPE of an IPv4 address. It also matches impossible octets
# like 999.999.999.999 — good enough to extract from logs you trust, not to
# validate untrusted input.
grep -E -o '([0-9]{1,3}\.){3}[0-9]{1,3}' "${sample}"
echo

echo "=== Drill 4: reformat a date field with capture groups (sed backreferences) ==="
# Turn YYYY/MM/DD into YYYY-MM-DD.
#   ([0-9]{4})/([0-9]{2})/([0-9]{2})   capture year, month, day into groups 1, 2, 3
#   \1-\2-\3                           rebuild them joined by hyphens
# We use '#' as the sed delimiter instead of the usual '/' so the slashes in the
# date do not clash with sed's own syntax.
echo '2026/07/12' | sed -E 's#([0-9]{4})/([0-9]{2})/([0-9]{2})#\1-\2-\3#'
echo

echo "=== Done. Compare these results with the counts in the README. ==="
examples/samples/data.txt (1096 bytes)
# Synthetic sample data for the Day 38 regex lab.
# Every name, address, phone number, and IP below is fictional and for practice only.
# Lines beginning with '#' are comments.
#
# --- Section 1: contacts ---
Ada Lovelace <ada.lovelace@example.com> joined 2026-01-15
Grace Hopper <grace.hopper@example.org> joined 2026-02-03
Alan Turing <alan.turing@example.net> joined 2026-03-21
Katherine Johnson, katherine.johnson@example.com, phone 555-0142
Edsger Dijkstra, edsger.dijkstra@example.org, phone (555) 867-5309
#
# --- Section 2: phone list (synthetic) ---
Support line: 555-0100
Fax (old): 555-0199
Mobile: 555-234-5678
#
# --- Section 3: access log (synthetic IPs and status codes) ---
192.168.1.44 - - [2026-07-12] "GET /health" 200
10.0.0.7 - - [2026-07-12] "POST /login" 401
172.16.5.9 - - [2026-07-12] "GET /report" 200
192.168.1.44 - - [2026-07-13] "GET /data.csv" 404
10.0.0.7 - - [2026-07-13] "GET /health" 500
#
# --- Section 4: mixed notes ---
Renewal due 2026-11-30 for account #4821 (pending)
No date on this line, just some plain text.
Contact backup: mission.control@example.net
metadata.yml (602 bytes)
lesson_id: D038
day: 38
kind: conversion-exercises
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-038-regular-expressions
run_commands:
  - bash examples/regex_drills.sh
  - bash starter/regex_drills.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/regex_drills.sh starter/regex-worksheet.md  # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon, BSD grep/sed), bash tests/run_tests.sh → 8 checks, 0 failure(s).'
requirements/README.md (871 bytes)
# Dependencies — Day 038 lab

**None beyond a POSIX shell with `grep` and `sed`.** This lab has zero
installable dependencies:

- `bash` ≥ 3.2 (preinstalled on macOS and every mainstream Linux distribution)
- `grep` with the `-E` (extended regex) option — preinstalled everywhere
- `sed` with the `-E` (extended regex) option — preinstalled on macOS (BSD sed)
  and Linux (GNU sed)

There is deliberately no `requirements.txt`/`package.json`: every tool used
ships with the operating system, nothing reaches the network, and no account or
API key is required. The lab only *reads* the committed sample file and prints
to your terminal.

If `grep -E` or `sed -E` is somehow missing (extremely minimal containers),
install `grep` and `sed` from your package manager (`apt install grep sed` on
Debian/Ubuntu). See `../troubleshooting.md` for BSD-vs-GNU differences.
starter/regex_drills.sh (2768 bytes)
#!/usr/bin/env bash
# Day 038 lab — YOUR regex drills.
#
# Four numbered exercises. Each has a pattern placeholder set to
# WRITE_YOUR_PATTERN_HERE. Replace each placeholder with a working extended
# regular expression, then run:  bash starter/regex_drills.sh
#
# Use grep -E and sed -E, and prefer POSIX classes ([0-9], [[:alpha:]]) over the
# PCRE shorthands \d and \w so your patterns work in both BSD (macOS) and GNU
# (Linux) tools. The completed reference is in examples/regex_drills.sh — try it
# yourself before peeking. Expected results are in the README's "Expected output".
set -uo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
sample="${script_dir}/../examples/samples/data.txt"

echo "=== Exercise 1: extract every phone-ish number ==="
# Phone numbers in the sample look like 555-0142, 555-0100, 867-5309, 234-5678.
# Write a pattern for "three digits, a hyphen, four digits" and pass it to
# grep -E -o. Expected: 5 matches.
EX1_PATTERN="WRITE_YOUR_PATTERN_HERE"   # hint: [0-9]{3}-[0-9]{4}
grep -E -o "${EX1_PATTERN}" "${sample}" || echo "(no matches yet — fill in EX1_PATTERN)"
echo

echo "=== Exercise 2: extract just the status codes from the log lines ==="
# The access-log lines end with a quoted request then a 3-digit status code,
# e.g.  "GET /health" 200 . Beware: several phone lines also end in digits, so
# a naive [0-9]{3}$ would match those too. First keep only log lines (they
# contain a quote followed by a space and three digits at end of line), then
# extract the trailing 3-digit code. Expected: 200 401 200 404 500.
EX2_SELECT="WRITE_YOUR_PATTERN_HERE"    # hint: " [0-9]{3}$   (selects log lines)
EX2_EXTRACT="WRITE_YOUR_PATTERN_HERE"   # hint: [0-9]{3}$     (pulls the code)
grep -E "${EX2_SELECT}" "${sample}" | grep -E -o "${EX2_EXTRACT}" || echo "(fill in EX2 patterns)"
echo

echo "=== Exercise 3: count how many lines are NOT comments ==="
# Comment lines begin with '#'. Use grep with -c (count) and -v (invert match)
# and an anchor to count every line that does NOT start with '#'. Expected: 16.
EX3_PATTERN="WRITE_YOUR_PATTERN_HERE"   # hint: ^#
grep -E -c -v "${EX3_PATTERN}" "${sample}" || echo "(fill in EX3_PATTERN)"
echo

echo "=== Exercise 4: reformat a day-first date with capture groups ==="
# Turn a day-first date like 12-07-2026 into ISO order 2026-07-12 by capturing
# the three fields and rebuilding them in a new order with backreferences.
# Fill in EX4_FIND (three capture groups) and EX4_REPLACE (\3-\2-\1).
EX4_FIND="WRITE_YOUR_PATTERN_HERE"      # hint: ([0-9]{2})-([0-9]{2})-([0-9]{4})
EX4_REPLACE="WRITE_YOUR_PATTERN_HERE"   # hint: \3-\2-\1
echo '12-07-2026' | sed -E "s#${EX4_FIND}#${EX4_REPLACE}#"
echo

echo "=== Done. Compare with the README's Expected output. ==="
starter/regex-worksheet.md (1870 bytes)
# Regex worksheet — Day 038

Fill this in as you work through the lab. Write your patterns in the code spans
and record the counts you get. The sample file is
`../examples/samples/data.txt` (relative to this `starter/` directory).

## 1. Write the pattern for an email address

Write an extended-regex pattern that extracts every email address from the
sample with `grep -E -o`.

- My pattern: `______________________________________________`
- Command I ran: `grep -E -o '______' ../examples/samples/data.txt`
- Number of addresses it found: `____`

> One-sentence note on what your pattern does NOT guarantee (hint: think about
> what "a valid email address" really requires):
>
> _______________________________________________________________

## 2. Write the pattern for a YYYY-MM-DD date

Write a pattern that matches an ISO date such as `2026-07-12`.

- My pattern: `______________________________________________`
- One thing about dates this pattern does NOT verify: `__________________`

## 3. Count how many lines match a pattern of your choice

Pick any pattern (an IP address, a phone number, lines containing `@`, lines
that are comments, …) and use `grep -E -c` to count the matching lines.

- Pattern I chose: `______________________________________________`
- What it is meant to match (in words): `__________________________`
- Command: `grep -E -c '______' ../examples/samples/data.txt`
- Count returned: `____`

## 4. Reflection (2–3 sentences)

Which was harder to get right: describing the shape you wanted, or avoiding
matches you did not want? Give one example from this lab where a pattern was
"too loose" (matched something unintended) or "too tight" (missed something)
and how you fixed it.

_______________________________________________________________________

_______________________________________________________________________
tests/run_tests.sh (2737 bytes)
#!/usr/bin/env bash
# Tests for the Day 038 regex lab. Run from anywhere:
#   bash tests/run_tests.sh
#
# Verifies that the reference drills extract the KNOWN correct counts from the
# committed synthetic sample, and that the example script runs clean. No
# network, no writes outside stdout. Exits 0 on success, non-zero on any
# failure, so it is safe for CI.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
sample="${lab_dir}/examples/samples/data.txt"
example="${lab_dir}/examples/regex_drills.sh"
failures=0
checks=0

check() {
  local label="$1" expected="$2" actual="$3"
  checks=$((checks + 1))
  if [ "${expected}" = "${actual}" ]; then
    echo "  ok: ${label} (${actual})"
  else
    echo "  FAIL: ${label} — expected '${expected}', got '${actual}'"
    failures=$((failures + 1))
  fi
}

echo "Checking the committed sample exists ..."
if [ ! -f "${sample}" ]; then
  echo "  FAIL: sample file not found at ${sample}"
  echo
  echo "1 checks, 1 failure(s)."
  exit 1
fi
echo "  ok: ${sample}"

echo "Checking known counts from the sample ..."

# Drill 1: email addresses (expected 6)
emails="$(grep -E -o '[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}' "${sample}" | grep -c .)"
check "email addresses extracted" "6" "${emails}"

# Drill 2: lines containing a YYYY-MM-DD date (expected 9)
dated="$(grep -E -c '[0-9]{4}-[0-9]{2}-[0-9]{2}' "${sample}")"
check "lines with a YYYY-MM-DD date" "9" "${dated}"

# Drill 3: IP-ish addresses (expected 5)
ips="$(grep -E -o '([0-9]{1,3}\.){3}[0-9]{1,3}' "${sample}" | grep -c .)"
check "IP-ish addresses extracted" "5" "${ips}"

# Status codes from log lines (expected 5): select log lines, then pull the code
codes="$(grep -E '" [0-9]{3}$' "${sample}" | grep -E -o '[0-9]{3}$' | grep -c .)"
check "status codes from log lines" "5" "${codes}"

# Non-comment lines (expected 16)
noncomment="$(grep -E -c -v '^#' "${sample}")"
check "non-comment lines" "16" "${noncomment}"

# Drill 4: sed reformat of a slash date to a hyphen date
reformatted="$(echo '2026/07/12' | sed -E 's#([0-9]{4})/([0-9]{2})/([0-9]{2})#\1-\2-\3#')"
check "sed backreference reformat" "2026-07-12" "${reformatted}"

echo "Checking the example drills script runs clean ..."
if out="$(bash "${example}" 2>&1)"; then
  check "examples/regex_drills.sh exits 0" "yes" "yes"
else
  check "examples/regex_drills.sh exits 0" "yes" "no"
  echo "${out}" | sed 's/^/    /'
fi
# The example output must contain the reformatted date it demonstrates.
if echo "${out}" | grep -q '2026-07-12'; then
  check "example output shows reformatted date" "yes" "yes"
else
  check "example output shows reformatted date" "yes" "no"
fi

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 038 lab

grep: invalid option or my metacharacters are treated literally

You almost certainly omitted -E. Basic grep (BRE) requires a backslash before +, ?, {, (, and | to make them special; grep -E (extended, ERE) treats them as special directly. Every pattern in this lab assumes -E.

sed errors on macOS but the same command works on Linux (BSD vs GNU)

macOS ships BSD sed; most Linux distributions ship GNU sed. They differ:

  • -E for extended regex: both accept -E. (GNU also accepts -r; BSD does not. Use -E for portability — this lab does.)
  • In-place editing: GNU is sed -i 's/…/…/' file; BSD requires an argument, sed -i '' 's/…/…/' file. This lab never edits files in place, so you will not hit this, but it is the most common cross-platform sed surprise.
  • Shorthands: GNU sed understands \+, \?, and (in some builds) \d; BSD sed does not. This lab uses only [0-9], [[:alpha:]], and explicit {n} counts, which behave the same on both.

If a sed command from elsewhere fails on macOS, first rewrite any \d as [0-9] and any \+ as + under -E.

\d matches nothing

Many command-line tools do not support the PCRE shorthand \d. Use [0-9] or the POSIX class [[:digit:]]. The \d, \w, \s shorthands live in Python and JavaScript, not in every grep/sed.

The IP or status-code pattern matches something unexpected

This is the "too loose" failure mode and it is the whole point of the lab. For example, [0-9]{3}$ matches the last three digits of a phone number as well as a status code — that is why Exercise 2 first selects log lines with " [0-9]{3}$ before extracting. Tighten the pattern or add context (a neighbouring literal, an anchor) until it matches only what you mean.

Permission denied when running a script

Run it through bash explicitly: bash starter/regex_drills.sh. You do not need to chmod +x anything. If you prefer ./starter/regex_drills.sh, first run chmod +x starter/regex_drills.sh.

The sed date reformat prints the input unchanged

Your pattern did not match, so sed left the line alone. Check that the field counts match the input exactly ({4} vs {2}) and that you used # as the delimiter so the slashes in the date do not clash with sed's syntax: sed -E 's#([0-9]{4})/([0-9]{2})/([0-9]{2})#\1-\2-\3#'.

Windows

Use WSL (wsl --install, then open Ubuntu) and follow the Linux path, or use Git Bash. The GNU tools inside WSL behave exactly as described for Linux.

Security notes

Security notes — Day 038 lab

  • What the scripts do: read one committed, synthetic sample file (examples/samples/data.txt) and print matches and counts to your terminal. They make no network connections, write no files (except the tests you choose to redirect), and change no settings.
  • The sample is fictional. Every name, email, phone number, and IP address in the sample is invented for practice (example.com/.org/.net are reserved documentation domains; the IPs are private-range). Nothing in this lab touches real personal data.
  • Never eval matched text. A recurring real-world danger with regex is taking text a pattern extracted and executing it as a command (eval "$match") or interpolating it into a shell command. Matched text is untrusted input; treat it as data, never as code. This lab only prints matches — it never runs them — and you should keep that habit.
  • Watch for catastrophic backtracking (ReDoS). If you later run patterns over untrusted input, avoid nested quantifiers over overlapping classes (the classic (a+)+$ shape), prefer specific classes like [^"]* to broad .*, and use tools or engines with a timeout. A single crafted input can otherwise freeze a process.
  • Privileges: everything runs as your normal user. Nothing here needs sudo; if any tutorial asks you to sudo a script you have not read, stop and read it first.