From one command to a WordPress article remix, and from there to a broader terminal skillset.
The terminal is easiest to learn when it solves a real task first.
How to use this deck:
We start with a real task.
Goal:
Tool chain:
curl -> fetch data from the web
jq -> extract fields from JSON
sed -> transform text
sort -> sort lines
uniq -> count repeated neighboring lines
grep -> filter
Because the shell becomes convincing when it feels useful.
This first topic demonstrates three big ideas immediately:
export WP_URL="https://YOUR-SITE.example"
curl -s "$WP_URL/wp-json/wp/v2/posts?per_page=1&_fields=id,link,title" \
| jq .
Expected shape:
[
{
"id": 123,
"link": "https://YOUR-SITE.example/my-post/",
"title": { "rendered": "My Post Title" }
}
]
curl -s "$WP_URL/wp-json/wp/v2/posts?per_page=1&_fields=id,link,title" \
| jq -r '.[0] | "id=\(.id)\ntitle=\(.title.rendered)\nlink=\(.link)"'
Key jq ideas:
. current JSON value
.[0] first item in an array
.id object field
-r output raw text rather than JSON strings
export POST_ID="$({
curl -s "$WP_URL/wp-json/wp/v2/posts?per_page=1&_fields=id" \
| jq -r '.[0].id'
})"
printf 'POST_ID=%s\n' "$POST_ID"
curl -s "$WP_URL/wp-json/wp/v2/posts/$POST_ID?_fields=title,content" \
| jq -r '.content.rendered' \
> article.html
wc -c article.html
head -n 5 article.html
sed -E \
-e 's/<[^>]+>/ /g' \
-e 's/ / /g' \
-e 's/&/\&/g' \
-e 's/"/"/g' \
article.html \
> article.txt
head -n 10 article.txt
Note:
cat article.txt \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^[:alpha:]]+/\n/g' \
| sed '/^$/d' \
> words.txt
head words.txt
wc -l words.txt
cat words.txt \
| sort \
| uniq -c \
| sort -nr \
| head -n 20
Why it works:
sort puts equal words next to each other
uniq -c counts repeated neighboring lines
sort -nr sorts counts numerically, largest first
head limits visible output
cat > stopwords.txt <<'STOPWORDS'
the
and
or
a
an
to
of
in
on
for
with
is
are
was
were
this
that
STOPWORDS
grep -vxF -f stopwords.txt words.txt \
| sort \
| uniq -c \
| sort -nr \
| head -n 20
export WORD="example"
export FUNNY="bananaphone"
sed -E \
"s/(^|[^[:alnum:]_])${WORD}([^[:alnum:]_]|$)/\1${FUNNY}\2/g" \
article.txt \
> remixed.txt
head -n 20 remixed.txt
Good live-demo move:
WORDFUNNYprintf '\n--- original ---\n'
grep -n "$WORD" article.txt | head
printf '\n--- remixed ---\n'
grep -n "$FUNNY" remixed.txt | head
fold -s -w 80 remixed.txt | less
Useful keys inside less:
Space next page
b previous page
/word search
n next search result
q quit
curl -s "$WP_URL/wp-json/wp/v2/posts/$POST_ID?_fields=content" \
| jq -r '.content.rendered' \
| sed -E 's/<[^>]+>/ /g; s/ / /g; s/&/\&/g; s/"/"/g' \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^[:alpha:]]+/\n/g' \
| sed '/^$/d' \
| grep -vxF -f stopwords.txt \
| sort \
| uniq -c \
| sort -nr \
| head -n 20
cat > wp-word-remix.sh <<'SCRIPT'
#!/usr/bin/env sh
set -eu
: "${WP_URL:?set WP_URL, e.g. https://example.org}"
: "${POST_ID:?set POST_ID, e.g. 123}"
: "${WORD:?set WORD, e.g. terminal}"
: "${FUNNY:=bananaphone}"
curl -s "$WP_URL/wp-json/wp/v2/posts/$POST_ID?_fields=content" \
| jq -r '.content.rendered' \
| sed -E 's/<[^>]+>/ /g; s/ / /g; s/&/\&/g; s/"/"/g' \
| sed -E "s/(^|[^[:alnum:]_])${WORD}([^[:alnum:]_]|$)/\1${FUNNY}\2/g"
SCRIPT
chmod +x wp-word-remix.sh
./wp-word-remix.sh > remixed-script-output.txt
curl -s "$WP_URL/wp-json/wp/v2/posts/$POST_ID?_fields=content" | head
curl -s "$WP_URL/wp-json/wp/v2/posts/$POST_ID?_fields=content" \
| jq . \
| head
curl -s "$WP_URL/wp-json/wp/v2/posts/$POST_ID?_fields=content" \
| jq -r '.content.rendered' \
| head
When a pipeline fails, do not stare at the whole thing. Break it at every pipe and inspect each stage.
POST_IDWe now step back and name the concepts we have already used.
CLI / command line interface
terminal
shell
sh, bash, zshsh
/bin/shbash
zshprogram argument1 argument2 argument3
Read a command line as:
first position -> the program to run
later positions -> arguments passed to that program
Example:
curl -s https://example.org
curl -> program
-s -> option / argument
URL -> argument
curl -s "https://example.org/search?q=hello world"
-s, --helpEvery shell process has a current working directory.
pwd
This matters because relative paths are interpreted from there.
. current directory
.. parent directory
article.txt relative path
./article.txt relative path, explicit current dir
/tmp/article.txt absolute path
~/article.txt path inside your home directory
Short demo:
mkdir -p terminal-demo
cd terminal-demo
printf 'hello terminal\n' > note.txt
cat note.txt
cat ./note.txt
cat "$HOME/terminal-demo/note.txt"
A pipe sends the output of one command into the input of another.
cat fish.txt | sort
program A stdout ---> stdin program B
You can read | as “then feed that
into”.
printf 'one fish\ntwo fish\nred fish\nblue fish\n' > fish.txt
cat fish.txt | sort | uniq -c | sort -nr
The shell is not one giant tool. It is a place where small tools cooperate.
The next layer is filesystem confidence.
pwd
ls
ls -l
ls -la
ls -lh
pwd prints the current working
directoryls lists files-l detailed listing-a include hidden files-h human-readable sizescd /tmp
pwd
cd ~
pwd
cd ..
pwd
cd -
pwd
/ root of the filesystem~ your home directory.. parent directory- previous directorymkdir terminal-demo
cd terminal-demo
mkdir notes
mkdir -p project/src project/out project/tmp
find . -maxdepth 2 -type d | sort
touch notes/first.txt
touch project/src/main.txt
ls -l notes project/src
printf 'alpha\nbeta\ngamma\ndelta\n' > notes/first.txt
cat notes/first.txt
head -n 2 notes/first.txt
tail -n 2 notes/first.txt
less notes/first.txt
mv notes/first.txt notes/renamed.txt
ls notes
mv notes/renamed.txt project/src/article.txt
find . -type f | sort
mv OLD NEW renamesmv FILE DIR/ moves into a
directoryrm project/src/article.txt
rmdir notes
rm -r project/tmp
find . -maxdepth 3 | sort
rm FILE removes a filermdir DIR removes an empty
directoryrm -r DIR removes a directory tree
recursivelymkdir -p workshop/{raw,clean,out}
printf 'The shell is small tools.\n' > workshop/raw/text.txt
cat workshop/raw/text.txt
mv workshop/raw/text.txt workshop/clean/text.txt
cp workshop/clean/text.txt workshop/out/copy.txt
find workshop -type f | sort
Editing the command line efficiently is part of terminal literacy.
history old commands remembered by the shell
line editing moving and changing the current command
signals keys that talk to the running process
Many default shell bindings are Emacs-style.
Ctrl-R opens reverse incremental search
through command history.
(reverse-i-search)`curl': curl -s "$WP_URL/wp-json/wp/v2/posts?per_page=1"
Useful rhythm:
Ctrl-R start search
curl search for old curl commands
Ctrl-R jump to older matches
Enter run the selected command
Hands-on:
history | tail -n 20
# then press Ctrl-R interactively
Ctrl-C interrupt the currently running command
Ctrl-D end input / exit shell when the line is empty
Try:
sleep 999
# press Ctrl-C
sh
# press Ctrl-D on an empty line to leave the subshell
Ctrl-A beginning of line
Ctrl-E end of line
Ctrl-B backward one character
Ctrl-F forward one character
Ctrl-P previous history entry
Ctrl-N next history entry
Exercise:
printf 'hello terminal tutorial\n'
# Before pressing Enter, move through the line with Ctrl-A/E/B/F
# Then browse history with Ctrl-P and Ctrl-N
Many shells support either style.
Emacs-style:
Ctrl-A, Ctrl-E, Ctrl-R, Ctrl-P, Ctrl-N
vi-style:
modal editing, often preferred by vim/neovim users
In bash:
set -o emacs
set -o vi
This is not an editor war. The important point is that command lines are editable text, not a one-shot prompt.
The shell becomes deep once you understand streams and status.
stdin standard input stream 0
stdout standard output stream 1
stderr standard error stream 2
These streams explain piping and redirection.
printf 'banana\napple\nbanana\n' > fruit.txt
cat fruit.txt
cat < fruit.txt
cat fruit.txt | sort
Three views of the same idea:
file argument cat fruit.txt
stdin redirect cat < fruit.txt
pipe cat fruit.txt | sort
ls fruit.txt missing-file.txt > out.log 2> err.log
printf 'stdout:\n'
cat out.log
printf 'stderr:\n'
cat err.log
Important idea:
stdout can be clean datastderr can remain diagnosticsls fruit.txt missing-file.txt > combined.log 2>&1
cat combined.log
ls fruit.txt missing-file.txt 2>/dev/null
Read 2>&1 as:
send stderr to the same destination stdout currently uses
Every command exits with a status code.
0 success
non-0 failure or a special condition
Hands-on:
true
echo "$?"
false
echo "$?"
grep banana fruit.txt
echo "$?"
grep dragon fruit.txt
echo "$?"
cmd1 ; cmd2 run cmd2 regardless of success
cmd1 && cmd2 run cmd2 only if cmd1 succeeded
cmd1 || cmd2 run cmd2 only if cmd1 failed
Hands-on:
mkdir -p safe-demo && cd safe-demo && pwd
false ; echo 'semicolon still continues'
false && echo 'this will not run'
false || echo 'this handles failure'
grep root /etc/passwd
grep -n root /etc/passwd
grep -R 'TODO' .
grep -i 'error' logfile.txt
-n line numbers-R recursive search-i case-insensitivefind . -type f
find . -type f -name '*.org'
find . -type f -mtime -1
find . -type f -size +10M
find precise questionsprintf 'cats like cats\n' > demo.txt
sed 's/cats/badgers/' demo.txt
sed 's/cats/badgers/g' demo.txt
sed -n '1,3p' demo.txt
s/old/new/ replace first match on each
lines/old/new/g replace all matches on
each line-n ... p print only selected
linesprintf 'alice 10\nbob 25\ncarol 15\n' > scores.txt
awk '{ print $1 }' scores.txt
awk '{ print $1, $2 * 2 }' scores.txt
awk '$2 >= 15 { print $1 }' scores.txt
awk works well on column-like
text$1 first field, $2 second fieldA shell script is often just the commands you would otherwise type, saved into a file.
terminal session script file
--------------- ----------------
command 1 command 1
command 2 ---> command 2
command 3 command 3
It is a dynamic interpreted language: the shell reads and executes the file at runtime.
cat > word-count-demo.sh <<'SCRIPT'
#!/usr/bin/env sh
set -eu
mkdir -p reports
printf 'hello\nterminal\nhello\n' > reports/input.txt
cat reports/input.txt | sort | uniq -c
SCRIPT
chmod +x word-count-demo.sh
./word-count-demo.sh
#!/usr/bin/env sh
set -eu
Meaning:
#!/usr/bin/env sh run the file with sh
set -e stop on command failure
set -u stop on unset variables
$0 script name
$1 first argument
$2 second argument
$@ all arguments
$# number of arguments
Example:
cat > greet.sh <<'SCRIPT'
#!/usr/bin/env sh
set -eu
name="$1"
word="$2"
printf 'Hello %s, welcome to %s!\n' "$name" "$word"
SCRIPT
chmod +x greet.sh
./greet.sh Flo terminal
cat > replace-word.sh <<'SCRIPT'
#!/usr/bin/env sh
set -eu
if [ "$#" -lt 3 ]; then
printf 'usage: %s FILE WORD REPLACEMENT\n' "$0" >&2
exit 2
fi
file="$1"
word="$2"
replacement="$3"
sed -E "s/(^|[^[:alnum:]_])${word}([^[:alnum:]_]|$)/\1${replacement}\2/g" "$file"
SCRIPT
chmod +x replace-word.sh
./replace-word.sh article.txt terminal cyberbadger > replaced-by-script.txt
cat > wp-remix-args.sh <<'SCRIPT'
#!/usr/bin/env sh
set -eu
if [ "$#" -lt 4 ]; then
printf 'usage: %s WP_URL POST_ID WORD REPLACEMENT\n' "$0" >&2
exit 2
fi
wp_url="$1"
post_id="$2"
word="$3"
replacement="$4"
curl -s "$wp_url/wp-json/wp/v2/posts/$post_id?_fields=content" \
| jq -r '.content.rendered' \
| sed -E 's/<[^>]+>/ /g; s/ / /g; s/&/\&/g; s/"/"/g' \
| sed -E "s/(^|[^[:alnum:]_])${word}([^[:alnum:]_]|$)/\1${replacement}\2/g"
SCRIPT
chmod +x wp-remix-args.sh
./wp-remix-args.sh "$WP_URL" "$POST_ID" terminal cyberbadger > remixed-args.txt
Add one feature:
--help modeGit records snapshots of a directory over time.
working tree files as they exist now
staging area selected changes for the next commit
commit a saved snapshot
branch a movable name pointing at a commit
mkdir -p git-demo
cd git-demo
git init
git status
printf '# Terminal Demo\n\nFirst note.\n' > README.md
git status
git diff
Interpretation:
git add README.md
git status
git commit -m 'Add README'
git log --oneline --decorate --graph
printf '\nSecond note: pipelines compose tools.\n' >> README.md
git status
git diff
Read git diff like a patch:
--- old version
+++ new version
+ added line
- removed line
git add README.md
git commit -m 'Add pipeline note'
git log --oneline --decorate --graph --all
git switch -c funny-version
sed 's/pipelines/bananaphones/g' README.md > README.next
mv README.next README.md
git diff
git add README.md
git commit -m 'Make README funny'
git log --oneline --decorate --graph --all
git switch main 2>/dev/null || git switch master
cat README.md
git switch funny-version
cat README.md
The terminal is also how we inspect the machine itself.
du -sh .
du -h --max-depth=1 . | sort -h
du answers: where did the space
go?df -h
df -h /
df -h /home
df answers: how much space remains on
this filesystem?systemctl status ssh
systemctl is-active ssh
systemctl list-units --type=service --state=running
sudo systemctl restart ssh
sudo systemctl enable --now ssh
journalctl -b
journalctl -u ssh
journalctl -u ssh -n 50
journalctl -u ssh -f
journalctl --since '1 hour ago'
ps ax
ps ax | grep ssh
htop
ps ax gives a quick process
listinghtop gives an interactive process
viewersleep 999 &
pgrep sleep
kill $(pgrep sleep)
sleep 999 &
pkill sleep
pgrep -a ssh
kill PID signal a process by idpkill NAME signal by namepgrep search processes more safely
than ps ax | grep ...df -h
du -h --max-depth=1 /var | sort -h
journalctl --disk-usage
journalctl --vacuum-time=7d
Good terminal work means knowing which question each tool answers.
A default shell works. A tuned interactive shell can feel dramatically more humane.
For interactive use, Zsh is often the nicer shell. For scripts,
shorbashstill remain the practical default.
interactive daily shell:
zsh is often the most comfortable
team / deployment scripts:
bash is common and predictable
maximum portability:
POSIX sh
Zsh for Humans is a curated Zsh setup
that bundles many quality-of-life features people otherwise assemble
manually.
Typical value:
With a fuzzy-history setup, Ctrl-R
becomes more like a searchable command picker.
search: wp curl jq
curl -s https://example.org/wp-json/wp/v2/posts?per_page=1 | jq .
./wp-remix.sh https://example.org absurd badger
jq -r '.content.rendered' post.json | sed 's/<[^>]*>//g'
valid command -> visually reassuring
unknown command -> warning color
quoted strings -> visually distinct
history suggestion -> ghost text after the cursor
This reduces mistakes before you press Enter.
tmux is a terminal multiplexer.
one terminal window
-> multiple sessions
-> multiple windows
-> multiple panes
-> survives disconnects
Why it matters:
Modern terminal setups often use Unicode for:
Practical warning:
1. read the README
2. inspect what the installer changes
3. back up ~/.zshrc and related config
4. test on a VM or non-critical user first
5. keep a rescue shell open
A better shell should reduce friction without hiding how the system works.
practice first -> WordPress text remix
mental model -> CLI / terminal / shell / paths / pipes
everyday basics -> ls cd mkdir touch cat mv rm
fluency -> history and key bindings
composition -> streams, exit codes, grep, find, sed, awk
automation -> shell scripts with arguments
history of work -> Git
system operation -> du df systemctl journalctl ps htop kill
comfort -> zsh, zsh4humans, tmux, Unicode
The terminal is not one skill. It is a small number of patterns repeated across many domains.
This file is intended for Org export to Reveal.js.
Typical preparation:
git clone https://github.com/hakimel/reveal.js.git
Relevant header settings in this deck:
Reading the REVEAL_HLEVEL choice:
* headings become horizontal
slides** headings become vertical
sub-slidesBefore the talk:
curl, jq, sed, sort, uniq, greparticle.html and article.txt locallyIf the network fails, create local demo text instead.
cat > article.txt <<'TEXT'
The terminal is a small room with a very large door.
The shell waits for a command.
The command calls a program.
The program reads text, writes text, and changes the world a little.
TEXT
cat article.txt \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^[:alpha:]]+/\n/g' \
| sed '/^$/d' \
| sort \
| uniq -c \
| sort -nr