Just Enough Shell to Survive

A practical terminal tutorial for AI engineers

Tutorial
Shell
CLI
DevOps
AI Engineering
A practical shell tutorial for AI engineers — navigation, glue workflows, and interactive challenges. Just enough to get unstuck.
Author

Rich Leyshon

Published

June 20, 2026

A glowing cyberpunk Koopa shell hovering above a neon terminal

NoteA note on platforms

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.

Use arrow keys to navigate prior commands (work smart, not hard)

The shell remembers commands you have already run. You do not need to retype a long pipeline every time you want to tweak it.

  • Up arrow — step back through previous commands
  • Down arrow — step forward again through that history

This is especially useful when you are iterating on something fiddly: a grep search (you will meet it in section 3), a curl HTTP call (section 8), or a script you keep running with small edits. A pipeline chains commands with | — also coming in section 2. Run the command once, press Up, adjust a word or two, press Enter.

Try It

Open the panel and build a little history, then walk back through it with the arrow keys:

  1. Run pwd
  2. Run ls
  3. Run echo "history practice"
  4. Run ls -la

Now navigate without retyping:

  1. Press Up once — you should see ls -la at the prompt
  2. Press Up again — echo "history practice"
  3. Press Up again — ls
  4. Press Down twice — forward again to echo "history practice", then ls -la
  5. 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 -l

Use ⌘ + ← 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:

  1. Press Ctrl+R
  2. Type a few characters you remember (e.g. grep, docker, curl)
  3. The shell shows the most recent matching command
  4. Press Ctrl+R again to step to the next older match
  5. Press Enter to run it, or Esc / the arrow keys to edit first

This pays off in specific scenarios:

  • You ran a long curl or docker command yesterday and need it again
  • You are debugging and want to rerun a grep pipeline 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 grep

Try 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:

  1. Press Ctrl+R
  2. Type echo — the shell should surface echo "history practice"
  3. Press Ctrl+R again to step to any older match that contains echo
  4. 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
  • export lines — environment variables such as PATH (where the shell looks for programs)
  • Tool setup — many installers (Conda, nvm, Homebrew) ask you to paste a line into ~/.zshrc so 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 60

When 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_command
terminal
cat secret.txt
terminal
touch locked.txt
terminal
chmod 000 locked.txt
terminal
cat locked.txt

touch 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 $HOME still 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 $FILE

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

  1. Create a file whose name contains a space.

    Show command
    terminal
    touch "notes draft.txt"
  2. 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_NAME
  • export makes the variable visible to commands you run from this shell
  • echo $NAME prints 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=info

Most 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 | head

That 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 directory

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

  1. Print your current working directory.

    Show command
    terminal
    pwd
  2. List files in the current directory (without hidden entries).

    Show command
    terminal
    ls
  3. List all files, including hidden ones.

    Show command
    terminal
    ls -la
  4. Create a folder named tmp.

    Show command
    terminal
    mkdir tmp
  5. List files again and confirm tmp appears.

    Show command
    terminal
    ls
  6. Change into tmp.

    Show command
    terminal
    cd tmp
  7. Create an empty file named visited.txt inside tmp.

    Show command
    terminal
    touch visited.txt
  8. 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 (like echo, but handles newlines reliably)
  • cat — print a file’s contents to the terminal
  • grep — 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 -l

The ^ 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 -lword 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.

  1. Create fruits.txt with three lines: apple, apricot, and blueberry.

    Show command
    terminal
    printf '%s\n' apple apricot blueberry > fruits.txt
  2. Count how many fruits start with b and append that number to b-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 containing PATTERN
  • grep -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.log

On 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.log

Try a few searches:

terminal
grep 'ERROR' app.log
terminal
grep 'INFO' app.log

You 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.log

You 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 healthcheck

You 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:

  1. broad match (ERROR)
  2. 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.

  1. Filter app.log for ERROR lines, excluding anything containing retry or healthcheck, and save the result to grep-results.txt.

    Show command
    terminal
    grep 'ERROR' app.log | grep -v retry | grep -v healthcheck > grep-results.txt
  2. Display grep-results.txt to 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 .json files)
  • ? — matches exactly one character (e.g. file?.txtfile1.txt, fileA.txt)
