Generated Page
Goal
Write a shell script that builds ~/projects/first-site/index.html and MANIFEST.txt entirely from command output using >, >>, and |. Neither file gets typed by hand. ---
Type: code-notebook
Unit: 2 — No mouse
Goal
Write a shell script that builds ~/projects/first-site/index.html and MANIFEST.txt entirely from command output using >, >>, and |. Neither file gets typed by hand.
How this works
Two code cells, run differently.
Cell 2 is read-only. It rebuilds ~/projects/first-site/ so your script has somewhere to stand. Do not edit it.
Cell 4 is yours. It is saved as ~/build.sh and run as ./build.sh. Leave the first line #!/bin/bash alone.
Your tasks
Open starter/cell-4-build.sh.
- Run it once as given. You will see a title-only page and a manifest that claims
0pages. - TODO A. Append
<h1>Hello from the terminal</h1>toindex.htmlwithechoand>>. The wrong arrow wipes the title. - TODO B. Delete
echo "0" >> MANIFEST.txt. Replace it with a real count:ls -la | grep html | wc -l, appended to the manifest. - Run it again.
catat the bottom should show both lines of the page and a count that is not the character0.
What the scaffolding is for
The title line is already written with >. That is the only wipe. Everything you add must use >> or a pipe, or you throw the title away. The fake 0 is there so a script that “runs” is not the same as a script that counted.
Expected output
--- index.html ---
<title>First Site</title>
<h1>Hello from the terminal</h1>
--- MANIFEST.txt ---
MANIFEST for first-site
-rw-r--r-- … index.html
pages:
2
Your ls line will not match that one. The count will match how many html lines grep kept.
Rules
- Do not type the heading into an editor. The script has to print it.
- Do not edit cell 2 or
tests/. - Do not change the shebang unless you mean to change interpreters.
See rubric.md for how this is scored.
Steps
Your files
Download these onto your machine and fill them in. The brief says which, and what “done” looks like.
cell-1-intro.md Download
<!-- MARKDOWN CELL 1 -->
# Project 3 — Generated Page
Everything in `~/projects/first-site/index.html` so far got there because your fingers put it there.
This project takes your fingers off the file.
You are going to write a **script**: a plain text file holding the commands you would have typed, in
order, so the machine types them for you. When it finishes, two files exist that nobody wrote by
hand:
- `index.html` — a title line and a heading, both produced by `echo` and redirection.
- `MANIFEST.txt` — a header, the page listing, and a count of the pages, produced by a pipeline.
That is the whole idea, and it is the idea from Unit 1 arriving from the other side. A file is bytes.
A program can write bytes. Therefore a program can write a file — including this one.
cell-2-reference.sh Download
# ============================================================================
# CODE CELL 2 — READ ONLY, do not edit
# ============================================================================
# Carried forward from Project 2 (Cold start): rebuild the working tree from
# nothing, using only the terminal.
#
# This cell runs first, before your script, in the container's own shell.
# It is the reason `~/projects/first-site/` exists when your script arrives.
#
# `mkdir -p` is what makes it safe to run every single time:
# "-p, --parents" -> "no error if existing, make parent directories as
# needed" [src 19]
# Run it on a fresh machine and it builds both folders. Run it on a machine
# that already has them and it does nothing and says nothing.
# ============================================================================
mkdir -p ~/projects/first-site
cd ~/projects/first-site
ls -la
# ============================================================================
# END READ ONLY
# ============================================================================
cell-3-how-it-runs.md Download
<!-- MARKDOWN CELL 3 -->
## Before you edit the next cell: the first line
The next cell is not run the way the cell above it was. It is saved to a file called `build.sh`, made
executable, and then **run as a program** — `./build.sh`. Nobody tells the machine which shell to use.
The file has to say so itself, on its first line:
```
#!/bin/bash
```
That line names an absolute path to the program that will read the rest of the file. It matters more
than it looks, because on Ubuntu the two obvious answers are two different programs:
| You write | The machine runs |
| --- | --- |
| `#!/bin/bash` | bash — the same shell you have been typing into all unit |
| `#!/bin/sh` | whatever `/bin/sh` points at, and on Ubuntu that is a symlink to `dash` |
`sh` is not a nickname for bash here. It is a different program wearing a shorter name. Leave the
shebang alone unless you mean to change interpreters, and never delete it — a file with no first line
saying what it is has to be run by hand every time.
Two more things about the cell below before you touch it:
- **`>` starts a file. `>>` adds to it.** `>` empties whatever was there, with no warning and no
question — the same class of hazard as `rm`. Every file in this script is started exactly once with
`>` and extended after that with `>>`.
- **`|` is a connection, not a container.** A pipeline leaves nothing on disk. If you want the number
a pipeline produced to survive, the pipeline's last stage still has to be redirected into a file.
cell-4-build.sh Download
#!/bin/bash
# ============================================================================
# CODE CELL 4 — this is the one you edit.
#
# Saved as ~/build.sh, made executable, and run as ./build.sh
# Everything below is a command you already know from this unit.
# Two gaps are marked TODO A and TODO B. Nothing else needs to change.
# ============================================================================
cd ~/projects/first-site
# ---------------------------------------------------------------- 1. the page
# `>` creates index.html if it is missing and empties it if it is not.
# This is the only line in the whole script that is allowed to start the page,
# which is why it is the only line here using a single arrow.
echo "<title>First Site</title>" > index.html
# TODO A ---------------------------------------------------------------------
# Add the heading line to index.html. The heading is:
#
# <h1>Hello from the terminal</h1>
#
# Use `echo`, and use the arrow that ADDS to a file rather than replacing it.
# Pick the wrong arrow and the title line above disappears — check with `cat`.
# ----------------------------------------------------------------------------
# ------------------------------------------------------------ 2. the manifest
# Start the manifest, then append the listing of every page in this folder.
# `ls -la` prints one line per entry; `grep html` keeps only the lines that
# mention html; `>>` puts what survives on the end of MANIFEST.txt.
echo "MANIFEST for first-site" > MANIFEST.txt
ls -la | grep html >> MANIFEST.txt
echo "pages:" >> MANIFEST.txt
# TODO B ---------------------------------------------------------------------
# The line below is a lie. It appends the character 0 no matter how many pages
# this folder actually holds. Delete it and append a real COUNT instead.
#
# Build the count out of three stages you have already run by hand:
#
# ls -la -> grep html -> wc -l
#
# then send the last stage's output to the end of MANIFEST.txt. Remember that
# the pipe is a connection: on its own it puts nothing on disk.
echo "0" >> MANIFEST.txt
# ----------------------------------------------------------------------------
# --------------------------------------------------------- 3. show your work
echo "--- index.html ---"
cat index.html
echo "--- MANIFEST.txt ---"
cat MANIFEST.txt
On your machine, run the tests in this project’s tests/ folder. Each one
prints PASS or a FAIL: line that names what is wrong.
How it is graded
| Criterion | What earns it | Weight |
|---|---|---|
| The page is generated, not typed | TODO A is resolved with a single Full credit: two lines, correct order, one | 30 |
| The page count is computed, not asserted | TODO B is resolved with a pipeline — Full credit: the count tracks reality for any number of pages, including numbers the learner never saw.
Partial: the pipeline is present and correct but its output is not redirected, so the number prints to
the screen and | 30 |
| The script declares its own interpreter | Line 1 is a shebang naming an absolute path to a shell, and it is intact. This is worth grading because
the two obvious answers are not the same program: the shell the learner has been typing into all unit is
bash, Full credit: line 1 is | 15 |
| Craft — the script reads like a build | The script is something another person could pick up. Both Full credit: nothing in the file is stale, contradictory, or left over. Partial: correct and readable, but TODO markers or commented-out placeholder lines survive. No credit: the working lines have to be found among abandoned ones. | 25 |
| Total | 100 |
Test cases and grader source
| Test | Checks | Expected | Weight |
|---|---|---|---|
| canonical | Correctness on a clean machine: both files exist, index.html is two lines with a <title> and an <h1>, and the manifest's last line is 1. | PASS | 40 |
| awkward_count | Adversarial. The learner who typed the count instead of computing it. Seeds the folder with 12 extra pages so the true answer is 13 — a number nothing in the starter hints at, and not one anybody reaches by rounding — then asserts the manifest matches. | PASS | 30 |
| generated_not_typed | Adversarial. The learner who opened index.html in an editor, typed the two lines, and left build.sh a stub. Reads the script source with comments stripped and asserts a shebang on line 1, a > into index.html, a >> into index.html, a >> into MANIFEST.txt, a pipe, a call to wc, and no editor invocation. | PASS | 20 |
| rerun | The learner who concluded that >> is simply the safe arrow and used it everywhere. Runs the script twice and asserts index.html is still two lines and MANIFEST.txt has not grown. | PASS | 10 |
tests/test-1-canonical.sh
#!/bin/bash
# ============================================================================
# TEST 1 — canonical run weight: 40
#
# Does the script build both files correctly on a clean machine?
# Runs the READ ONLY reference cell, then the learner's build.sh, in a
# throwaway HOME, and checks the two artifacts.
#
# Prints exactly: PASS
# ============================================================================
BUILD_SH="${BUILD_SH:-$HOME/build.sh}"
if [ ! -f "$BUILD_SH" ]; then
echo "FAIL: no build.sh found at $BUILD_SH"
exit 1
fi
SANDBOX="$(mktemp -d)"
export HOME="$SANDBOX"
# --- the READ ONLY reference cell, verbatim ---
mkdir -p "$HOME/projects/first-site"
cp "$BUILD_SH" "$HOME/build.sh"
chmod +x "$HOME/build.sh"
cd "$HOME" || { echo "FAIL: could not enter the sandbox home"; exit 1; }
./build.sh > "$HOME/run.log" 2>&1
STATUS=$?
SITE="$HOME/projects/first-site"
if [ "$STATUS" -ne 0 ]; then
echo "FAIL: build.sh exited with status $STATUS"
exit 1
fi
if [ ! -s "$SITE/index.html" ]; then
echo "FAIL: index.html is missing or empty"
exit 1
fi
if ! grep -q "<title>" "$SITE/index.html"; then
echo "FAIL: index.html has no title element"
exit 1
fi
if ! grep -q "<h1>" "$SITE/index.html"; then
echo "FAIL: index.html has no h1 heading - TODO A is unresolved"
exit 1
fi
LINES=$(wc -l < "$SITE/index.html")
if [ "$LINES" -ne 2 ]; then
echo "FAIL: index.html has $LINES lines, expected 2"
exit 1
fi
if [ ! -s "$SITE/MANIFEST.txt" ]; then
echo "FAIL: MANIFEST.txt is missing or empty"
exit 1
fi
COUNT=$(tail -n 1 "$SITE/MANIFEST.txt" | tr -d '[:space:]')
case "$COUNT" in
''|*[!0-9]*)
echo "FAIL: the last line of MANIFEST.txt is not a number: '$COUNT'"
exit 1
;;
esac
if [ "$COUNT" -ne 1 ]; then
echo "FAIL: MANIFEST.txt reports $COUNT pages, but the folder holds 1"
exit 1
fi
echo "PASS"
tests/test-2-awkward-count.sh
#!/bin/bash
# ============================================================================
# TEST 2 — ADVERSARIAL: the count must be computed weight: 30
#
# Catches the learner who resolved TODO B by typing the answer instead of
# producing it - either leaving `echo "0"` in place, or replacing it with
# `echo "1"` after looking at their own folder once.
#
# The folder is seeded with a deliberately awkward number of pages before the
# script runs: 12 extra .html files plus the generated index.html = 13.
# Nothing about 13 can be guessed from the starter, and it is not a number
# anyone reaches by rounding.
#
# Prints exactly: PASS
# ============================================================================
BUILD_SH="${BUILD_SH:-$HOME/build.sh}"
if [ ! -f "$BUILD_SH" ]; then
echo "FAIL: no build.sh found at $BUILD_SH"
exit 1
fi
SANDBOX="$(mktemp -d)"
export HOME="$SANDBOX"
# --- the READ ONLY reference cell, verbatim ---
mkdir -p "$HOME/projects/first-site"
SITE="$HOME/projects/first-site"
# --- seed 12 extra pages; index.html will make 13 ---
for n in 01 02 03 04 05 06 07 08 09 10 11 12; do
echo "<title>page $n</title>" > "$SITE/page-$n.html"
done
cp "$BUILD_SH" "$HOME/build.sh"
chmod +x "$HOME/build.sh"
cd "$HOME" || { echo "FAIL: could not enter the sandbox home"; exit 1; }
./build.sh > "$HOME/run.log" 2>&1
STATUS=$?
if [ "$STATUS" -ne 0 ]; then
echo "FAIL: build.sh exited with status $STATUS"
exit 1
fi
if [ ! -s "$SITE/MANIFEST.txt" ]; then
echo "FAIL: MANIFEST.txt is missing or empty"
exit 1
fi
# what the folder really holds, computed the same way the script should
REAL=$(cd "$SITE" && ls -la | grep html | wc -l)
COUNT=$(tail -n 1 "$SITE/MANIFEST.txt" | tr -d '[:space:]')
case "$COUNT" in
''|*[!0-9]*)
echo "FAIL: the last line of MANIFEST.txt is not a number: '$COUNT'"
exit 1
;;
esac
if [ "$REAL" -ne 13 ]; then
echo "FAIL: test setup is wrong - the folder holds $REAL pages, expected 13"
exit 1
fi
if [ "$COUNT" -ne "$REAL" ]; then
echo "FAIL: MANIFEST.txt reports $COUNT pages, but the folder holds $REAL - the count was typed, not computed"
exit 1
fi
echo "PASS"
tests/test-3-generated-not-typed.sh
#!/bin/bash
# ============================================================================
# TEST 3 — ADVERSARIAL: the page was generated, not typed weight: 20
#
# Catches the learner who produced a correct index.html by opening it in nano
# or VS Code and typing the two lines, then left build.sh as a stub. The
# artifacts would look right; the script would not contain the operators that
# are the actual subject of this unit.
#
# Reads the script source with every comment line stripped, so a `>>` sitting
# inside a comment cannot pass the test on its own.
#
# Prints exactly: PASS
# ============================================================================
BUILD_SH="${BUILD_SH:-$HOME/build.sh}"
if [ ! -f "$BUILD_SH" ]; then
echo "FAIL: no build.sh found at $BUILD_SH"
exit 1
fi
FIRST=$(head -n 1 "$BUILD_SH")
case "$FIRST" in
'#!'/*)
;;
*)
echo "FAIL: first line is not a shebang naming an absolute path: '$FIRST'"
exit 1
;;
esac
CODE=$(grep -v '^[[:space:]]*#' "$BUILD_SH")
if ! printf '%s\n' "$CODE" | grep -q '>[[:space:]]*index\.html'; then
echo "FAIL: nothing in build.sh redirects into index.html"
exit 1
fi
if ! printf '%s\n' "$CODE" | grep -q '>>[[:space:]]*index\.html'; then
echo "FAIL: build.sh never appends to index.html with >>"
exit 1
fi
if ! printf '%s\n' "$CODE" | grep -q '>>[[:space:]]*MANIFEST\.txt'; then
echo "FAIL: build.sh never appends to MANIFEST.txt with >>"
exit 1
fi
if ! printf '%s\n' "$CODE" | grep -q '|'; then
echo "FAIL: build.sh contains no pipe"
exit 1
fi
if ! printf '%s\n' "$CODE" | grep -q 'wc'; then
echo "FAIL: build.sh never calls wc"
exit 1
fi
if printf '%s\n' "$CODE" | grep -qE '(^|[[:space:]])(nano|vim|vi|code)([[:space:]]|$)'; then
echo "FAIL: build.sh launches an editor - the page must be built from command output"
exit 1
fi
echo "PASS"
tests/test-4-rerun.sh
#!/bin/bash
# ============================================================================
# TEST 4 — running it twice changes nothing weight: 10
#
# Catches the learner who decided that >> is simply the safe arrow and used
# it everywhere, including on the line that starts each file. That script is
# correct exactly once: run it a second time and index.html has four lines
# and MANIFEST.txt has two of everything.
#
# Prints exactly: PASS
# ============================================================================
BUILD_SH="${BUILD_SH:-$HOME/build.sh}"
if [ ! -f "$BUILD_SH" ]; then
echo "FAIL: no build.sh found at $BUILD_SH"
exit 1
fi
SANDBOX="$(mktemp -d)"
export HOME="$SANDBOX"
# --- the READ ONLY reference cell, verbatim ---
mkdir -p "$HOME/projects/first-site"
SITE="$HOME/projects/first-site"
cp "$BUILD_SH" "$HOME/build.sh"
chmod +x "$HOME/build.sh"
cd "$HOME" || { echo "FAIL: could not enter the sandbox home"; exit 1; }
./build.sh > "$HOME/run1.log" 2>&1
if [ ! -f "$SITE/index.html" ] || [ ! -f "$SITE/MANIFEST.txt" ]; then
echo "FAIL: the first run did not produce both index.html and MANIFEST.txt"
exit 1
fi
PAGE_1=$(wc -l < "$SITE/index.html")
MAN_1=$(wc -l < "$SITE/MANIFEST.txt")
./build.sh > "$HOME/run2.log" 2>&1
STATUS=$?
PAGE_2=$(wc -l < "$SITE/index.html")
MAN_2=$(wc -l < "$SITE/MANIFEST.txt")
if [ "$STATUS" -ne 0 ]; then
echo "FAIL: the second run of build.sh exited with status $STATUS"
exit 1
fi
if [ "$PAGE_1" -ne 2 ] || [ "$PAGE_2" -ne 2 ]; then
echo "FAIL: index.html had $PAGE_1 lines after one run and $PAGE_2 after two, expected 2 and 2"
exit 1
fi
if [ "$MAN_1" -ne "$MAN_2" ]; then
echo "FAIL: MANIFEST.txt grew from $MAN_1 lines to $MAN_2 - a file is being started with >> instead of >"
exit 1
fi
COUNT=$(tail -n 1 "$SITE/MANIFEST.txt" | tr -d '[:space:]')
if [ "$COUNT" != "1" ]; then
echo "FAIL: after two runs MANIFEST.txt reports '$COUNT' pages, expected 1"
exit 1
fi
echo "PASS"