Terminal Power

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.

Navigation map

flowchart TD A([Start]) --> B[1. First tutorial\nWordPress text remix] B --> C[2. Concepts\nCLI, terminal, shell, paths] C --> D[3. Basic shell commands\nls, cd, mkdir, touch, cat, mv, rm] D --> E[4. Fluency\nhistory and key bindings] E --> F[5. Streams and text tools\nstdin/out/err, &&, grep, find, sed, awk] F --> G[6. Shell scripting\ncommands in files, $1, $2] G --> H[7. Git by doing] H --> I[8. Linux system toolkit\ndu, df, systemd, journalctl, ps, htop, kill] I --> J[9. Enhancing the terminal\nzsh, zsh4humans, tmux, Unicode] J --> K([Appendix / export / fallback]) B -. optional revisit .-> F C -. if beginners need it earlier .-> D F -. leads naturally to .-> G D -. supports live practice in .-> H

How to use this deck:

  • Horizontal navigation moves through the main storyline
  • Vertical navigation enters sub-chapters for each topic
  • Beginners can stay mostly on the upper path
  • Advanced audiences can dive downward into detail where useful

1. First tutorial: remix a WordPress article from the shell

We start with a real task.

Goal:

  • fetch one WordPress article
  • extract its text
  • count common words
  • replace one word with a funny one
  • save the result locally

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

Why start here?

Because the shell becomes convincing when it feels useful.

This first topic demonstrates three big ideas immediately:

  • commands are composable
  • data often flows as text
  • a long task can be built from tiny observable steps

Safety and setup

  • Use a site you own, a staging site, or a public site that exposes its REST API
  • We stay read-only in this tutorial
  • We keep output intentionally small and inspectable
  • Replace the placeholder URL with a real WordPress site for the live demo
export WP_URL="https://YOUR-SITE.example"

Step 1: inspect the post list

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

Step 2: extract exactly what we need

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

Step 3: capture the post id

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"

Step 4: download the article body as HTML

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

Step 5: make it plain-ish text

sed -E \
  -e 's/<[^>]+>/ /g' \
  -e 's/&nbsp;/ /g' \
  -e 's/&amp;/\&/g' \
  -e 's/&quot;/"/g' \
  article.html \
  > article.txt

head -n 10 article.txt

Note:

  • this is not a perfect HTML parser
  • it is good enough for a transparent beginner demo

Step 6: normalize into one word per line

cat article.txt \
  | tr '[:upper:]' '[:lower:]' \
  | sed -E 's/[^[:alpha:]]+/\n/g' \
  | sed '/^$/d' \
  > words.txt

head words.txt
wc -l words.txt

Step 7: count common words

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

Step 8: ignore boring words

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

Step 9: replace one common word with a funny one

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:

  • let the audience choose WORD
  • let the audience choose FUNNY

Step 10: compare before and after

printf '\n--- original ---\n'
grep -n "$WORD" article.txt | head

printf '\n--- remixed ---\n'
grep -n "$FUNNY" remixed.txt | head

Step 11: view the result comfortably

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

Full one-shot pipeline

curl -s "$WP_URL/wp-json/wp/v2/posts/$POST_ID?_fields=content" \
  | jq -r '.content.rendered' \
  | sed -E 's/<[^>]+>/ /g; s/&nbsp;/ /g; s/&amp;/\&/g; s/&quot;/"/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

Script version

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/&nbsp;/ /g; s/&amp;/\&/g; s/&quot;/"/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

Debugging pattern: inspect from left to right

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.

Mini exercises

  1. Show the 30 most common words instead of 20
  2. Change the funny replacement word
  3. Pick a different POST_ID
  4. Save a small report file with the top words

2. Concepts after practice

We now step back and name the concepts we have already used.

CLI, terminal, shell

  • CLI / command line interface
    • a text-based interface to software
  • terminal
    • the app or device that shows text input and output
    • examples: GNOME Terminal, Alacritty, kitty, iTerm2
  • shell
    • the program and language reading your commands
    • examples: sh, bash, zsh

