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.

  1. Run it once as given. You will see a title-only page and a manifest that claims 0 pages.
  2. TODO A. Append <h1>Hello from the terminal</h1> to index.html with echo and >>. The wrong arrow wipes the title.
  3. TODO B. Delete echo "0" >> MANIFEST.txt. Replace it with a real count: ls -la | grep html | wc -l, appended to the manifest.
  4. Run it again. cat at the bottom should show both lines of the page and a count that is not the character 0.

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

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
CriterionWhat earns itWeight
The page is generated, not typed

TODO A is resolved with a single echo whose output is appended to index.html using >>. The finished index.html is exactly two lines: the <title> line written earlier with >, still present and still first, followed by the <h1> heading. The > on the title line has not been changed to >>, so a second run of the script produces the same two-line file rather than a four-line one. No editor is invoked anywhere in the script, and the HTML is not created by any means other than command output.

Full credit: two lines, correct order, one > and one >>, and re-running the script changes nothing. Partial: correct output but both lines appended with >>, so the file grows on every run. No credit: index.html has one line (TODO A unresolved), or the heading was typed into the file by hand and the script does not produce it.

30
The page count is computed, not asserted

TODO B is resolved with a pipeline — ls -la into grep into wc -l — whose output is appended to MANIFEST.txt with >>. No literal number survives anywhere in the script as a stand-in for the count. The distinguishing evidence is behavioural: change how many .html files the folder holds, run the script again without touching it, and the last line of MANIFEST.txt changes to match. A script that is correct only for the folder the learner happened to have is not correct.

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 MANIFEST.txt ends without it — the connection/container confusion, caught but understood. No credit: the last line of MANIFEST.txt comes from an echo of a fixed number.

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, echo $SHELL/usr/bin/bash [src 28], while /bin/sh on Ubuntu is a symlink, /bin/sh -> dash [src 29]. A file that is run directly, as ./build.sh is, has no other way to say which of those should read it.

Full credit: line 1 is #!/bin/bash (or another explicit absolute path to a shell the script's contents suit), unmodified or deliberately and consistently changed. No credit: the shebang is deleted, indented off line 1, or reduced to a bare word such as bash with no #! and no path.

15
Craft — the script reads like a build

The script is something another person could pick up. Both TODO A and TODO B comment blocks are gone, resolved rather than left sitting above the new code. The placeholder echo "0" >> MANIFEST.txt is deleted, not commented out and abandoned below the real line. The three section comments still describe what the sections now do rather than what they used to. No dead commands, no duplicated work, no second copy of a line left behind from an experiment. The two files are each started exactly once with > and extended with >> after that, so the intent of every arrow is legible at a glance.

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
Total100
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"

                      

Erase saved progress?

This erases all quiz scores, reading progress, project checklists, and your name on the certificate. It cannot be undone, and it affects only this course in this browser.