Terminal Mastery

The bar: you can drive any Unix box entirely from the keyboard — navigate, edit, search, chain, and automate — faster than you could ever do it with a mouse.

Every serious tool you will touch as an engineer ships a command-line interface. Git, npm, Prisma, Playwright, your cloud provider, your database — all of them speak terminal first and GUI second, if at all. The engineers who ship fast are the ones who stopped fearing the black window and started living in it.

This module is where you earn that fluency. Not memorizing commands for a quiz, but building the muscle memory to move through a filesystem, transform text, wire commands together, and automate the boring parts. Clear this and the rest of the stack stops feeling like magic and starts feeling like tools you own.

1.1 File System Navigation

Objective: move through any Unix filesystem with confidence and speed.

A Unix system is a single tree. Everything hangs off the root /: your work lives under /home, installed programs under /usr, system config under /etc, logs and variable data under /var. Knowing that map means you never wonder where a file "went" — there is one tree, and every path is a route through it.

The three verbs you run thousands of times a day are cd, pwd, and ls. Learn ls with flags cold: -la shows everything including permissions and dotfiles, -lh prints human-readable sizes, -R walks subdirectories. The dotfiles that -a reveals are not junk — they are where your shell, your git config, and your tools keep their settings.

The mistake self-taught engineers make is retyping full paths. Absolute paths start at / and always mean the same place; relative paths start from where you are. Tab completion finishes names for you, and history navigation with the up arrow or Ctrl-r recalls what you already ran. Typing less means fewer typos and faster work.

Drill: navigate with cd, pwd, ls -la, ls -lh, ls -R; reach the same file by both absolute and relative path; reveal dotfiles with -a; finish names with Tab and search history with Ctrl-r.

1.2 File Operations

Objective: create, modify, move, and delete files and directories without leaving the keyboard.

Making things is cheap: touch creates an empty file, mkdir makes a directory, and mkdir -p creates a whole nested path in one shot instead of one level at a time. Moving and copying use cp and mv, and both need -r to recurse into directories rather than choking on them.

Viewing is its own skill. cat dumps a whole file, less pages through it so you can scroll and search, head and tail show the top or bottom, and tail -f follows a file live — the single fastest way to watch a log while your app runs.

Finding files means find for real-time recursive search, locate for a fast indexed lookup, and which / whereis to answer "where does this command actually live." The one command that deserves genuine fear is rm -rf: it deletes recursively and without asking. There is no undo. Read every rm -rf twice before you hit enter, and never run one built from a variable you haven't checked.

Drill: touch, mkdir -p, cp -r, mv, rmdir, and rm (with real caution on rm -rf); view with cat, less, head, tail, and tail -f; locate things with find, locate, which, whereis.

1.3 Text Processing

Objective: search and transform text from the command line instead of by hand.

Most of an engineer's data is text — logs, config, source, CSV — and the terminal has sharp tools for slicing it. grep finds lines matching a pattern; grep -r searches a whole tree; grep -E unlocks extended regular expressions when a plain string isn't enough. This one command will save you more time than any IDE feature.

For changing text in a stream, sed does find-and-replace without opening an editor, and awk pulls columns out of structured data like log lines or delimited files. Pair them with sort to order lines and uniq to collapse duplicates (remember uniq only dedupes adjacent lines, so you almost always sort first).

Rounding it out: wc counts lines, words, and characters, and diff and comm compare files so you can see exactly what changed between two versions. The gotcha here is regex — beginners reach for it too early. Try a plain grep first, and only escalate to -E and full patterns when the simple search genuinely can't do the job.

Drill: grep, grep -r, grep -E; sed for substitution; awk for column extraction; sort, uniq, wc; compare with diff and comm.

1.4 Piping and Redirection

Objective: chain small commands into data pipelines that do real work.

The Unix philosophy is small tools that do one thing, glued together. That glue is three standard streams: stdin (input), stdout (normal output), and stderr (errors). Understanding that errors travel on a separate channel is what lets you handle them separately.

Redirection sends those streams to files: > overwrites, >> appends, 2> captures errors, and 2>&1 merges errors into the normal output stream. The pipe | feeds one command's output straight into the next — command1 | command2 — and this is where the terminal becomes powerful. A grep piped into sort piped into uniq -c answers real questions in one line.

Two more building blocks finish the toolkit. Command substitution $(command) drops one command's output into another as text. And xargs takes a list on stdin and turns it into arguments for a command that doesn't read stdin — the classic being find ... | xargs rm. The common trap is > versus >>: one silently destroys the file's contents, the other adds to it. Know which you meant.

Drill: redirect with >, >>, 2>, 2>&1; pipe with |; capture output with $(command); fan arguments out with xargs; describe stdin, stdout, and stderr from memory.

1.5 Shell Configuration

Objective: build and tune a zsh environment from an empty file.

Your shell reads startup files when it launches. On zsh the main one is .zshrc; .zprofile runs at login and .zshenv runs for every shell including scripts. Knowing which file runs when is how you stop guessing why a setting "didn't take." You will build a .zshrc from scratch in the lab, so treat it as yours to own.

The two highest-leverage customizations are environment variables and your PATH. export sets a variable, printenv lists what's set, and PATH is the ordered list of directories your shell searches for commands — order matters, because the first match wins. Get PATH wrong and the terminal "can't find" tools that are clearly installed.

