Just Enough Shell to Survive
A practical terminal tutorial for AI engineers

This tutorial assumes a Linux-like terminal — bash or zsh on macOS, Linux, GitHub Codespaces, or similar (see How to use this tutorial). That is where most AI tooling, containers, and CI pipelines expect you to work.
On Windows, things can look a little different: keyboard shortcuts, path separators (\ vs /), and some command names may not match. WSL (Windows Subsystem for Linux) or Git Bash are good ways to follow along with minimal friction. If you are using PowerShell or Command Prompt, treat this guide as conceptually useful, but expect differences in detail.
Introduction
If you build AI products, sooner or later you end up stitching together a small orchestra of tools:
- local scripts
- API calls
- logs
- config files
- containers
- CI pipelines
The shell is often the quickest way to make these components cooperate. This tutorial covers just enough shell to survive day-to-day AI engineering — navigation, safety habits, glue pipelines, and hands-on challenges — without pretending you need to become a terminal wizard.
Intended Audience
Engineers who are comfortable writing code but have little (or no) shell experience. If you are already fluent in shell, skip ahead to process control and service debugging.
What You’ll Need
How To Use This Tutorial
Pick how you want to follow along. Your choice in the tabs below filters the Try It panels and challenges in the rest of the tutorial — you only see exercises that match your environment. Switch tabs any time; no need to read warnings in every section about what works where.
Use the shell sandbox panel at the bottom of this page — a simulated bash environment that runs in your browser, not on your machine.
Getting started
- Click Show sandbox to expand the panel. It loads once and stays available as you scroll.
- Many sections include a challenge: click Start challenge to reset a clean environment, complete the task, then Check my work for automated feedback.
Good to know
- This is a learning sandbox, not a full Linux terminal — behaviour differs in places (permissions, processes, networking, keyboard shortcuts).
- Sessions are ephemeral — refreshing the page usually resets your environment.
- Do not paste secrets, tokens, private keys, or credentials.
Best for: navigation, pipes, grep, globs, quoting, glue workflows, and automated challenges.
Switch to Local or Codespaces for: Tab completion, Ctrl+R/C, line-editing shortcuts, background jobs, curl, your real shell config files, and anything that needs full host fidelity.
Your machine’s own shell — Terminal.app or iTerm on macOS, the default terminal on Linux, WSL or Git Bash on Windows, or similar.
Getting started
- Open your terminal application and follow the Try It steps in each section.
- Challenge Check my work buttons use the browser sandbox — here, self-check against the expected output in each exercise.
What you get
- Full keyboard support: Tab completion, Ctrl+R, modifier keys, and the rest of the shortcuts in the hints section.
- Real process IDs, signals, ports, and filesystem behaviour.
curl,jq, and outbound network access (when installed).
Platform notes
- This tutorial assumes a Linux-like shell (bash or zsh). See A note on platforms for Windows differences.
- Your config files (
~/.zshrc,~/.bashrc) and command history behave as on any normal system.
Best for
- Tab completion, modifier-key practice, Ctrl+R, process debugging (section 7), and API probes (section 8).
GitHub Codespaces is a cloud development environment — a Linux virtual machine with VS Code in your browser and a real terminal, hosted by GitHub.
Getting started
- Codespaces quickstart (opens in a new tab)
- Or open a repository on GitHub and launch a Codespace from the green Code button.
What you get
- The same full terminal capabilities as a local terminal tab — real Linux, full keyboard shortcuts, processes, ports, and network tools.
- A good option if you do not have a local shell set up yet, or you want Linux parity without installing WSL.
Best for
- Same as Local terminal when you want a real Linux shell without local setup.
GitHub Codespaces documentation (opens in a new tab).
A few words we reuse constantly
- Command — a program name you type at the prompt (e.g.
ls,grep) - Flag — an option after a command, usually starting with
-(e.g.ls -la) - Pipeline — commands chained with
|, where each command’s output feeds the next (covered properly in section 2) - Prompt — the line where you type, ending in
$(or%on some shells)
See the full Glossary at the end of this tutorial for every command and term with links back to where it is introduced.
Interactive shell sandbox
Click to expand. Starts in a few seconds.
Starting sandbox…
Hints and Tips
A handful of habits that pay off immediately. These are worth learning early, before the command vocabulary gets more involved.
Try It
Open the panel and build a little history, then walk back through it with the arrow keys:
- Run
pwd - Run
ls - Run
echo "history practice" - Run
ls -la
Now navigate without retyping:
- Press Up once — you should see
ls -laat the prompt - Press Up again —
echo "history practice" - Press Up again —
ls - Press Down twice — forward again to
echo "history practice", thenls -la - Press Enter to run whichever command is showing
Use Tab to complete matches
Tab completion is the shell’s way of finishing your typing for you. Start a command or path, press Tab, and the shell suggests the rest based on what exists in your current directory or what it recognises as a valid command name.
- Type the first few letters of a filename, then press Tab
- If there is only one match, the shell fills in the rest
- If there are several matches, press Tab again (or twice quickly) to see the options
This saves time and cuts down on typos — particularly handy for long paths like project/src/eval_metrics.py when you only need to type project/src/e and hit Tab.
Try It
Create a file with touch my-long-filename.txt, then type cat my-l and press Tab to complete the name before pressing Enter.
Jump around the command line with modifier keys
Once a command gets long, tapping the arrow key character-by-character is painful. On macOS, modifier keys let you move in bigger jumps:
- ⌘ + ← — jump to the start of the line
- ⌘ + → — jump to the end of the line
- ⌥ + ← — jump back one word at a time
- ⌥ + → — jump forward one word at a time
This is useful when you need to fix a typo in the middle of a pipeline without retyping the whole thing. Jump to the word, edit it, Enter.
On Linux terminals, the equivalents are usually Ctrl+A / Ctrl+E for the start and end of the line, and Alt + ← / Alt + → to move word by word.
Try It
Type a long command such as the one below — it uses grep (search lines), wc -l (count lines), and | (pipe output along). You do not need to memorise it yet; the goal is cursor practice:
terminal
grep ERROR app.log | grep -v retry | wc -lUse ⌘ + ← and ⌥ + → to move the cursor without retyping. On Linux, try Ctrl+A and Alt + → instead.
Search your command history
Pressing Up walks back one command at a time. That is fine for recent history, but less fine when you ran something useful twenty commands ago and only remember a fragment of it.
Ctrl+R starts a reverse search through your command history:
- Press Ctrl+R
- Type a few characters you remember (e.g.
grep,docker,curl) - The shell shows the most recent matching command
- Press Ctrl+R again to step to the next older match
- Press Enter to run it, or Esc / the arrow keys to edit first
This pays off in specific scenarios:
- You ran a long
curlordockercommand yesterday and need it again - You are debugging and want to rerun a
greppipeline with a small tweak - You know part of a command but not enough to retype the whole thing
If Ctrl+R is not available, you can list and filter history manually with the built-in history command:
terminal
history | grep grepTry It
If you have not done the arrow-key exercise above, run through steps 1–9 there first. With those four commands already in your history, try reverse search:
- Press Ctrl+R
- Type
echo— the shell should surfaceecho "history practice" - Press Ctrl+R again to step to any older match that contains
echo - Press Enter to run the command, or use the arrow keys to edit it first
Know your ~/.zshrc and history file
On macOS, your default shell is usually zsh. When you open a new terminal tab, zsh reads a config file in your home directory called ~/.zshrc (pronounced “zsh-R-C”). Think of it as startup instructions for your shell.
Typical things people store in ~/.zshrc:
- Aliases — shortcuts for commands you use often
exportlines — environment variables such asPATH(where the shell looks for programs)- Tool setup — many installers (Conda, nvm, Homebrew) ask you to paste a line into
~/.zshrcso their commands work in every new terminal
That last point is often why you need to configure it. A tool installs successfully, but the command is “not found” until you add its setup snippet to ~/.zshrc and open a fresh terminal (or run source ~/.zshrc to reload the file into your current shell).
Your command history lives in a separate file — usually ~/.zsh_history. That is how Ctrl+R and the Up arrow can find commands from previous days, not just the current session. The history file is the shell’s memory; ~/.zshrc is its personality.
On Linux with bash, the same ideas apply but the files are often named ~/.bashrc and ~/.bash_history instead.
Be careful what you put in ~/.zshrc. It runs automatically every time you open a terminal. Do not store secrets (API keys, passwords) here unless you understand the security implications — and never commit this file to a public repository.
Try It
On your local machine, run echo $SHELL to see which shell you are using, then ls -la ~/.zshrc ~/.zsh_history (or ~/.bashrc / ~/.bash_history on Linux). Open ~/.zshrc in your editor and see whether you recognise any aliases or export lines already there.
Stop a runaway command with Ctrl+C
Sometimes you paste the wrong thing, start a command that will not finish, or realise mid-run that you made a mistake. Ctrl+C sends an interrupt signal and stops the command at the current prompt.
- Press Ctrl+C once and wait a moment — most commands exit cleanly
- If nothing happens, press Ctrl+C again (some programs need a second nudge)
- You get a fresh prompt; nothing is “broken” — you can try again
This is the first reflex to build. It is faster and safer than closing the terminal window when something is hanging.
Try It
Run sleep 60, then press Ctrl+C before the minute is up. You should return to the prompt immediately.
terminal
sleep 60When things go wrong, read the error first
Error messages look intimidating, but they usually tell you which of a few common problems you hit. A quick scan saves a lot of guessing.
| Message | Likely cause | What to try |
|---|---|---|
command not found |
Typo, or the program is not installed / not on your PATH (the list of directories the shell searches for programs) |
Check spelling; run which name to locate a command; check install docs |
Permission denied |
You lack rights to read, write, or execute that file | ls -l the path (long listing shows permissions); chmod +x adds execute permission for scripts; check you own the file |
No such file or directory |
Wrong path, or you are in the wrong folder | pwd (print working directory); ls (list files); check for typos in the filename |
The shell is not scolding you — it is pointing at the mismatch between what you asked for and what exists on disk (or in your PATH).
Try It
Trigger each error on purpose and read the message before fixing it. Run these one at a time.
terminal
typo_commandterminal
cat secret.txtterminal
touch locked.txtterminal
chmod 000 locked.txtterminal
cat locked.txttouch creates an empty file. chmod changes permissions. Elsewhere in this tutorial, && chains commands so each step runs only if the previous one succeeded.
Quote paths and patterns carefully
The shell splits your command on spaces unless you tell it not to. Quotes control that — and they behave differently.
Before the shell runs a command, it often replaces shorthand in what you typed with the real value. That replacement step is called expansion. For example, if you write echo $HOME, the shell expands $HOME to your home directory path (such as /Users/you) and then runs echo with that path. Globs like *.txt expand to a list of matching filenames — you will meet those in section 4.
Quotes tell the shell how much expansion to allow:
- Single quotes
'...'— everything inside is literal. The shell does not expand anything; it passes the text exactly as you typed it. - Double quotes
"..."— variables like$HOMEstill expand; most other characters are literal.
Set a filename with a space:
terminal
FILE="my report.txt"With quotes, one file:
terminal
touch "$FILE"Without quotes, the shell splits on spaces — with FILE="my report.txt", touch $FILE tries to create my and report.txt as two separate files instead of one:
terminal
touch $FILEIf a path or filename might contain spaces, wrap it in double quotes. When you expand a variable in a path, wrap it in double quotes — "$TARGET" treats the whole value as one path even when it contains spaces. For grep patterns and quotes, see section 3.
-
Create a file whose name contains a space.
Show command
terminal
touch "notes draft.txt" -
Print the file’s contents to confirm it exists.
Show command
terminal
cat "notes draft.txt"
Environment variables and .env files
An environment variable is a named value the shell (and child programs) can read. You set one for the current session like this:
terminal
export MODEL_NAME=gpt-4o-mini
echo $MODEL_NAMEexportmakes the variable visible to commands you run from this shellecho $NAMEprints the current value (the$tells the shell to expand it)- Variables set this way last until you close the terminal tab
Putting export lines in ~/.zshrc (see above) makes them available every time you open a new terminal. That is fine for non-secret defaults like export EDITOR=code.
For API keys and tokens, prefer a .env file in your project root instead of hard-coding secrets in your shell config:
terminal
# .env — loaded by your app or tooling, not committed to git
OPENAI_API_KEY=sk-...
LOG_LEVEL=infoMost Python and Node AI stacks load .env automatically (via python-dotenv, dotenv, or framework conventions). Add .env to .gitignore so secrets never land in a public repository.
Never paste API keys into the browser sandbox, commit them to git, or store them in a public gist. Use .env locally and platform secret stores in CI (GitHub Actions secrets, etc.).
For a one-off command in your terminal, you can set a variable inline without touching a file. curl fetches a URL over HTTP (section 8); head prints the first few lines of output — handy when a response is long:
terminal
OPENAI_API_KEY=sk-your-key curl -s https://api.openai.com/v1/models | headThat sets the variable only for that single command — useful for quick tests, but .env is easier to maintain across a project.
Try It
Set a variable and read it back:
terminal
export DEMO_VAR=hello-shell
echo $DEMO_VAR
echo "Model would be: $DEMO_VAR"1) Shell Fundamentals
We’ll begin with a tiny command set that you will use repeatedly.
terminal
pwd # print working directory — where am I?
ls # list files and folders here
ls -la # -l long format; -a include hidden (dot) entries
mkdir tmp # create a folder called tmp here
ls # tmp should appear in this listing
cd tmp # change directory
touch visited.txt
cd - # jump back to previous directorymkdir creates a directory. touch creates an empty file (or updates a timestamp).
Start challenge seeds a hidden .sandbox_marker file — you will need ls -la in step 3 to spot it.
-
Print your current working directory.
Show command
terminal
pwd -
List files in the current directory (without hidden entries).
Show command
terminal
ls -
List all files, including hidden ones.
Show command
terminal
ls -la -
Create a folder named
tmp.Show command
terminal
mkdir tmp -
List files again and confirm
tmpappears.Show command
terminal
ls -
Change into
tmp.Show command
terminal
cd tmp -
Create an empty file named
visited.txtinsidetmp.Show command
terminal
touch visited.txt -
Return to the directory you were in before
cd tmp.Show command
terminal
cd -
Check yourself: Where were you before cd tmp? What hidden entries did ls -la show? Where did cd - take you?
2) Data Flow: Pipes and Redirection
The shell gets powerful when you connect simple tools together.
printf— print formatted text (likeecho, but handles newlines reliably)cat— print a file’s contents to the terminalgrep— print lines that match a pattern (more in section 3)
terminal
printf "apple\nbanana\napricot\n" > fruits.txt
cat fruits.txt
grep '^a' fruits.txt
grep '^a' fruits.txt | wc -lThe ^ in '^a' means “starts with” — only lines that begin with a match (apple, apricot, but not banana).
>writes output to a file (overwrites existing content).|passes output from one command into the next.wc -l— word count, lines only. Counts how many lines are in the input (a file or piped output). Useful when you want a total, not every matching line.
Only blueberry starts with b — that is the line your count should find.
-
Create
fruits.txtwith three lines:apple,apricot, andblueberry.Show command
terminal
printf '%s\n' apple apricot blueberry > fruits.txt -
Count how many fruits start with
band append that number tob-count.txt.Show command
terminal
grep '^b' fruits.txt | wc -l >> b-count.txt
3) Grep Chaining
grep searches text for lines matching a pattern. It is one of the best shell tools for quick investigations.
grep 'PATTERN' file— lines containingPATTERNgrep -v 'PATTERN'— v invert: lines that do not match (exclude noise)
Set up a practice log
Create app.log before the Try Its below:
terminal
printf "INFO startup\nWARN cache_miss\nERROR timeout\nERROR retry_failed\nERROR healthcheck ping\nlevel=warn cache_miss\nlevel=error oom\n" > app.logOn a local terminal, create it once and reuse it for every exercise in this section.
In the browser sandbox, files persist while you work — but Start challenge on any exercise wipes the environment and starts fresh. When you Start the grep-1 challenge later in this section, app.log is reloaded for you with the same content as above. Until then, run the printf yourself whenever you need the file.
See what you are working with:
terminal
cat app.logTry a few searches:
terminal
grep 'ERROR' app.logterminal
grep 'INFO' app.logYou should see every line that contains ERROR, and only the startup line for INFO.
Quotes and patterns
Single quotes keep the search pattern literal; double quotes let variables expand (see quoting if expansion is new).
Variable in the pattern — set the variable, then use double quotes:
terminal
export LOG_LEVEL=warn
grep "level=$LOG_LEVEL" app.logYou should see the line containing level=warn.
Chaining filters
Pipe grep commands together to narrow noisy logs:
terminal
grep 'ERROR' app.log | grep -v retry | grep -v healthcheckYou should see only ERROR timeout.
The ^ in patterns like '^a' means “starts with” (used in section 2).
This approach is useful for narrowing noisy logs quickly:
- broad match (
ERROR) - exclude known noise (
-v retry,-v healthcheck)
AI Engineer Use Case
When a local model service fails, this pattern helps isolate relevant errors before you disappear into a debugging rabbit hole.
Start challenge pre-loads app.log. If you are practising without Start, create it first with the printf from Set up a practice log.
-
Filter
app.logforERRORlines, excluding anything containingretryorhealthcheck, and save the result togrep-results.txt.Show command
terminal
grep 'ERROR' app.log | grep -v retry | grep -v healthcheck > grep-results.txt -
Display
grep-results.txtto verify the filter worked.Show command
terminal
cat grep-results.txt
You should see only ERROR timeout.
4) Globs and Brace Expansion
Globs
A glob is a wildcard pattern the shell uses to match filenames. Before a command runs, the shell expands the pattern into a list of matching files.
Common wildcards:
*— matches any characters (e.g.*.json→ all.jsonfiles)?— matches exactly one character (e.g.file?.txt→file1.txt,fileA.txt)
terminal
ls *.txt
ls project/src/*.pyIf you used echo rm ./logs/*.json earlier in the hints section, that * is a glob — echo lets you preview which files would be matched before you run rm (remove) for real.
Brace expansion
Brace expansion is a different trick — it generates text from a pattern in curly braces. It saves surprising amounts of time when scaffolding projects.
mkdir— create a directory;-pcreates parent folders too if missingls -R— list recursively (every subdirectory)
terminal
mkdir -p project/{data,src,tests,logs}
touch project/src/{ingest,embed,serve}.py
ls -R projectThis is great for scaffolding small prototypes quickly.
-
Create the
project/srcdirectory (create parent folders if needed).Show command
terminal
mkdir -p project/src -
Create
eval_metrics.pyandeval_runner.pyin one step using brace expansion.Show command
terminal
touch project/src/{eval_metrics,eval_runner}.py
5) Simple For Loops
A for loop runs the same commands once per item in a list. That is how you batch-process files, hit several endpoints, or rerun a check across a folder without copy-pasting.
Basic shape:
terminal
for name in item1 item2 item3; do
echo "Processing $name"
donefor ... in ...— the list can be words you type, or a glob the shell expandsdo/done— wrap the commands to repeat$name— the current item (quote it as"$name"when it might contain spaces)
Loop over files with a glob (see section 4). Here we use wc -l on each file directly — it prints how many lines that file contains (you met wc -l on piped output in section 2):
terminal
for f in *.txt; do
echo "=== $f ==="
wc -l "$f"
doneQuoting "$f" matters when filenames contain spaces — the same rule from the quoting hints.
AI Engineer Use Case
Got twelve eval output files and need the same grep on each? A loop beats running the command twelve times by hand:
terminal
for f in outputs/run_*.log; do
echo "== $f =="
grep 'ERROR' "$f"
doneThe || in step 2 means “if grep finds nothing, run the command on the right instead” — here, print (no errors).
-
Create a
logs/folder with three files:alpha.log(INFO only),beta.log(one ERROR line), andgamma.log(ERROR plus WARN).Show command
terminal
mkdir logs && printf 'INFO ok\n' > logs/alpha.log && printf 'ERROR timeout\n' > logs/beta.log && printf 'ERROR retry\nWARN slow\n' > logs/gamma.log -
Loop over every
.logfile inlogs/and print any line containingERROR. If a file has no errors, print(no errors)instead.Show command
terminal
for f in logs/*.log; do echo "== $f =="; grep 'ERROR' "$f" || echo "(no errors)"; done
You should see errors from beta.log and gamma.log, and (no errors) for alpha.log.
6) Executables and Permissions
A script file is not executable by default, even if it contains valid shell code.
cat > file <<'EOF'— write several lines to a file (a heredoc); text untilEOFis saved intofile./hello.sh— run a script in the current directory (./means “here”)ls -l— long listing; shows permission letters includingxfor executablechmod +x— add execute permission so you can run the script
terminal
cat > hello.sh <<'EOF'
#!/usr/bin/env bash
echo "Hello from shell"
EOF
ls -l hello.sh
./hello.shThe first line #!/usr/bin/env bash is a shebang — it tells the system which interpreter to use when you run ./hello.sh.
You should see a permission error before adding execute permission. That’s normal.
terminal
chmod +x hello.sh
ls -l hello.sh
./hello.shThis pattern matters when packaging helper scripts for teammates, automation, or CI jobs.
-
Create
hello.sh— a script that printsHello from shell.Show command
terminal
printf '%s\n' '#!/usr/bin/env bash' 'echo "Hello from shell"' > hello.sh -
Try to run the script. You should get Permission denied — it is not executable yet.
Show command
terminal
./hello.sh -
Check the file permissions with a long listing.
Show command
terminal
ls -l hello.sh -
Make the script executable.
Show command
terminal
chmod +x hello.sh -
Check permissions again.
Show command
terminal
ls -l hello.sh -
Run the script successfully.
Show command
terminal
./hello.sh
You should see Permission denied on the first run, then Hello from shell after chmod +x. Check my work verifies that behaviour — even if ls -l still shows -rw-r--r-- in the sandbox. On a local terminal or in Codespaces, ls -l updates to show x after chmod +x.
Use a DRY_RUN flag before you trust a script
Once you are writing scripts, you will often touch real files, call live APIs, or change application state — and hoping for the best is not a strategy.
A common pattern is a DRY_RUN flag. Instead of performing the action, the script prints what it would do. You inspect the output (on screen or in a log file). If it looks right, run again without the flag and trust the result.
Some familiar commands have a built-in safety switch. For example, rm (remove/delete) with the -i flag asks you to confirm before each delete:
terminal
rm -i notes.txtFor your own scripts, an environment variable keeps things simple:
terminal
# safe preview
DRY_RUN=1 ./cleanup_old_embeddings.sh
# real run, only after the preview looked correct
./cleanup_old_embeddings.shA minimal pattern inside a script might look like this. find searches the filesystem; -type f limits to files, -mtime +30 finds files older than 30 days, and -delete removes them (we only run that branch when DRY_RUN is not set):
terminal
if [ "$DRY_RUN" = "1" ]; then
echo "Would delete files older than 30 days in ./cache/"
else
find ./cache/ -type f -mtime +30 -delete
fiYou can redirect dry-run output to a log for a paper trail. >> appends output to a file (you will not see it on screen unless you open the log):
terminal
DRY_RUN=1 ./cleanup_old_embeddings.sh >> dry-run.logtee does both — it prints to the terminal and writes to the file at the same time. That is handy when you want to inspect the preview immediately while still keeping a record:
terminal
DRY_RUN=1 ./cleanup_old_embeddings.sh | tee dry-run.logUse >> when you only need the log. Use tee when you want to read the output and save it in one step.
This is especially wise when a command is destructive, expensive, or hard to undo — bulk deletes, deployment scripts, or anything that hits production data.
For quick one-liners, you do not always need a full script. echo prints text to the terminal — use it to preview a destructive command first, then run the real thing once you are satisfied:
terminal
# preview
echo rm ./logs/*.json
# enact
rm ./logs/*.jsonWith variables or wildcard patterns like *.json, echo shows you how the shell will expand things before anything irreversible happens:
terminal
TARGET="./cache/embeddings"
echo rm -rf "$TARGET"
# rm -rf "$TARGET"The same idea works inside pipelines. Swap the destructive command for echo temporarily:
terminal
find ./tmp -name "*.bak" -exec echo rm {} \;
# find ./tmp -name "*.bak" -exec rm {} \;If the echoed output looks right, rerun without echo and let the command do its job.
-
Create
dry_demo.sh. WhenDRY_RUN=1, it should print a preview message instead of doing anything destructive.Show command
terminal
printf '%s\n' '#!/usr/bin/env bash' 'if [ "$DRY_RUN" = "1" ]; then' ' echo "Would delete files older than 30 days in ./cache/"' 'fi' > dry_demo.sh -
Make the script executable.
Show command
terminal
chmod +x dry_demo.sh -
Run the script with
DRY_RUN=1to preview what it would do.Show command
terminal
DRY_RUN=1 ./dry_demo.sh -
Run the script without
DRY_RUN— it should do nothing visible.Show command
terminal
./dry_demo.sh -
Preview a destructive command safely with
echobefore you would run it for real.Show command
terminal
echo rm ./welcome.txt
7) Processes, Ports, and Signals
Now we’re in classic operations/debugging territory.
Start and inspect a process
sleep N— pause for N seconds (useful for testing)&at the end — run the command in the background so you get your prompt backjobs— list background tasks started from this shellps— list running processes;-efshows all processes in full format- PID — process identifier; a numeric handle you pass to
kill
terminal
sleep 300 &
jobs
ps -ef | grep sleep | grep -v grepThe second grep -v grep drops the grep process itself from the results — a common trick when searching for processes.
Stop a process gracefully
kill PID sends a polite shutdown signal (replace PID with the number from ps):
terminal
kill <PID>Force stop (last resort)
kill -9 PID forces immediate termination — no cleanup:
Use kill -9 sparingly. It bypasses graceful shutdown and can leave temporary state or partial writes behind.
Check which process is using a port
lsof — list open files; sockets count as files. -i :8000 shows what is bound to port 8000:
terminal
lsof -i :8000In real projects, this is often the fastest answer to: “Why won’t my service start on this port?”
Try It
Start a background job and find its process ID:
terminal
sleep 120 &
jobs
ps -ef | grep sleep | grep -v grepCopy the PID from the ps output (the number in the second column).
Stop it gracefully, then check it is gone:
terminal
kill <PID>
ps -ef | grep sleep | grep -v grepReplace <PID> with the number you copied. The second ps should return nothing.
8) Practical AI Glue Workflows
These small shell pipelines can save a lot of time during development. Work through them in order — each builds on files you created in earlier sections.
Filter noisy logs
terminal
cat app.log | grep 'ERROR' | grep -v 'healthcheck'Start challenge loads app.log.
-
Filter
app.logforERRORlines, exclude anything containinghealthcheck, and save the result tofiltered-errors.txt.Show command
terminal
grep 'ERROR' app.log | grep -v healthcheck > filtered-errors.txt -
Review
filtered-errors.txtto see what you captured.Show command
terminal
cat filtered-errors.txt
You should see ERROR oom and ERROR timeout — no healthcheck line.
Quick file inventory
grep '\.py$' — lines ending in .py ($ means end of line; \. is a literal dot). Useful for spotting Python files in a long listing:
terminal
ls -R project | grep '\.py$'Start challenge loads project/ with three Python files.
-
Find every
.pyfile underproject/and save the listing topython-inventory.txt.Show command
terminal
ls -R project | grep '\.py$' > python-inventory.txt -
Review
python-inventory.txt.Show command
terminal
cat python-inventory.txt
You should see at least three lines ending in .py.
Verify executable helpers
terminal
ls -l *.shStart challenge loads hello.sh (not executable yet).
-
Make
hello.shexecutable.Show command
terminal
chmod +x hello.sh -
Verify permissions with a long listing.
Show command
terminal
ls -l *.sh -
Run the script.
Show command
terminal
./hello.sh
On a local terminal or in Codespaces, ls -l would also show x in the permission string after chmod +x.
Probe an API with curl (and parse JSON with jq)
AI work involves a lot of HTTP — model APIs, embedding services, health checks. curl is the shell’s Swiss Army knife for sending requests and inspecting responses without opening Postman.
A minimal GET with the response body on stdout:
terminal
curl -s https://httpbin.org/get-s— silent mode (hides the progress meter so pipes stay clean)
When the response is JSON, jq filters and formats it — pass a path like .headers.Host to pull one field. Install jq locally if you do not have it (brew install jq on macOS).
terminal
curl -s https://httpbin.org/get | jq '.headers.Host'For authenticated APIs, pass the key from an environment variable (set in your shell or loaded from .env — never hard-code it in the command history). -H adds an HTTP header:
terminal
curl -s -H "Authorization: Bearer $OPENAI_API_KEY" \
https://api.openai.com/v1/models | jq '.data[].id'You can practise the jq part without network access — save a fake response and parse it locally:
-
Create
response.jsonwith sample API-style JSON.Show command
terminal
printf '{"choices":[{"message":{"content":"pong"}}]}\n' > response.json -
Inspect the file to see the raw JSON.
Show command
terminal
cat response.json -
Extract the
contentfield withjq.Show command
terminal
cat response.json | jq '.choices[0].message.content'
You should see "pong" (JSON strings include the quote marks in jq output).
On a local terminal or in Codespaces, probe a live URL with curl and jq:
terminal
curl -s https://httpbin.org/get | jq '.headers.Host'9) Extension: Run Advanced Commands Locally or in Codespaces
Process debugging, live API calls, and advanced keyboard shortcuts assume a full terminal. Select Local terminal or GitHub Codespaces at the top of this tutorial if you have not already.
10) Pre-Publish Checklist For Your Own Shell Docs
- Are all interactive commands safe and reversible?
- Did you clearly warn readers not to paste secrets?
- Did you distinguish sandbox behavior from real host behavior?
- Are advanced commands (
kill -9,lsof -i) explained with caution? - Does each section include a concrete “why this matters” use case?
Glossary
Quick reference for commands and terms in this tutorial. Each entry links to where it is introduced — use this when you meet something unfamiliar mid-challenge.
Concepts
- Background job — A command running while you keep your prompt (
sleep 300 &). - Brace expansion — Generate text from a pattern in curly braces (
project/{data,src}). - Command — A program name you type at the prompt (
ls,grep). - DRY_RUN — Preview pattern: run with a flag set so nothing destructive happens until you are sure.
- Environment variable — A named value programs can read (
export MODEL_NAME=...). - Flag — An option after a command, usually starting with
-(ls -la,grep -v). - For loop — Repeat commands for each item in a list (
for f in *.log; do ... done). - GitHub Codespaces — Cloud Linux dev environment with VS Code and a real terminal in the browser.
- Glob — Wildcard filename pattern (
*.json,logs/*.log). - Heredoc — Multi-line input into a file (
cat > file <<'EOF'). - PATH — Directories the shell searches when you type a command name.
- PID — Process identifier — numeric handle for
kill. - Pipeline — Commands chained with
|; output of one feeds the next. - Prompt — Where you type commands, usually ending in
$. - Shebang — First line of a script (
#!/usr/bin/env bash) telling the system which interpreter to use.
Commands
cat— Print a file’s contents.cd— Change directory;cd -returns to the previous one.chmod— Change permissions;chmod +xmakes a script executable.curl— Send HTTP requests from the terminal.echo— Print text (also used to preview destructive commands in DRY_RUN).export— Set an environment variable for child programs.find— Search the filesystem by name, type, age, etc.grep— Search lines matching a pattern;-vinverts (exclude matches).head— Print the first few lines of output.history— List commands you have run in this session.jobs— List background tasks from this shell.jq— Filter and format JSON (jq '.field').kill— Send a signal to a process;-9forces immediate stop.ls— List files;-llong format;-aincludes hidden entries;-Rrecursive.lsof— List open files;-i :PORTshows what is using a port.mkdir— Create a directory;-pcreates parent folders too.printf— Print formatted text (reliable newlines for building files).ps— List processes;-efshows all in full format.pwd— Print working directory (where am I?).rm— Remove files;-iasks before each delete.sleep— Pause for N seconds.source— Reload a config file into the current shell (source ~/.zshrc).tee— Write to a file and print to the terminal at the same time.touch— Create an empty file (or update timestamp).wc— Word count;-lcounts lines.which— Show path to a command executable.
Operators and syntax
&— Run a command in the background.&&— Run the next command only if the previous one succeeded.||— Run the next command only if the previous one failed.|(pipe) — Send one command’s output into the next.>/>>— Redirect output to a file (overwrite / append)../script.sh— Run an executable in the current directory.$VAR— Expand an environment variable.'...'/"..."— Single quotes: literal text. Double quotes: allow variable expansion.
Keyboard shortcuts
- Ctrl+C — Stop a running command.
- Ctrl+R — Reverse-search command history.
- Tab — Complete filenames and command names.
- Modifier keys — Jump by word or to the start/end of the line.
- Up / Down arrows — Step through previous commands.
Further reading
A short list of resources worth your time. Each one is widely respected, free, and does something this tutorial does not — deeper courses, correct bash habits, dense references, or tools you will use for years.
The Unix Shell (Software Carpentry) — The best structured follow-on from here. Clear lessons, exercises, and a gentle ramp from navigation through scripting. If you liked the hands-on style of this tutorial, start here next.
Bash Guide and Bash Pitfalls (Greg’s Wiki) — The bash community’s gold standard for doing things correctly. The guide teaches good habits; the pitfalls page is a catalogue of ways bash will surprise you. Essential once you start writing scripts that other people (or CI) will run.
The Art of Command Line — One dense page of practical CLI wisdom — file processing, debugging, one-liners, macOS and Windows notes. Legendary for a reason (160k+ GitHub stars). Keep it bookmarked and skim a section when you have five minutes.
explainshell.com — Paste any command; get a visual breakdown of every flag and argument against the man page text. Invaluable when you copy a
curl,find, ortarone-liner from Stack Overflow and want to know what it actually does before you run it.tldr pages — Practical examples for everyday commands (
tldr tar,tldr curl). The man page when you need the full spec; tldr when you need to remember the flags you always forget. Install once (brew install tldron macOS) and never leave home without it.ShellCheck — Static analysis for shell scripts. Paste a script or hook it into your editor; it catches quoting bugs, portability issues, and logic traps before they bite you in production. Non-negotiable once you are writing glue scripts for real.
Bite size bash (Julia Evans) — A short, illustrated zine on how bash actually works — quoting,
iftests, variables, and the gotchas experienced programmers still trip over. Her terminal deep-dives are excellent if you want to understand why the terminal feels inconsistent (spoiler: it is four different programs pretending to be one).
Wrap-up
Shell skills compound quickly. Start with navigation and pipes, add pattern matching and loops, then permissions and process control, and apply them to your own tool chain. Once these habits click, the shell becomes a dependable orchestration layer for AI engineering work. Keep the Glossary handy when you forget a flag or command name.