terminal
ls *.txt
ls project/src/*.py

If 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; -p creates parent folders too if missing
  • ls -R — list recursively (every subdirectory)
terminal
mkdir -p project/{data,src,tests,logs}
touch project/src/{ingest,embed,serve}.py
ls -R project

This is great for scaffolding small prototypes quickly.

  1. Create the project/src directory (create parent folders if needed).

    Show command
    terminal
    mkdir -p project/src
  2. Create eval_metrics.py and eval_runner.py in 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"
done
  • for ... in ... — the list can be words you type, or a glob the shell expands
  • do / 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"
done

Quoting "$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"
done

The || in step 2 means “if grep finds nothing, run the command on the right instead” — here, print (no errors).

  1. Create a logs/ folder with three files: alpha.log (INFO only), beta.log (one ERROR line), and gamma.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
  2. Loop over every .log file in logs/ and print any line containing ERROR. 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 until EOF is saved into file
  • ./hello.sh — run a script in the current directory (./ means “here”)
  • ls -l — long listing; shows permission letters including x for executable
  • chmod +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.sh

The 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.sh

This pattern matters when packaging helper scripts for teammates, automation, or CI jobs.

  1. Create hello.sh — a script that prints Hello from shell.

    Show command
    terminal
    printf '%s\n' '#!/usr/bin/env bash' 'echo "Hello from shell"' > hello.sh
  2. Try to run the script. You should get Permission denied — it is not executable yet.

    Show command
    terminal
    ./hello.sh
  3. Check the file permissions with a long listing.

    Show command
    terminal
    ls -l hello.sh
  4. Make the script executable.

    Show command
    terminal
    chmod +x hello.sh
  5. Check permissions again.

    Show command
    terminal
    ls -l hello.sh
  6. 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.txt

For 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.sh

A 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
fi

You 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.log

tee 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.log

Use >> 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/*.json

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

  1. Create dry_demo.sh. When DRY_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
  2. Make the script executable.

    Show command
    terminal
    chmod +x dry_demo.sh
  3. Run the script with DRY_RUN=1 to preview what it would do.

    Show command
    terminal
    DRY_RUN=1 ./dry_demo.sh
  4. Run the script without DRY_RUN — it should do nothing visible.

    Show command
    terminal
    ./dry_demo.sh
  5. Preview a destructive command safely with echo before 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 back
  • jobs — list background tasks started from this shell
  • ps — list running processes; -ef shows all processes in full format
  • PIDprocess identifier; a numeric handle you pass to kill
terminal
sleep 300 &
jobs
ps -ef | grep sleep | grep -v grep

The 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:

Warning

Use kill -9 sparingly. It bypasses graceful shutdown and can leave temporary state or partial writes behind.

Check which process is using a port

lsoflist open files; sockets count as files. -i :8000 shows what is bound to port 8000:

terminal
lsof -i :8000

In 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 grep

Copy 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 grep

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

  1. Filter app.log for ERROR lines, exclude anything containing healthcheck, and save the result to filtered-errors.txt.

    Show command
    terminal
    grep 'ERROR' app.log | grep -v healthcheck > filtered-errors.txt
  2. Review filtered-errors.txt to 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.

  1. Find every .py file under project/ and save the listing to python-inventory.txt.

    Show command
    terminal
    ls -R project | grep '\.py$' > python-inventory.txt
  2. 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 *.sh

Start challenge loads hello.sh (not executable yet).

  1. Make hello.sh executable.

    Show command
    terminal
    chmod +x hello.sh
  2. Verify permissions with a long listing.

    Show command
    terminal
    ls -l *.sh
  3. 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:

  1. Create response.json with sample API-style JSON.

    Show command
    terminal
    printf '{"choices":[{"message":{"content":"pong"}}]}\n' > response.json
  2. Inspect the file to see the raw JSON.

    Show command
    terminal
    cat response.json
  3. Extract the content field with jq.

    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 +x makes 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; -v inverts (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; -9 forces immediate stop.
  • ls — List files; -l long format; -a includes hidden entries; -R recursive.
  • lsof — List open files; -i :PORT shows what is using a port.
  • mkdir — Create a directory; -p creates parent folders too.
  • printf — Print formatted text (reliable newlines for building files).
  • ps — List processes; -ef shows all in full format.
  • pwd — Print working directory (where am I?).
  • rm — Remove files; -i asks 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; -l counts 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, or tar one-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 tldr on 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, if tests, 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.