Two commands every engineer (and every AI coding agent) leans on: grep and curl
They're decades old. They're also the first thing a code model reaches for when it's working in your repo.
The shell commands worth knowing cold
Small, composable tools. Each does one job, reads text in, writes text out, and chains with a pipe.
grep -rn "TODO" src/ grep -ril "api_key" . grep -E "error|warn" app.log grep -c "GET /" access.log grep -v "^#" config.ini
curl -s https://api.example.com/v1
curl -I https://example.com
curl -o file.zip -L https://host/a.zip
curl -X POST -H "Content-Type: application/json" \
-d '{"q":"hi"}' https://api.example.com
Why these two
grep answers "where is this?" Hand it a string or a regex and it tears through a codebase in milliseconds. When an AI agent needs to find every call site before refactoring a function, it isn't reading files one by one — it's running grep -rn.
curl answers "what does this endpoint actually return?" Headers, status codes, a JSON body you can pipe straight into jq. When an agent is debugging an integration, curl is how it checks reality instead of guessing from the docs.
Why they fit code models so well
- Text in, text out. A model reads and writes text. Nothing to translate.
- Composable. One pipe turns five small tools into a one-line pipeline.
- Everywhere. Same behavior on a laptop, a container, or a CI runner.
- They fail loudly and specifically. Exit codes and stderr give the model something to reason about.
The lesson isn't "learn old tools." It's that the Unix philosophy — small programs, plain text, clear contracts — turned out to be the ideal interface for machines that think in language.
The full reference
Beyond the graphic: the flags and patterns that actually get used.
grep
| command | what it does |
|---|---|
| grep -rn "pattern" dir/ | recursive search with file:line prefixes — the default move |
| grep -ril "pattern" . | case-insensitive, print only matching filenames |
| grep -E "a|b" file | extended regex (alternation, +, ?, {n}) |
| grep -F "literal.string" file | fixed string, no regex — safer and faster for literals |
| grep -w "word" file | whole-word match only |
| grep -c "pattern" file | count matching lines |
| grep -v "pattern" file | invert: lines that do not match |
| grep -A3 -B1 "pattern" file | show 3 lines after, 1 before (-C for both) |
| grep -o "[0-9]\+" file | print only the matching part, one per line |
| grep --include="*.py" -rn "def " . | restrict recursion to certain file types |
| grep -rn "x" . --exclude-dir=node_modules | skip a directory |
| rg "pattern" | ripgrep: same idea, respects .gitignore, much faster on big trees |
curl
| command | what it does |
|---|---|
| curl -s URL | silent: no progress meter, just the body |
| curl -sS URL | silent but still show errors |
| curl -I URL | HEAD request — headers only |
| curl -i URL | include response headers with the body |
| curl -L URL | follow redirects |
| curl -o out.bin URL | save to a named file (-O keeps the remote name) |
| curl -X POST -d '{"a":1}' -H "Content-Type: application/json" URL | JSON POST |
| curl -H "Authorization: Bearer $TOKEN" URL | auth header |
| curl -u user:pass URL | basic auth |
| curl -F "file=@report.pdf" URL | multipart file upload |
| curl -w "%{http_code}\n" -o /dev/null -s URL | print only the status code |
| curl --retry 3 --max-time 10 URL | retries and a hard timeout |
| curl -v URL | verbose: TLS handshake, request and response headers |
Finding and reading files
| command | what it does |
|---|---|
| find . -name "*.log" | find by name (-iname for case-insensitive) |
| find . -type f -mtime -1 | files modified in the last day |
| find . -size +100M | files larger than 100 MB |
| find . -name "*.tmp" -delete | find and delete (run without -delete first) |
| ls -lah | long listing, hidden files, human sizes |
| head -n 20 file / tail -n 20 file | first / last 20 lines |
| tail -f app.log | follow a growing file |
| less +F app.log | follow, but with search and scrollback |
| wc -l file | line count (-w words, -c bytes) |
| cat a b > c | concatenate |
Transforming text
| command | what it does |
|---|---|
| sed 's/old/new/g' file | replace on every line (prints to stdout) |
| sed -i 's/old/new/g' file | replace in place (macOS: sed -i '' …) |
| sed -n '10,20p' file | print lines 10–20 |
| awk '{print $1}' file | first whitespace-separated column |
| awk -F, '{print $3}' file.csv | third comma-separated column |
| awk '$3 > 100' file | rows where column 3 exceeds 100 |
| cut -d: -f1 /etc/passwd | field 1 using : as delimiter |
| tr 'a-z' 'A-Z' | character translation |
| sort | uniq -c | sort -rn | frequency count, most common first |
| sort -t, -k2 -n file.csv | numeric sort on column 2 |
| jq '.items[] | {id, name}' data.json | reshape JSON |
| jq -r '.[].email' data.json | raw strings, no quotes |
| xargs -n1 cmd | run cmd once per input token |
| … | tee out.log | write to file and still print |
Processes, ports, disk
| command | what it does |
|---|---|
| ps aux | grep node | find a process |
| pgrep -f "python app.py" | PIDs matching a full command line |
| kill -9 PID | force kill (try plain kill first) |
| lsof -i :3000 | what's listening on port 3000 |
| ss -tulpn | all listening sockets with owning process |
| top / htop | live CPU and memory |
| df -h | disk free per filesystem |
| du -sh * | sort -h | size of each item here, sorted |
| nohup cmd & | keep running after you log out |
| cmd 2>&1 | tee run.log | capture stdout and stderr together |
Archives, permissions, misc
| command | what it does |
|---|---|
| tar -czf out.tgz dir/ | create a gzipped tarball |
| tar -xzf in.tgz | extract one |
| zip -r out.zip dir/ · unzip in.zip | zip equivalents |
| chmod +x script.sh | make executable |
| chown user:group file | change owner |
| ln -s target link | symbolic link |
| which cmd / type cmd | where a command resolves to |
| history | grep ssh | find that command you ran last week |
| !! | re-run the last command (sudo !! is the classic) |
| ctrl-r | reverse search your history |