01 — Shell survival
Read this first: this lesson teaches what a shell actually is, how to move around a filesystem, how to chain commands together, and — the part everyone skips and then regrets — how a command reports success or failure. It does not teach scripting yet; that is lesson 02.
Time: about 30 minutes. Everything here runs on your own machine. Nothing you type can damage anything, because we work inside a scratch directory you create and delete.
Why this comes first
Every deployment tool you will meet is a thin wrapper over a shell. GitHub Actions runs shell.
Dockerfiles run shell. deploy/deploy.sh is 439 lines of shell. If the shell is a mystery, all of
those stay a mystery, and you end up "fixing" things by shuffling lines until the red goes away.
What a shell actually is
A shell is a program that reads a line of text, decides which program you meant, starts it, and
hands it your arguments. That is genuinely all it is. ls is not a shell feature — it is a file at
/usr/bin/ls that the shell finds and executes.
Three ideas make up almost everything else:
- Every program gets input and produces two output streams. Normal output (
stdout) and error output (stderr). They are separate on purpose. - Every program returns a number when it exits. Zero means success. Anything else means failure.
- You can connect programs together, feeding one's output into the next's input.
Hold onto those three. The rest of this lesson is just their consequences.
Set up a scratch directory
mkdir -p ~/devops-course/l01
cd ~/devops-course/l01
mkdir -p means "make the directory, and its parents, and don't complain if it already exists".
The -p is why you can run that line twice safely — a property called idempotence that turns out
to matter enormously later, because deploy scripts get re-run.
cd changes your current directory. To see where you are:
pwd
/home/you/devops-course/l01
Predict: you are about to create two empty files and list them. What do you think ls shows
versus ls -l?
Write your answer down before you run it. Really.
mkdir notes
touch notes/a.txt notes/b.txt
ls -l notes
total 0
-rw-rw-r-- 1 you you 0 Aug 5 19:47 a.txt
-rw-rw-r-- 1 you you 0 Aug 5 19:47 b.txt
(Real output from running the command above; only the username has been replaced with you.)
That output is worth decoding, because you will read it a hundred times:
| Piece | Meaning |
|---|---|
-rw-rw-r-- | The type and permissions. Leading - = regular file (d = directory). Then three groups of three: owner may read+write, group may read+write, everyone else may only read. |
1 | How many names point at this file (hard links). Almost always 1. |
lrjab lrjab | Owning user, then owning group. |
0 | Size in bytes. touch makes empty files. |
Aug 5 19:47 | Last modified. |
a.txt | The name. |
That permissions column is not trivia. Later in the course you will meet umask 077, which exists
so that a file holding production secrets comes out -rw------- instead of -rw-rw-r--. The real
deploy workflow does exactly that before writing .env on the server.
Exit codes: how a command says "that went wrong"
Every command sets a variable you can read with $?. This is the single most important thing in
this lesson.
Predict: what number do you expect after a command that works? After one that fails?
ls notes > /dev/null
echo "$? = success case"
0 = success case
Now a command that fails:
ls nope
echo "exit was $?"
ls: cannot access 'nope': No such file or directory
exit was 2
Two things happened there:
- The error message went to stderr, which is why
> /dev/nullin the first example silenced the normal output but an error would still have reached your screen. - The exit code was 2, not 1. Different programs use different non-zero codes to mean different failures. Zero is success; everything else is failure. Do not memorise the specific numbers.
This is how every CI system on earth decides whether a step passed. A GitHub Actions step is green if the command exited 0 and red if it did not. That is the entire mechanism. When you understand this, "why did my workflow fail" stops being mysterious and becomes "which command returned non-zero, and why".
Chaining commands
cd ~/devops-course/l01
printf 'alpha\nbravo\ncharlie\nbravo\n' > words.txt
cat words.txt
alpha
bravo
charlie
bravo
> redirects stdout into a file, replacing its contents. >> appends instead. printf writes
text, and \n is a newline.
Now connect two programs with a pipe (|), which feeds the left one's stdout into the right one's
stdin:
grep bravo words.txt
bravo
bravo
grep bravo words.txt | wc -l
2
grep printed the matching lines; wc -l counted them. Neither program knows the other exists.
That is the whole Unix idea: small programs, connected by text.
A few more you will actually use:
sort -u words.txt
alpha
bravo
charlie
sort -u words.txt > unique.txt
cat unique.txt
alpha
bravo
charlie
Break it on purpose: the pipeline that lies
This is the most important drill in the lesson, and it is the reason set -o pipefail exists.
Predict: grep delta words.txt finds nothing — delta is not in the file. What exit code do
you expect from grep? And what exit code do you expect from grep delta words.txt | wc -l?
Most people answer "non-zero, and non-zero". Run it:
grep delta words.txt
echo "grep exit = $?"
grep exit = 1
Good — grep correctly reported failure with exit code 1. Now the pipeline:
grep delta words.txt | wc -l
echo "pipeline exit = $?"
0
pipeline exit = 0
Read that again. The pipeline reported success even though the first command failed.
By default, a shell pipeline's exit code is the exit code of the last command only. wc -l
succeeded — it counted zero lines, which is a perfectly successful count — so the whole pipeline
reported 0. The failure of grep was thrown away.
This is not a curiosity. This is how a deploy script silently continues past a step that did not work, and how a CI job goes green while doing nothing. Fixing it is one line, and you will write it in lesson 03.
Fix it
You can see the truth by asking for every stage's exit code:
grep delta words.txt | wc -l
echo "${PIPESTATUS[@]}"
0
1 0
PIPESTATUS is an array holding one exit code per pipeline stage: 1 from grep, 0 from
wc -l. In practice you will not use PIPESTATUS much — you will turn on pipefail instead — but
seeing it once makes clear that the information was always there and the default just discards it.
Where this shows up in MotorPH
- Every script under deploy/ opens with
set -euo pipefail. Thepipefailpart is there entirely because of the behaviour you just saw. - deploy/deploy.sh contains
{ grep -s '^CURRENT_TAG=' "$STATE_FILE" || true; } | cut -d= -f2. The|| trueis there becausegrepreturning 1 on no-match would otherwise kill the whole script — a direct consequence of grep's exit code, which you just measured. - Every green check in .github/workflows/ means "this command exited 0".
Clean up
cd ~
rm -rf ~/devops-course/l01
rm -rf deletes recursively and without asking. It is worth building the habit now of reading an
rm -rf line before pressing enter, because it does not have an undo and it will not warn you.
Recap
- A shell finds programs and runs them;
stdoutandstderrare separate streams, and>only redirects the first one. - Exit code 0 means success; anything else means failure. This is how CI decides pass or fail.
- A pipeline reports only its last command's exit code, so a failure in the middle disappears
unless you ask for
pipefail.
Next: 02 — Your first script, where these commands stop being things you type and become a file you can re-run.