=sh= and =bash=

  • sh
    • the classic shell interface
    • portable scripts often target /bin/sh
  • bash
    • Bourne Again SHell
    • common and featureful
  • today
    • most examples also feel natural in zsh
    • some details vary across systems

The core shell pattern

program 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

Options, arguments, quoting

curl -s "https://example.org/search?q=hello world"
  • options change behavior: -s, --help
  • arguments provide values
  • quotes keep spaces together
  • the shell processes the line before the program sees it

Current working directory

Every shell process has a current working directory.

pwd

This matters because relative paths are interpreted from there.

Relative and absolute paths

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

Pipes: why the shell feels powerful

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

One more tiny pipeline

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.

3. Basic shell commands

The next layer is filesystem confidence.

Listing and location: =pwd= and =ls=

pwd
ls
ls -l
ls -la
ls -lh
  • pwd prints the current working directory
  • ls lists files
  • -l detailed listing
  • -a include hidden files
  • -h human-readable sizes

Change directory: =cd=

cd /tmp
pwd

cd ~
pwd

cd ..
pwd

cd -
pwd
  • / root of the filesystem
  • ~ your home directory
  • .. parent directory
  • - previous directory

Make directories: =mkdir=

mkdir terminal-demo
cd terminal-demo

mkdir notes
mkdir -p project/src project/out project/tmp
find . -maxdepth 2 -type d | sort

Create files: =touch=

touch notes/first.txt
touch project/src/main.txt
ls -l notes project/src
  • creates an empty file if missing
  • updates timestamps if present

Read files: =cat=, =head=, =tail=, =less=

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

Move and rename: =mv=

mv notes/first.txt notes/renamed.txt
ls notes

mv notes/renamed.txt project/src/article.txt
find . -type f | sort
  • mv OLD NEW renames
  • mv FILE DIR/ moves into a directory

Remove: =rm= and =rmdir=

rm project/src/article.txt
rmdir notes
rm -r project/tmp
find . -maxdepth 3 | sort
  • rm FILE removes a file
  • rmdir DIR removes an empty directory
  • rm -r DIR removes a directory tree recursively

Mini drill: build a workspace

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

4. Fluency: history and key bindings

Editing the command line efficiently is part of terminal literacy.

The idea

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=: reverse history search

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= and =Ctrl-D=

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

Movement keys

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

Emacs bindings vs vi bindings

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.

5. Streams, exit codes, and core text tools

The shell becomes deep once you understand streams and status.

Standard input, output, error

stdin    standard input   stream 0
stdout   standard output  stream 1
stderr   standard error   stream 2

These streams explain piping and redirection.

stdin and stdout in practice

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

stderr is separate

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 data
  • stderr can remain diagnostics

Combining or discarding streams

ls 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

Exit codes

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

=&&=, =;=, and =||=

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=: search text

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

=find=: search the filesystem

find . -type f
find . -type f -name '*.org'
find . -type f -mtime -1
find . -type f -size +10M
  • ask find precise questions
  • then pipe the result onward if needed

=sed=: basic stream editing

printf '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 line
  • s/old/new/g replace all matches on each line
  • -n ... p print only selected lines

=awk=: fields and rows

printf '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 field

6. Shell scripting

A shell script is often just the commands you would otherwise type, saved into a file.

The basic mental model

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.

From command list to script

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

Shebang and basic safety flags

#!/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

Command-line arguments: =$1= and =$2=

$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

Validate arguments

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

Script version of the WordPress remix with arguments

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/&nbsp;/ /g; s/&amp;/\&/g; s/&quot;/"/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

Exercise ideas

Add one feature:

  • print the post title first
  • accept an optional output filename
  • add a --help mode
  • save the top 20 words next to the remixed article

7. Git by doing

Git records snapshots of a directory over time.

