Scripts & Automation: Teach the Machine Your Routine
"Anything you have typed twice is something the machine should be doing for you."
You have been writing one-line programs since Text & Pipes without calling them that. A script is only the next step: those same commands, saved in a file, run whenever you like. This part turns you from someone who types commands into someone who keeps them — and teaches the exit-code logic that decides whether the next command runs at all. It is also the part that makes agent-written scripts stop being mysterious, because you will have written the same shapes yourself.
&& chain.6.1 Your First Script
.sh files in your projects. Both problems
have the same answer: a shell script is nothing more than commands saved in a file.That's the idea. Everything you've typed at the prompt this entire course could be pasted into a file and replayed. Let's build one — a tiny backup script — and hit every ingredient along the way.
#!/usr/bin/env bash
# Back up the notes folder with today's date in the name.
BACKUP_NAME="notes-backup-$(date +%F)"
mkdir -p backups
cp -r notes "backups/$BACKUP_NAME"
echo "Backed up notes to backups/$BACKUP_NAME"Three new things in eight lines:
The shebang: #!/usr/bin/env bash
The first line of every script tells the system which program should interpret the rest
of the file. #!/usr/bin/env bash means "run this with bash, wherever bash lives on this machine" — which is why it's preferred
over hard-coding a path like /bin/bash. There's no magic in it: env is itself a program — the one that printed your variables in Environment Variables — and handed a name, it looks that name up on PATH and runs it. Everything after that first line is what you'd type at the
prompt, and the # lines are notes to yourself, using the comment character from Introduction.
Variables
BACKUP_NAME="..." creates a variable (no spaces around the = — bash is strict about that), and $BACKUP_NAME uses it — the same dollar-sign expansion you met with environment
variables in Environment Variables. There's no export in front of it, so this one stays inside the script and no program
the script launches will ever see it.
Quoted paths
"backups/$BACKUP_NAME" — double quotes, by the rule in Paths: the variable still
expands, and any spaces stay put. Quoting variables is the habit that separates scripts
that work from scripts that work until a filename has a space in it.
Now make it runnable. Two steps, both from chmod: give the file
execute permission, then run it with the explicit ./ path. The .sh on the end is convention for humans and editors — the execute bit and that
first line are what actually run it.
chmod +x backup.sh
./backup.sh
# Backed up notes to backups/notes-backup-2026-07-12The ./ is there for the reason that section gave: a bare backup.sh sends the shell hunting through PATH, and this
folder deliberately isn't on it. Running it this way also starts a child shell, so the
script's variables and any cd inside it are gone the moment it exits — exactly the distinction Your Shell Config drew.
Arguments: $1 makes it reusable
Inside a script, $1 is the first argument the script was handed — the words after its name (Getting Help), numbered in the order they arrived, so $2 is the second. One change turns our notes-only script into a back-up-anything
script:
#!/usr/bin/env bash
# Usage: ./backup.sh <folder>
TARGET="$1"
BACKUP_NAME="$TARGET-backup-$(date +%F)"
mkdir -p backups
cp -r "$TARGET" "backups/$BACKUP_NAME"
echo "Backed up $TARGET to backups/$BACKUP_NAME"./backup.sh notes
# Backed up notes to backups/notes-backup-2026-07-12
./backup.sh recipes
# Backed up recipes to backups/recipes-backup-2026-07-12$(date +%F)? The $( ... ) around a command means "run this, and drop its output right here" — command
substitution. So $(date +%F) becomes today's date, and the backup name carries it. Agents lean
on this constantly, so it's worth recognizing on sight. One caveat for the sandbox just below:
this in-browser playground doesn't run $( ... ) yet, so build your backup.sh there with a plain fixed name (the audit habit is the point). On your
real machine, the dated version works exactly as shown.How agents deliver a file: the here-doc
Watch a coding agent create a file and you'll see one shape over and over — a whole file, delivered in a single command:
cat <<'EOF' > backup.sh
#!/usr/bin/env bash
mkdir -p backups
cp -r notes "backups/notes-$(date +%F)"
EOFRead the first line in three parts. cat you know. <<'EOF' is a here-document: "feed the lines that
follow straight in as input, until a line that says exactly EOF." And > backup.sh is the redirection from Text & Pipes, catching all of it in a file. The delimiter is any word you like
— EOF ("end of file") is convention, not a keyword. The quotes around it are the
single-quote rule from Paths in a new costume: quoted, every $ between the markers travels into the file literally — which is what you want when the script's own $(date +%F) should run later, not now. Unquoted, the shell expands them all before
the file is even written. One habit to take from this: the lines between the markers are the file — audit them exactly the way you'd audit the script they're about to become.
(The playground doesn't speak here-docs; this one is for reading agent transcripts, and for your
real machine.)
.sh file. Until today that file was a black box you ran on trust. Now it's a short text file you can cat, read line by line, and audit the way you would any command: name it,
read every flag, check what it touches, ask whether it's reversible. That routine gets its
own section in Read Before You Run — because a script is just commands, and you read
commands now.cp copies, rm deletes, and curl fetches something off the network to run on your machine.Try It: Automate the Backup
backup.sh right in the sandbox — write it line by line with echo >> (this playground has no full-screen editor), then chmod +x it and run it with ./backup.sh. Check your work with cat and ls backups.Loading playground...
Now make it reusable. A hard-coded path backs up one folder; $1 backs up whatever
you name. Same audit habit applies — read the script before you run it.
Try It: One Script, Any Folder
Loading playground...
"Write a bash script that backs up a folder I pass as $1, and explain every line before I run it"
"Here is a script an agent generated — walk me through what each line does and flag anything risky"
6.2 Exit Codes & Chaining
Every command, when it finishes, hands the shell a number called its exit code: 0 means success, and anything else (1
to 255) means some flavor of failure. Which number it picks is each command's own business
and means nothing outside that command, so there's no master table to go hunting for — only
zero-or-not-zero travels between commands. You never see it unless you ask — the special
variable $? holds the exit code of the last command:
ls notes
# recipes.md todo.md
echo $?
# 0 <- found it, success
ls no-such-folder
# ls: cannot access 'no-such-folder': No such file or directory
echo $?
# 2 <- non-zero: it failed, and it said soOn its own that's a curiosity. It earns its keep with the three chaining operators, which decide whether the next command runs based on the last one's exit code:
a && b — "and then" (only on success)
Run b only if a exited 0. The workhorse: "do this, and if it worked, do that."
a || b — "or else" (only on failure)
Run b only if a failed. The fallback: "try this, or else do that."
a ; b — "and regardless"
Run b no matter what happened to a. Just two commands on one line — no safety logic at all.
true && echo "ran" # ran (true exits 0)
false && echo "ran" # (nothing - && skips after failure)
true || echo "ran" # (nothing - || skips after success)
false || echo "ran" # ran
# The pattern you'll use every day:
npm test && npm run deploy
# tests pass -> deploy runs
# tests fail -> deploy never happens
# And the three-operator shape, read left to right:
npm test && npm run deploy || echo "something failed"true and false in the first four lines are real little programs
whose entire job is to exit 0 and 1 — which is what makes that truth table runnable rather than
illustrative. The last line has a catch. The chain runs left to right and neither operator outranks
the other, so the || is not paired off against the && — it fires whenever the command immediately before it failed. Tests
pass, deploy fails, and the message prints anyway. Useful here, but it is not the one-branch-or-the-other
shape it looks like. (Package Managers covers what npm run deploy is running.)
One number decides which branch runs — the same mechanism behind every CI pipeline (continuous integration: servers that build and test your code for you, automatically).
The classic horror story: ; where && belonged
Why does the choice of connector matter so much? Because of lines like this one, which has
genuinely destroyed home directories. The target sits in /tmp, the machine's
shared scratch space (Paths), so emptying a folder inside it is a
perfectly ordinary thing to want:
# THE TRAP - a semicolon runs the rm NO MATTER WHAT:
cd /tmp/build ; rm -rf *
# If /tmp/build doesn't exist, cd FAILS... and rm -rf * runs
# anyway - in whatever directory you were standing in. Maybe ~.
# THE SAFE VERSION - && only deletes if the cd succeeded:
cd /tmp/build && rm -rf *
# cd fails -> the chain stops -> nothing is deleted.; says "I don't care" — which is almost never true when the next command is destructive.
If you see cd <anywhere> ; rm, send it back and ask for &&.And here's why this little number matters beyond one-liners: the entire automated world runs on exit codes. CI pipelines decide pass-or-fail by the exit code of your test command — and GitHub,
which stores projects and runs those tests every time someone sends up new code, draws its
green checkmark to mean "everything exited 0." Coding agents watch exit codes the same way:
run a command, read $?, and decide whether to continue, retry, or fix. When your agent says "the
tests failed, let me look" — it didn't read your mind. It read an exit code. Scripts join
the same game: a script's own exit code is that of its last command, so scripts can chain
scripts, and the whole tower stands on one convention. Zero means go.
Try It: Deploy Only on Green
$?, &&, and || to build a one-liner that deploys only when the tests pass — then fix the failing
check and watch the same line take the other branch.Loading playground...
"Rewrite this chained command so the destructive step only runs if everything before it succeeded"
"Explain what this one-liner does if the first command fails — trace it connector by connector"
Loading challenge...