Advanced CLI Operations | Advanced Linux Administration

Slide 1 of 35  |  ALA-01  |  Week 1 of 8
Advanced CLI Operations
Pipes, Process Control & Multiplexing
Advanced Pipes  •  Process Substitution  •  Job Control  •  tmux  •  Subshells
Your cell has been assigned a degraded sector node. You need to audit running processes, redirect output to three separate log streams, and keep your session alive while SSH flaps. You have sixty seconds. These tools are how you operate under that tempo.
35 Slides ALA-01 Week 1 of 8 Ubuntu 22.04 LTS
Slide 2 of 35
The Unix Philosophy
Why the CLI is still more powerful than any GUI ever built.
Do One Thing Well
Each CLI tool solves exactly one problem. grep finds patterns. sort sorts. uniq removes duplicates. wc counts. None of them try to be more. The power comes from connecting them.
Text Is the Universal Interface
Every Unix tool reads text and writes text. That shared contract is what makes pipes work. A tool written in 1975 can receive output from a tool written in 2024 because both speak lines of text.
Compose, Don't Monolith
Instead of one program that does everything, you chain small programs into pipelines. A five-command pipeline that you understand completely is far more powerful than a GUI tool you cannot modify or script.
Why This Matters for You
Every advanced CLI technique in this module is an application of composition. You are not learning isolated commands. You are learning how to build pipelines that would take a GUI operator ten times as long to reproduce.
Slide 3 of 35
Pipes: Beyond the Basics
You know | exists. Now understand exactly what it does and where it breaks.
What a Pipe Actually Does
A pipe | connects stdout of the left command to stdin of the right command. The kernel creates an in-memory buffer between them. Both processes run concurrently. The left writes; the right reads. No disk I/O involved.
Pipes Do NOT Pass stderr
Only stdout flows through a pipe. Error messages written to stderr bypass the pipe entirely and appear on your terminal. If you want stderr in the pipeline, redirect it first: cmd 2>&1 | next. This is a common debugging trap.
# Classic pipe: stdout flows left-to-right ps aux | grep 'nginx' | awk '{print $2, $11}' # stderr bypasses the pipe by default — errors appear on terminal find /etc -name '*.conf' | wc -l # Permission errors go to terminal # Merge stderr into stdout before piping find /etc -name '*.conf' 2>&1 | grep -v 'Permission denied' | wc -l # Pipe to tee: write to file AND continue the pipeline journalctl -u nginx | tee /tmp/nginx.log | grep 'error'
Slide 4 of 35
Named Pipes: FIFOs
Pipes that exist on the filesystem, enabling IPC between unrelated processes.
What Is a FIFO?
A named pipe (FIFO = First In, First Out) is a special file created with mkfifo. Unlike an anonymous pipe, it persists on the filesystem and allows two separate processes, started independently, to communicate through it.
When to Use Named Pipes
When the two processes cannot be started in the same command line (e.g., different terminals, different scripts, different users). Common in logging architectures: one process writes continuously; another reads and filters.
# Create a named pipe (FIFO) mkfifo /tmp/grid-feed # Terminal 1 — writer: continuously send data into the FIFO tail -f /var/log/syslog > /tmp/grid-feed # Terminal 2 — reader: consume from the FIFO and filter grep --line-buffered 'CRIT' /tmp/grid-feed | tee /var/log/critical-events.log # Inspect the pipe file — notice the 'p' type in ls output ls -la /tmp/grid-feed # prw-r--r-- 1 user user 0 Apr 9 09:00 /tmp/grid-feed # Remove when done rm /tmp/grid-feed
Key Behavior
A FIFO blocks. The writer blocks until a reader opens the pipe. The reader blocks until the writer writes. This synchronization is by design. If you need non-blocking behavior, use a socket or message queue instead.
Slide 5 of 35
Process Substitution: <(command)
Treat the output of a command as if it were a file. One of bash's most powerful features.
You want to compare two live outputs without writing temp files. Process substitution lets you hand a command's output to another command that expects a filename. The shell handles the plumbing behind the scenes.
# Syntax: <(command) provides output as a readable pseudo-file # diff expects two file arguments — we feed it two live command outputs diff <(cat /etc/passwd) <(getent passwd) # Compare sorted package list from two different hosts via SSH diff <(ssh node-a 'dpkg -l | sort') <(ssh node-b 'dpkg -l | sort') # comm needs two sorted files — use process substitution comm -23 <(sort /tmp/hosts-a.txt) <(sort /tmp/hosts-b.txt) # Output process substitution >(command): redirect into a command's stdin tee >(grep ERROR > /var/log/errors.log) >(grep WARN > /var/log/warnings.log) < /var/log/app.log
Under the Hood
Bash creates a temporary file descriptor (e.g., /dev/fd/63) connected to the command's stdout. The outer command receives that path as its argument. No disk writes occur. This is a bash and zsh feature -- it does not work in plain sh.
Slide 6 of 35
Command Chaining: &&, ||, ;
Control whether the next command runs based on whether the previous one succeeded.
&& — AND (run if success)
Run the right command only if the left command exits with status 0 (success). Use for sequential operations where each step depends on the previous one completing successfully. Most common chaining operator.
|| — OR (run if failure)
Run the right command only if the left command exits with a non-zero status (failure). Use for fallback logic: try the primary action; if it fails, run the recovery. Common in deployment scripts and service restarts.
; — Unconditional
Always run the right command regardless of the exit status of the left. Equivalent to pressing Enter between two commands. Use when the commands are independent and you do not care if one fails.
# && — create dir AND copy only if mkdir succeeds mkdir -p /opt/sector/config && cp /etc/app.conf /opt/sector/config/ # || — restart service if status check fails systemctl is-active nginx || systemctl start nginx # Chain: install, then enable, then start — stop at first failure apt-get install -y nginx && systemctl enable nginx && systemctl start nginx # ; — run cleanup regardless of what happened above cp /tmp/deploy.sh /opt/ ; rm -f /tmp/deploy.sh # Combine: try primary, fall back, then log result either way systemctl start app-primary || systemctl start app-backup ; echo "startup attempted"
Slide 7 of 35
Exit Codes: The Language of Success and Failure
Every command reports its outcome as a number. Your chaining logic depends on reading it correctly.
Exit Code 0 = Success
By convention, exit code 0 means the command completed without error. This is the only value that && treats as success. The shell stores the last exit code in $?. Read it immediately after a command -- the next command overwrites it.
Non-Zero = Failure
Any value 1-255 is failure. The specific value encodes the type of failure. Exit 1 is a generic error. Exit 2 is often a misuse of the command. Exit 126 = command found but not executable. Exit 127 = command not found. Exit 128+N = terminated by signal N.
# Check the exit code of the last command ls /etc/nginx echo $? # 0 — directory exists, ls succeeded ls /etc/nonexistent echo $? # 2 — no such file or directory # grep: exit 0 if match found, exit 1 if no match, exit 2 on error grep 'root' /etc/passwd echo $? # 0 — found grep 'zzz_no_such_user' /etc/passwd echo $? # 1 — not found (not an error — just no match) # In a script: test exit codes explicitly if systemctl is-active --quiet nginx; then echo "nginx is running" else echo "nginx is DOWN — alerting ops" fi
Slide 8 of 35
Subshells () vs Command Grouping {}
Both run multiple commands together. The critical difference: isolation.
( ) — Subshell
Commands run in a child process. Any variable assignments, directory changes, or environment modifications made inside do NOT affect the parent shell. When the subshell exits, it is as if nothing happened outside. Use when you need isolation.
{ } — Current Shell Group
Commands run in the current shell process. Variable changes, cd changes, and environment modifications DO persist after the group exits. The opening brace requires a space after it. The closing brace must be preceded by a semicolon or newline.
# Subshell: cd change is isolated — parent stays in /etc pwd # /etc (cd /tmp && ls) # runs in child, lists /tmp pwd # /etc — unchanged # Group: cd change persists in current shell { cd /tmp && ls; } # runs in current shell pwd # /tmp — changed! # Subshell: redirect combined output of multiple commands (echo "=== /etc/hosts ===" && cat /etc/hosts) > /tmp/report.txt # Group: redirect combined output while preserving shell state { date; uptime; who; } > /tmp/status.txt
Common Gotcha
Using ( ) when you meant { } in a script that sets variables. The variables exist inside the subshell and silently disappear the moment it exits. Always check which one you need before writing deployment scripts.
Slide 9 of 35
Redirection: Full Reference
stdin, stdout, stderr — redirect each precisely or lose control of your output.
stdout Redirect
> file truncates and writes. >> file appends. File descriptor 1 is stdout. Explicit form: 1> file. The file is created if it does not exist, truncated if > is used.
stderr Redirect
2> file redirects stderr (fd 2) to a file. 2>&1 redirects stderr to wherever stdout currently points. Order matters: cmd > file 2>&1 is different from cmd 2>&1 > file.
stdin Redirect
< file feeds a file as stdin. <<EOF ... EOF is a here-document: inline multiline input. <<<"string" is a here-string: single string on stdin.
# Redirect stdout only — errors still appear on terminal find / -name 'shadow' > /tmp/shadow-results.txt # Redirect stderr only — output still appears on terminal find / -name 'shadow' 2> /tmp/errors.txt # Redirect both to the same file (bash 4+ shorthand) find / -name 'shadow' >/tmp/all.txt 2>&1 find / -name 'shadow' &>/tmp/all.txt # bash shorthand, same result # Discard stderr entirely find / -name 'shadow' 2>/dev/null # Here-string: pass a string directly as stdin base64 <<<"sector-alpha-credentials"
Slide 10 of 35
Job Control: Managing Background Processes
Keep your terminal free while long operations run. Essential under operational tempo.
You launch a 45-minute log archival job. Your terminal locks. Fifteen minutes in, a critical alert arrives and you need the terminal now. This is why job control exists -- you do not need a second terminal for every task.
& — Background Launch
Append & to any command to launch it in the background immediately. The shell prints the job number and PID, then returns your prompt. The job runs as a background process in your current session.
Ctrl+Z — Suspend
Sends SIGTSTP to the foreground process, pausing it. The process is now stopped but not terminated. Its state is preserved. You get your prompt back. Use fg or bg to resume it.
jobs — List Jobs
Shows all background and stopped jobs in the current shell session. Each job has a number like [1]. Reference jobs by this number with %1, %2, etc. The + marker indicates the current job.
# Launch a background job with & tar czf /backup/sector-$(date +%F).tar.gz /opt/sector/ & # [1] 14823 # List running jobs jobs # [1]+ Running tar czf /backup/sector-... & # Suspend the foreground process (press Ctrl+Z interactively) # [1]+ Stopped tar czf /backup/sector-... &
Slide 11 of 35
fg, bg, disown — Job Lifecycle
Move jobs between foreground, background, and full independence from your session.
fg [%n] — Bring to Foreground
Resume a stopped or background job in the foreground. Without an argument, resumes the current job (marked + in jobs). With %2, resumes job 2. The terminal attaches to it -- you will not get your prompt back until it finishes or you suspend it again.
bg [%n] — Resume in Background
Resume a stopped job in the background (as if launched with &). The job continues running but you keep your prompt. Useful after Ctrl+Z: suspend a job that is taking too long, then send it to background with bg.
disown [%n] — Detach from Session
Remove a job from the shell's job table. The process continues running, but the shell will no longer send it SIGHUP when your session ends. Use after & when you need a background job to outlive your SSH session. Works without nohup.
# Start a job, suspend it, resume in background rsync -av /data/ backup-node:/data/ # starts running # Press Ctrl+Z to suspend # [1]+ Stopped rsync -av /data/ ... bg %1 # resume in background disown %1 # detach — safe to close terminal # Useful pattern: launch, disown, walk away ./full-audit.sh > /var/log/audit.log 2>&1 & disown # no job number needed — acts on most recent
Slide 12 of 35
Signals: Communicating with Processes
Signals are the kernel's mechanism for sending notifications to running processes.
Sending Signals with kill
kill sends a signal to a process by PID. Despite the name, it does not always terminate. kill -l lists all signals. Signal numbers and names are interchangeable: kill -9 PID and kill -SIGKILL PID are identical.
SIGKILL vs SIGTERM
SIGTERM (15) is the polite request to terminate. The process can catch it, clean up, and exit gracefully. SIGKILL (9) is instant, unconditional termination by the kernel. The process cannot catch or ignore it. Always try SIGTERM first -- use SIGKILL only if the process ignores it.
# Common signals # SIGTERM (15) — graceful termination request kill 14823 # SIGTERM is the default kill -15 14823 # explicit SIGTERM # SIGKILL (9) — force kill, no cleanup possible kill -9 14823 # SIGHUP (1) — reload config without restarting (many daemons) kill -1 $(cat /var/run/nginx.pid) # SIGUSR1 (10) — user-defined, used by some apps for log rotation kill -10 $(pgrep nginx) # killall: signal all processes with a given name killall -TERM python3 # pkill: signal by pattern match (more flexible) pkill -f 'worker_process'
Slide 13 of 35
Terminal Multiplexing: screen
Keep sessions alive after SSH disconnects. The original survival tool.
Your SSH connection to the remote node drops mid-deployment. Without screen or tmux, your running process gets SIGHUP and dies. With a multiplexer, you reconnect and your session is exactly where you left it.
# Start a new named screen session screen -S deploy-session # Detach from screen (keep it running): Ctrl+A then d # List active screen sessions screen -ls # There is a screen on: 18293.deploy-session (Detached) # Reattach to a session by name screen -r deploy-session # Reattach if session is already attached (take over) screen -d -r deploy-session # Kill a screen session cleanly (from inside: Ctrl+A then :quit) screen -S deploy-session -X quit
Essential Key Bindings (Ctrl+A prefix)
Ctrl+A d = detach. Ctrl+A c = new window. Ctrl+A n = next window. Ctrl+A p = previous window. Ctrl+A " = list windows. Ctrl+A k = kill current window.
When to Use screen vs tmux
screen is available on virtually every Linux system, including minimal server installs. Use it when tmux is not installed and you cannot install packages. For new deployments where you control the environment, use tmux -- it is more capable.
Slide 14 of 35
tmux: The Modern Multiplexer
Sessions, windows, panes. Persistent workspaces for serious operations.
Sessions
The top-level container. A session persists independently of any terminal window. You can detach and reattach. Multiple sessions let you maintain separate work contexts (e.g., one for monitoring, one for deployments, one for log review).
Windows
Tabs within a session. Each window has its own shell. Switch between them instantly. Name them to stay organized. A window occupies the full terminal area and can be split into panes.
Panes
Splits within a window. Split horizontally or vertically. Each pane is an independent shell. Run a log monitor in one pane, a command prompt in another, a process viewer in a third -- all visible simultaneously.
# Install tmux (Ubuntu 22.04) apt-get install -y tmux # Start a new named session tmux new -s ops # Detach: Ctrl+B then d # List sessions tmux ls # Reattach to a session tmux attach -t ops # Create a new window: Ctrl+B then c # Switch to window 0: Ctrl+B then 0 # Split vertically: Ctrl+B then % # Split horizontally: Ctrl+B then "
Slide 15 of 35
tmux Key Bindings: Operational Reference
All bindings use the Ctrl+B prefix. Learn the essentials first, expand later.
Session Management
Ctrl+B d -- detach from session
Ctrl+B $ -- rename current session
Ctrl+B s -- list/switch sessions
Ctrl+B ( -- switch to previous session
Ctrl+B ) -- switch to next session
Window Management
Ctrl+B c -- create new window
Ctrl+B , -- rename current window
Ctrl+B n -- next window
Ctrl+B p -- previous window
Ctrl+B & -- kill current window
Pane Management
Ctrl+B % -- split vertically
Ctrl+B " -- split horizontally
Ctrl+B arrow -- move between panes
Ctrl+B z -- zoom/unzoom pane
Ctrl+B x -- kill current pane
Prefix Customization
The default prefix Ctrl+B conflicts with terminal scroll-back on some systems. Many admins remap it to Ctrl+A (screen-style) in ~/.tmux.conf: add set -g prefix C-a. This is a personal preference -- learn the default before remapping.
Slide 16 of 35
tmux: Scripted Session Layouts
Launch a complete multi-pane workspace with a single command.
#!/bin/bash # ops-layout.sh — Launch a 3-pane ops workspace in tmux SESSION="sector-ops" # Kill existing session if it exists, then create fresh tmux kill-session -t $SESSION 2>/dev/null tmux new-session -d -s $SESSION -n "monitor" # Window 1: split into three panes tmux send-keys -t $SESSION:0 'htop' Enter tmux split-window -h -t $SESSION:0 tmux send-keys -t $SESSION:0 'journalctl -f -u nginx' Enter tmux split-window -v -t $SESSION:0.1 tmux send-keys -t $SESSION:0 'watch -n 2 ss -tuln' Enter # Window 2: command prompt tmux new-window -t $SESSION -n "cmd" # Attach to the session tmux attach -t $SESSION
Operational Value
Check this script into your ops repository. Any team member who inherits your node can run one script and have a fully configured workspace -- same pane layout, same running monitors, same windows. Reproducible environments under pressure.
Slide 17 of 35
xargs — Building Commands from Input
Convert lines of stdin into arguments for another command. Bridges pipes and argument-based tools.
Why xargs Exists
Many commands (like rm, chmod, grep) accept arguments, not stdin. You cannot pipe a list of filenames directly to rm. xargs takes stdin lines and passes them as arguments to the specified command.
Use -0 for Safety
By default xargs splits on whitespace. A filename with a space (e.g., my file.txt) becomes two arguments. Use find -print0 paired with xargs -0 to use null bytes as delimiters instead. Always use this pattern in scripts.
# Basic: pipe filenames to rm via xargs find /tmp -name '*.tmp' | xargs rm -f # Safe: use null delimiter to handle filenames with spaces find /tmp -name '*.tmp' -print0 | xargs -0 rm -f # -P: run N processes in parallel (here: 4 simultaneous greps) find /var/log -name '*.log' -print0 | xargs -0 -P 4 grep -l 'CRITICAL' # -I {}: place each input item at a specific position in the command cat /tmp/host-list.txt | xargs -I{} ssh {} 'uptime' # -n 1: pass one argument at a time echo "alpha bravo charlie" | xargs -n 1 echo "Host:"
Slide 18 of 35
watch and Real-Time Monitoring
Repeat any command on an interval. Build live terminal dashboards without scripts.
# watch: run a command every N seconds and display the output watch -n 2 'ss -tuln' # refresh every 2 seconds watch -n 1 'ps aux --sort=-%cpu | head -10' # top 10 CPU consumers # -d: highlight differences between refreshes watch -d -n 2 'cat /proc/net/dev' # spot network counter changes # Practical: monitor disk I/O while a backup runs watch -n 1 'iostat -xz 1 1 | grep -v "^$"' # Combine watch with a pipeline for live log counting watch -n 5 'grep -c "ERROR" /var/log/app.log'
Useful watch Flags
-n N = interval in seconds (default 2). -d = highlight changes. -t = no header. -e = exit on non-zero. -g = exit when output changes (bash 3.2+). Ctrl+C to quit.
Alternatives
vmstat 2, iostat 2, and sar have built-in repeat intervals and produce more detailed resource data. For production monitoring, use these over watch. For quick one-liners, watch wins.
Slide 19 of 35
awk for System Administrators
Field extraction, pattern matching, and arithmetic in one tool. Indispensable for log and config parsing.
# awk: process text by fields, separated by whitespace by default # $1 = first field, $2 = second, $NF = last field, $0 = entire line # Extract PID and command name from ps aux ps aux | awk '{print $2, $11}' # Print lines where CPU usage (>column 3) exceeds 50% ps aux | awk '$3 > 50 {print $2, $3, $11}' # Change field separator: parse /etc/passwd (colon-delimited) awk -F: '{print $1, $3}' /etc/passwd # username, UID awk -F: '$3 >= 1000 {print $1}' /etc/passwd # human accounts (UID >= 1000) # Sum a column: total bytes received across all interfaces cat /proc/net/dev | awk 'NR > 2 {sum += $2} END {print "Total RX bytes:", sum}' # Print BEGIN and END blocks (run before/after processing) awk 'BEGIN {print "--- User Report ---"} $3 >= 1000 {print $1} END {print "---"}' -F: /etc/passwd
Pattern
The general awk form is: awk 'pattern { action }' file. Pattern is optional (match all lines). Action is optional (default: print the whole line). Combine them for precise control.
Slide 20 of 35
sed for Stream Editing
In-place substitution, line deletion, and address ranges for config file management.
# Basic substitution: s/pattern/replacement/flags sed 's/error/ERROR/g' /var/log/app.log # replace all occurrences, output to stdout # In-place edit: modify the file itself (always keep a backup) sed -i.bak 's/Listen 80/Listen 8080/g' /etc/apache2/ports.conf # Delete lines matching a pattern sed -i '/^#/d' /etc/fstab.tmp # delete comment lines sed -i '/^$/d' /etc/fstab.tmp # delete blank lines # Address range: edit only lines 10-20 sed -n '10,20p' /var/log/syslog # print lines 10-20 # Insert a line BEFORE a pattern match sed -i '/^ServerName/i # Managed by ops-config v2.1' /etc/apache2/sites-enabled/000-default.conf # Append a line AFTER a pattern match sed -i '/^GRUB_TIMEOUT=/a GRUB_TIMEOUT_STYLE=countdown' /etc/default/grub
Warning: Always Backup Before In-Place Edits
sed -i modifies the file directly. There is no undo. Use sed -i.bak to create an automatic backup with a .bak suffix before making changes. This has saved countless admins from restoring from actual backup.
Slide 21 of 35
Advanced grep
Context lines, recursive search, extended regex, and binary-safe operation.
# Context lines: show N lines before, after, or both sides of match grep -B 3 'CRITICAL' /var/log/app.log # 3 lines Before the match grep -A 5 'CRITICAL' /var/log/app.log # 5 lines After the match grep -C 2 'CRITICAL' /var/log/app.log # 2 lines each side (Context) # Extended regex (no backslash escaping needed) grep -E '(ERROR|WARN|CRIT)' /var/log/syslog # Recursive: search all files in a directory tree grep -r 'PermitRootLogin yes' /etc/ssh/ # Count matches per file grep -rc 'failed' /var/log/auth.log* # Print only the matching part of the line grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20 # Invert match: print lines that do NOT match grep -v 'DEBUG' /var/log/app.log | grep -v '^#'
Slide 22 of 35
sort, uniq, cut — Data Pipeline Tools
The three tools you combine most often after grep for structured output analysis.
# sort: alphabetic by default sort /etc/hosts sort -r /etc/hosts # reverse order sort -n /tmp/sizes.txt # numeric sort (not lexicographic) sort -k2 -n /tmp/data.txt # sort by field 2, numerically sort -t: -k3 -n /etc/passwd # sort by UID (field 3, colon-delimited) # uniq: remove adjacent duplicate lines (input MUST be sorted first) sort /tmp/ips.txt | uniq # unique IPs sort /tmp/ips.txt | uniq -c # count each unique line sort /tmp/ips.txt | uniq -d # only lines that appear more than once # cut: extract specific fields from delimited text cut -d: -f1 /etc/passwd # first field (username) cut -d: -f1,3 /etc/passwd # fields 1 and 3 (username, UID) cut -c1-20 /var/log/syslog # first 20 characters of each line # Classic pipeline: top 10 IP addresses hitting nginx awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
Slide 23 of 35
Bash Variable Expansion Tricks
Parameter expansion features that eliminate the need for external commands.
# Default value: use fallback if variable is unset or empty LOG_DIR=${LOG_DIR:-/var/log/sector} # use /var/log/sector if LOG_DIR is unset # Assign default and also set the variable : ${CONFIG_FILE:=/etc/sector/config.yml} # CONFIG_FILE is now set if it was empty # String manipulation without external tools FILENAME="sector-backup-2026-04-09.tar.gz" echo ${FILENAME%.tar.gz} # strip shortest suffix: sector-backup-2026-04-09 echo ${FILENAME#sector-} # strip prefix: backup-2026-04-09.tar.gz echo ${FILENAME^^} # uppercase: SECTOR-BACKUP-2026-04-09.TAR.GZ echo ${FILENAME,,} # lowercase # Substring extraction: ${var:offset:length} DATE="2026-04-09" echo ${DATE:0:4} # 2026 (year) echo ${DATE:5:2} # 04 (month) # Length of a variable's value echo ${#FILENAME} # 30
Slide 24 of 35
Here-Documents: Inline Multiline Input
Feed multiline content to commands without creating temp files. Critical for automation scripts.
# Basic heredoc: content between EOF markers goes to stdin cat <<EOF This is line one. This is line two. Hostname: $(hostname) Date: $(date +%F) EOF # Write a config file using heredoc cat > /etc/sector/worker.conf <<EOF listen_port = 9000 log_level = INFO max_workers = 4 node_id = $(hostname -s) EOF # Quoted delimiter: NO variable expansion inside (literal) cat <<'EOF' This ${var} will not expand. Neither will $(this command). Useful for generating scripts that contain bash syntax. EOF # SSH: run multiline commands on a remote host ssh node-b <<'EOF' apt-get update -qq apt-get install -y fail2ban systemctl enable fail2ban EOF
Slide 25 of 35
Command Substitution: $(command)
Capture the output of a command and use it as a value inline. Fundamental to scripting.
# $() runs the command and replaces itself with the output TODAY="$(date +%F)" echo "Backup started: $TODAY" # Backup started: 2026-04-09 # Capture IP address of an interface MY_IP="$(ip -4 addr show eth0 | awk '/inet /{print $2}' | cut -d/ -f1)" echo "Node IP: $MY_IP" # Use output in a loop for pid in $(pgrep python3); do echo "Found python3 PID: $pid" done # Nest substitutions for complex expressions DISK_FREE="$(df -h / | awk 'NR==2 {print $4}')" echo "Root filesystem free: $DISK_FREE" # Avoid old backtick syntax -- use $() instead # Old (avoid): version=`python3 --version` # New (use): version=$(python3 --version) # $() is readable, nestable, and unambiguous
Slide 26 of 35
trap — Signal Handling in Scripts
Define cleanup actions when your script receives a signal or exits unexpectedly.
#!/bin/bash # cleanup.sh — demonstrates trap for reliable resource cleanup # Create a temp directory for this script's work WORKDIR="$(mktemp -d /tmp/sector-work.XXXXXX)" echo "Working in: $WORKDIR" # trap: run cleanup() on EXIT, SIGINT, SIGTERM cleanup() { echo "[trap] Cleaning up $WORKDIR" rm -rf "$WORKDIR" echo "[trap] Done" } trap cleanup EXIT INT TERM # Do work cp /etc/passwd "$WORKDIR/passwd-snapshot" grep -E '^[a-z]' "$WORKDIR/passwd-snapshot" > "$WORKDIR/users.txt" cat "$WORKDIR/users.txt" # When the script exits (normally or via Ctrl+C), cleanup() runs automatically
Best Practice
Every script that creates temp files, locks, or holds resources should use trap cleanup EXIT. EXIT fires on normal termination AND on uncaught signals (depending on bash version). This prevents stale files and lock collisions in automated pipelines.
Slide 27 of 35
Bash Arrays in Pipelines
Store and iterate over collections in scripts without temporary files.
# Declare and populate an indexed array NODES=("web-01" "web-02" "db-01" "cache-01") # Access by index echo ${NODES[0]} # web-01 echo ${NODES[-1]} # cache-01 (last element) echo ${#NODES[@]} # 4 (count) # Iterate over all elements for node in "${NODES[@]}"; do echo "Checking: $node" ping -c 1 -W 1 "$node" >/dev/null 2>&1 && echo " UP" || echo " DOWN" done # Append to an array NODES+=("fw-01") # Build array from command output FAILED_SERVICES=($(systemctl list-units --state=failed --no-legend | awk '{print $1}')) echo "Failed services: ${#FAILED_SERVICES[@]}" for svc in "${FAILED_SERVICES[@]}"; do echo " - $svc" done
Slide 28 of 35
coproc — Coprocesses
Run a command in the background with bidirectional communication pipes.
What coproc Does
coproc launches a command as a background process and creates two file descriptors: one to write to its stdin, one to read from its stdout. This enables two-way communication without named pipes or temporary files.
When to Use It
When you need to send input to and read output from a long-running background process interactively in a script. Common with bc (calculator), python3 -i, or a database CLI. Avoids relaunching the process for every calculation.
# coproc launches bc as a coprocess named CALC coproc CALC (bc -l) # Send expression to bc's stdin via the write file descriptor echo "scale=4; 355/113" >&${CALC[1]} # Read bc's output from the read file descriptor read result <&${CALC[0]} echo "pi approximation: $result" # 3.1415 # Send another expression echo "sqrt(2)" >&${CALC[1]} read result <&${CALC[0]} echo "sqrt(2) = $result" # 1.41421356... # Terminate the coprocess kill "$CALC_PID"
Slide 29 of 35  |  Applied Pipeline
Applied Pipeline: Failed Auth Analysis
Identify brute-force sources from auth.log using everything covered so far.
You receive an alert: unusual SSH activity on the sector node. You need the top attacking IPs, the count per IP, and the usernames being tried -- in under 60 seconds, without a SIEM.
# Full auth.log pipeline -- run in a tmux window # Step 1: What are the top attacking IPs? grep 'Failed password' /var/log/auth.log | \ awk '{print $(NF-3)}' | \ sort | uniq -c | \ sort -rn | \ head -15 # Step 2: What usernames are being tried? grep 'Failed password' /var/log/auth.log | \ awk '{print $9}' | \ sort | uniq -c | \ sort -rn | \ head -10 # Step 3: Is it active right now? Watch in real-time tail -f /var/log/auth.log | grep --line-buffered 'Failed password'
Slide 30 of 35  |  Applied Pipeline
Applied Pipeline: Disk Hog Finder
Locate the largest files and directories on a system under time pressure.
# Find the 20 largest files in /var (where logs usually grow) find /var -type f -print0 | \ xargs -0 du -sh 2>/dev/null | \ sort -rh | \ head -20 # Disk usage summary by top-level directory under / du -sh //* 2>/dev/null | sort -rh | head -20 # Find files modified in the last 24 hours (new large files) find /var/log -mtime -1 -type f -print0 | \ xargs -0 du -sh 2>/dev/null | \ sort -rh # All of this in a tmux pane while running the backup in another pane
du flags
-s = summarize (one line per argument). -h = human-readable (GB, MB). -a = all files, not just directories. --max-depth=N = only descend N levels. -c = grand total at the end.
sort -h
sort -h sorts human-readable numbers correctly: 1K, 10M, 1G are ordered by magnitude. Without -h, lexicographic sort produces 10K < 1M < 2K which is wrong. Always use sort -h with du -h output.
Slide 31 of 35
Script Robustness: set -euo pipefail
Three options that turn a fragile script into one that fails loudly and safely.
set -e
Exit immediately on any non-zero exit code. Without this, your script silently continues after a failure. A script that keeps running after a failed mkdir or failed cp can cause serious damage on production systems.
set -u
Treat unset variables as an error. Without this, an unset $TARGET_DIR in rm -rf $TARGET_DIR/* expands to rm -rf /*. This has happened in production. set -u prevents it.
set -o pipefail
Without this, a pipeline's exit code is only the last command's code. false | true exits 0. With pipefail, the pipeline fails if any command in it fails. Catches silent failures in multi-command pipelines.
#!/bin/bash # Every production script starts with this set -euo pipefail # Now any unhandled failure exits the script immediately BACKUP_DIR="${BACKUP_DIR:-/var/backups/sector}" # safe default mkdir -p "$BACKUP_DIR" rsync -av /opt/sector/ "$BACKUP_DIR/" echo "Backup complete: $(date)" # only runs if rsync succeeded
Slide 32 of 35
Parallel Execution with wait
Launch multiple background jobs and block until all complete. Speeds up multi-host operations.
#!/bin/bash # Run SSH commands on multiple nodes in parallel, then collect results set -euo pipefail NODES=("web-01" "web-02" "db-01") PIDS=() # Launch one background job per node for node in "${NODES[@]}"; do ssh "$node" 'uptime && df -h /' > "/tmp/report-$node.txt" 2>&1 & PIDS+=("$!") # $! = PID of the last background job done # Wait for ALL background jobs to finish, check exit codes for pid in "${PIDS[@]}"; do wait "$pid" || echo "WARNING: PID $pid failed" done # Collect and display results for node in "${NODES[@]}"; do echo "=== $node ===" cat "/tmp/report-$node.txt" done
Slide 33 of 35
Profiling and Timing Commands
Measure what you build. A slow pipeline in a cron job is a ticking problem.
# time: measure wall clock, user, and system time time find / -name '*.log' -size +100M 2>/dev/null # real = wall clock elapsed # user = CPU time in user space # sys = CPU time in kernel (system calls) # time a full pipeline time (grep 'Failed' /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn) # Bash's built-in TIMEFORMAT for custom output TIMEFORMAT='Elapsed: %Rs' time { rsync --dry-run -av /opt/ /tmp/test/; } # strace: see every system call a command makes strace -c -p $(pgrep nginx | head -1) # profile a running process strace -e trace=network curl https://example.com # trace only network syscalls
Rule of Thumb
If a cron job takes more than 30 seconds, it needs to be profiled and likely optimized. A pipeline that reads a 500 MB log file on every run should instead tail only new entries. Use time before scheduling anything.
Slide 34 of 35  |  Lab Exercises
Practice Exercises
Complete these before leaving the lab. Real Ubuntu 22.04 system required.
1 Use process substitution to diff the output of getent passwd against /etc/passwd. Explain any differences you see.
2 Start a long-running command (e.g., sleep 300), suspend it with Ctrl+Z, send it to background with bg, verify with jobs, then disown it. Verify it survives shell exit with ps aux | grep sleep.
3 Write a pipeline that finds the top 5 users by number of processes currently running. Use ps, awk, sort, and uniq.
4 Create a tmux session named lab with two windows: one running htop, one with a shell. Detach, close the terminal, reattach, and verify both windows are intact.
5 Write a script using set -euo pipefail, trap for cleanup, and an array of at least 3 items. The script should process each item and write results to a temp file that is cleaned up on exit.
6 Extension: Write a parallel SSH runner that checks uptime on at least 2 hosts simultaneously using background jobs and wait. Time it with time and compare to sequential execution.
Slide 35 of 35  |  ALA-01
ALA-01 Summary: Key Takeaways
You can now operate at a level of CLI efficiency that most admins never reach. Pipes are your compositing tool. Process substitution eliminates temp files. Job control keeps your terminal free. tmux makes your session indestructible. These are not tricks -- they are professional standards.
1 Pipes connect stdout to stdin. They do NOT pass stderr -- use 2>&1 to merge before piping.
2 Process substitution <(cmd) passes command output as a pseudo-file. Requires bash or zsh -- not available in plain sh.
3 && runs only on success (exit 0). || runs only on failure. ; runs unconditionally. Exit code is in $?.
4 Subshells ( ) isolate changes. Command groups { } share the current shell's environment. Know which you need before writing deployment scripts.
5 Job control: & backgrounds, Ctrl+Z suspends, fg foregrounds, bg resumes in background, disown detaches from session.
6 tmux provides sessions (persistent), windows (tabs), and panes (splits). One script can launch your entire workspace. Use it for every remote session.
7 set -euo pipefail makes scripts fail loudly. trap cleanup EXIT ensures cleanup runs. Both are required in production scripts.
8 Use xargs -0 with find -print0 for safety with filenames containing spaces. Use -P N for parallel execution.