TerminalVibes / Scripts & Automation
Part 6

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.

Note
This is the last piece of ordinary bash you need. Everything after it is a specific toolkit — text surgery, processes, the network — and every one of those parts assumes you can read a small script and follow a && chain.

6.1 Your First Script

A script is just commands you saved — teach the file once, run it forever
Note
The chore: You keep typing the same three commands to back up your notes. And your AI agents keep leaving mysterious .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.

backup.sh
#!/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.

Make it executable, then run it
chmod +x backup.sh
./backup.sh
# Backed up notes to backups/notes-backup-2026-07-12

The ./ 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:

backup.sh, take two — back up anything
#!/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"
Same script, any folder
./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
Note
What's $(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:

A file in one command
cat <<'EOF' > backup.sh
#!/usr/bin/env bash
mkdir -p backups
cp -r notes "backups/notes-$(date +%F)"
EOF

Read 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.)

Important
Agents write scripts constantly. Ask an AI to "set up the project" or "automate the deploy" and odds are it produces a .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.
Tip
A script has a shape, and it reads in that order. The shebang tells you the language, the variables tell you the moving parts, and the verbs tell you the risk — cp copies, rm deletes, and curl fetches something off the network to run on your machine.

Try It: Automate the Backup

Build 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.
Automate the Backup

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

One Script, Any Folder

Loading playground...

Vibe it

"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 files a report when it finishes — 0 means success, anything else means trouble
Note
The goal: You want "run the tests, and deploy only if they pass" — but you've been eyeballing the output and deciding by hand. The shell has a built-in way for commands to report success or failure, and a grammar for acting on it.

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:

Asking how the last command went
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 so

On 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.

The truth table, live
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:

Two characters between routine and disaster
# 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.
Caution
Audit the connectors, not just the commands. When an AI proposes a chained one-liner, add a fifth question to those four: what happens if the first command fails? A ; 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

The tests here fail on the first run. Use $?, &&, 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.
Deploy Only on Green

Loading playground...

Vibe it

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

Challenge: Ship All Three

Loading challenge...