The Git mental model

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

Create a repository

mkdir -p git-demo
cd git-demo

git init
git status

Create a file and inspect status

printf '# Terminal Demo\n\nFirst note.\n' > README.md

git status
git diff

Interpretation:

  • the file exists in the working tree
  • Git sees it as untracked
  • it is not yet part of history

Stage and commit

git add README.md
git status

git commit -m 'Add README'
git log --oneline --decorate --graph

Modify and inspect

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

Commit again

git add README.md
git commit -m 'Add pipeline note'
git log --oneline --decorate --graph --all

Branch and try another version

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

Switch between versions

git switch main 2>/dev/null || git switch master
cat README.md

git switch funny-version
cat README.md

8. The Linux system toolkit

The terminal is also how we inspect the machine itself.

=du=: disk usage

du -sh .
du -h --max-depth=1 . | sort -h
  • du answers: where did the space go?

=df=: filesystem usage

df -h
df -h /
df -h /home
  • df answers: how much space remains on this filesystem?

=systemctl=: services

systemctl status ssh
systemctl is-active ssh
systemctl list-units --type=service --state=running

sudo systemctl restart ssh
sudo systemctl enable --now ssh

=journalctl=: logs

journalctl -b
journalctl -u ssh
journalctl -u ssh -n 50
journalctl -u ssh -f
journalctl --since '1 hour ago'

=ps ax= and =htop=: processes

ps ax
ps ax | grep ssh
htop
  • ps ax gives a quick process listing
  • htop gives an interactive process viewer

=kill=, =pkill=, =pgrep=

sleep 999 &
pgrep sleep
kill $(pgrep sleep)

sleep 999 &
pkill sleep
pgrep -a ssh
  • kill PID signal a process by id
  • pkill NAME signal by name
  • pgrep search processes more safely than ps ax | grep ...

Mini incident: “the server is full”

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.

9. Enhancing the terminal

A default shell works. A tuned interactive shell can feel dramatically more humane.

Why Zsh is often the nicer interactive shell

  • strong completion system
  • rich prompt customization
  • large plugin ecosystem
  • good integration with fuzzy search, autosuggestions, and syntax highlighting

For interactive use, Zsh is often the nicer shell. For scripts, sh or bash still remain the practical default.

A pragmatic rule: =zsh= for humans, =sh= for scripts

interactive daily shell:
  zsh is often the most comfortable

team / deployment scripts:
  bash is common and predictable

maximum portability:
  POSIX sh

Zsh for Humans: the idea

Zsh for Humans is a curated Zsh setup that bundles many quality-of-life features people otherwise assemble manually.

Typical value:

  • better history search
  • syntax highlighting
  • autosuggestions
  • better completion
  • good prompt defaults
  • tmux-oriented workflow

Better =Ctrl-R= history

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'
  • type fragments in any order
  • preview old commands
  • reuse complex commands without retyping them

Syntax highlighting and autosuggestions

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 by default

tmux is a terminal multiplexer.

one terminal window
  -> multiple sessions
  -> multiple windows
  -> multiple panes
  -> survives disconnects

Why it matters:

  • long-running commands
  • SSH sessions
  • side-by-side logs and shell work
  • resume after network interruption

Unicode characters and fonts

Modern terminal setups often use Unicode for:

  • prompt separators
  • Git branch icons
  • status indicators
  • box drawing
  • powerline-like segments

Practical warning:

  • use a font with the required glyphs
  • keep a simple fallback prompt for remote machines
  • broken glyphs are usually configuration issues, not mystery breakage

Installation philosophy

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.

Recap

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.

Appendix: export from Emacs to Reveal.js

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

Presenter checklist

Before the talk:

  • install curl, jq, sed, sort, uniq, grep
  • test the WordPress URL and post id once
  • keep fallback article.html and article.txt locally
  • increase terminal font size
  • use a clean demo directory
  • avoid live production credentials

Fallback without WordPress

If 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