After that comes ergonomics. Aliases (alias gs='git status') collapse commands you type constantly; shell functions handle logic an alias can't. You can customize your prompt through PROMPT (zsh's PS1) to show your branch and directory. Any change to .zshrc needs source ~/.zshrc to take effect in the current shell. Oh My Zsh is an optional framework that bundles themes and plugins — useful, but understand the raw config first so the framework never becomes a black box.

Drill: write a .zshrc from scratch; know when .zshrc, .zprofile, and .zshenv run; export and printenv; extend PATH in the right order; define aliases for git and navigation; write a shell function; customize PROMPT; reload with source ~/.zshrc.

1.6 Package Management

Objective: install and manage software across the systems you'll actually work on.

Every platform has a package manager, and refusing to learn them means installing things by hand and breaking them by hand. On Linux it's apt (Ubuntu/Debian); on macOS it's brew. Those handle system-level tools. Learn the install/update/remove verbs for whichever your machine runs.

At the project level you'll live in npm for Node and pip for Python. The single most important distinction is global versus local: an npm package installed globally is available everywhere, while a local install belongs to one project and is tracked in that project's package.json. Python's equivalent manifest is requirements.txt.

The gotcha that bites everyone is installing project dependencies globally, then wondering why a teammate can't reproduce your setup. Dependencies belong in the project manifest so anyone can recreate the exact environment. On Vets Who Code that discipline is enforced — the app is npm-only, and yarn, pnpm, and bun are blocked outright.

Drill: install/update/remove with apt and brew; npm install global vs local and read a package.json; pip install and read a requirements.txt.

1.7 Process Management

Objective: see what's running and take control of it.

When a dev server hangs or a port is stuck, you fix it by managing processes. ps gives a snapshot, top shows a live view, and htop (if installed) makes that view readable and interactive. These answer "what is running and what is it eating?"

Jobs and signals give you control. Appending & runs a command in the background; jobs, fg, and bg move work between foreground and background. To stop a process you send it a signal: kill asks politely, kill -9 forces it dead, and killall targets by name. Reach for -9 last, not first — a clean shutdown lets a program release its resources.

For monitoring and capacity, watch re-runs a command on an interval so you can observe change, df reports disk space, du shows what a directory is consuming, and free reports memory. The classic self-taught mistake is kill -9 on everything; learn the gentle signal first.

Drill: inspect with ps, top, htop; background and foreground with &, jobs, fg, bg; terminate with kill, kill -9, killall; monitor with watch; check resources with df, du, free.

1.8 Remote Operations

Objective: work on remote machines securely and keep sessions alive.

Sooner or later your code runs on a box you can't touch physically. ssh is how you get a secure shell on it. The professional way in is key-based auth: generate a keypair, keep the private key secret, put the public key on the server, and let ssh-agent hold the unlocked key so you aren't retyping a passphrase all day.

Moving files across that connection uses scp for simple copies and rsync for efficient syncing that only transfers what changed. When you connect to the same hosts repeatedly, an ~/.ssh/config file lets you define short aliases with the hostname, user, and key baked in — so ssh prod just works.

The piece beginners skip is session persistence. Close your laptop mid-SSH and your remote work dies with the connection. tmux (or the older screen) keeps a session running on the server so you can detach, disconnect, and reattach later exactly where you left off. On long-running jobs this isn't a nicety — it's the difference between finishing and starting over.

Drill: connect with ssh; generate and manage keys, load them into ssh-agent; transfer with scp and rsync; write an ~/.ssh/config entry; keep sessions alive with tmux or screen.

1.9 Shell Scripting Fundamentals

Objective: turn repeated command sequences into scripts that run themselves.

A shell script is just commands in a file, made repeatable. It starts with a shebang like #!/bin/zsh telling the system which interpreter to use, and it becomes runnable with chmod +x. The moment you catch yourself typing the same three commands twice, that's a script waiting to be written.

The language is small. Variables and parameter expansion ($1, ${VAR}) hold and reshape values. Conditionals with if, test, and [[ ]] let the script make decisions, and loops with for and while let it repeat over lists or until a condition changes. Functions group logic so a script stays readable instead of becoming one long wall.

What separates a script that works once from one you can trust is error handling. Every command returns an exit code — 0 for success, non-zero for failure — and good scripts check those codes and stop rather than charging ahead after something broke. Adding set -e to fail fast is a habit worth building early.

Drill: write a script with a shebang and chmod +x; use variables and $1-style parameter expansion; branch with if/test/[[ ]]; iterate with for and while; define functions; check exit codes and handle errors.

1.10 Hands-On Lab: Environment Setup & Automation

Objective: stand up your real development environment and back it with automation scripts you'll keep using.

What you ship

A working, personalized shell environment plus a small kit of automation:

  • A .zshrc written from scratch, with custom aliases for git, navigation, and other commands you run daily.
  • A PATH configured to include your development tools, in the right order.
  • A project scaffolding script that stamps out a new project's directory structure on demand.
  • A log analysis pipeline that chains grep, sort, uniq, awk, and friends to pull signal out of a log file.
  • A helper that automates the repetitive git operations you do every day.
  • A deployment helper script that wraps your ship steps behind one command.

On the VWC codebase

Everything here is what it takes to work in the Vets Who Code app the way the team does. The app runs on npm only — yarn, pnpm, and bun are blocked — so the package-management fluency from 1.6 is exactly the workflow you'll use, and your shell aliases will wrap the real npm run scripts (typecheck, lint with Biome, test with Jest, and Playwright end-to-end). The terminal isn't a detour on the way to shipping software that serves thousands of veterans. It is the cockpit you'll ship it from.