Why the shell matters
The terminal is the lowest-friction way to inspect and operate the systems you build. It is where you run tests, check logs, call APIs, manage Docker, examine Git diffs, and automate repeatable work.
A command line is powerful because it composes small tools. But it is literal: quote paths, read commands before running them, and test destructive operations on a narrow target first.
Navigation and files
pwd # current directory
ls -la # files, including hidden
cd /home/user/project # enter a directory
mkdir -p src/tests # create nested directories
cp source.txt copy.txt # copy
mv old-name new-name # move/rename
rm file.txt # delete one file
Use tab completion. Quote paths with spaces: cd "My Projects". Prefer rm -i when unsure; never casually run rm -rf against a variable/path you have not printed and checked.
Read and search code
less README.md # paged reader
head -n 30 file.txt # first lines
tail -n 100 service.log # recent lines
tail -f service.log # follow a log
rg "entity_id" src/ # fast code/content search
rg --files -g '*.py' # find files by pattern
Search before changing. Find a nearby working example in the same codebase and follow its conventions.
Pipes, redirects, and exit codes
command > output.txt # overwrite output file
command >> output.txt # append output file
command 2> errors.txt # redirect errors
command | jq . # pipe output to another tool
command; echo $? # print exit status (0 = success)
Use set -euo pipefail at the top of non-trivial Bash scripts:
#!/usr/bin/env bash
set -euo pipefail
It stops on failures, undefined variables, and failed pipeline segments rather than silently continuing.
Environment variables and secrets
export API_URL="http://localhost:8123"
export TOKEN="..." # do not put this in shell history if avoidable
printenv | sort
Use .env files with restricted permissions and .gitignore, or a secret manager/platform secret setting. Do not commit credentials, paste them to public issue trackers, or put them in command lines that other local users can inspect.
Processes and ports
ps aux | rg python
pgrep -af "astro|node"
ss -tulpn # listening TCP/UDP ports
lsof -i :4321 # who owns a specific port
kill PID # request graceful stop
A process can exist but still be unhealthy. Pair process inspection with logs and a real health/request check.
System and service logs
journalctl -u service-name -n 100 --no-pager
journalctl -u service-name -f
systemctl --user status service-name
Read the earliest relevant error in a cascade. Later errors are often consequences, not causes.
HTTP and JSON
curl -i https://example.com/health
curl -s https://api.example.com/status | jq .
curl -sS -X POST https://api.example.com/action \
-H 'Content-Type: application/json' \
-d '{"enabled":true}'
-i includes headers/status; -sS hides progress but shows errors. Use jq to inspect/transform JSON instead of visually parsing a single long line.
jq '.[] | select(.state == "on") | .entity_id' states.json
jq -r '.token // empty' config.json
Docker operations
docker compose ps
docker compose config
docker compose up -d
docker compose logs -f service-name
docker exec -it container-name sh
docker image ls
docker system df
Before updating: read the compose diff, back up stateful data, pull/apply one service at a time where possible, then check logs and an actual user-visible endpoint. docker compose config is an excellent preflight validator.
Python and Node project commands
# Python
source .venv/bin/activate
python -m pytest -q
ruff check .
# Node / Astro
npm ci
npm run build
npm run dev
Use the project’s declared package manager and lock file. Do not mix npm, yarn, and pnpm casually in one repository.
Write scripts as safe tools
#!/usr/bin/env bash
set -euo pipefail
readonly BACKUP_DIR="${HOME}/backups/app"
mkdir -p "$BACKUP_DIR"
if [[ "${1:-}" == "--dry-run" ]]; then
echo "Would back up to $BACKUP_DIR"
exit 0
fi
Good scripts validate arguments, quote variables ("$file"), print what they are doing, support dry-run for state-changing work, and exit non-zero on failure.
Debugging sequence
1. Reproduce the failure exactly.
2. Check the command’s exit code and stderr.
3. Inspect process/service/container logs.
4. Check configuration and environment variables.
5. Test the smallest dependency (DNS, port, API, file permission).
6. Change one thing, rerun the same test.
Commands to treat with care
| Command | Why it is risky | Safer habit |
|---|---|---|
rm -rf | Irreversible deletion | Print target; use narrow path/-i |
chmod -R | Can break all permissions | Apply to one path, inspect first |
| `curl | sh` | Executes unreviewed network content |
sudo | System-wide authority | Use the exact command only |
docker system prune | Deletes unused images/volumes | Inspect docker system df; avoid volumes unless intentional |
kill -9 | Prevents graceful cleanup | Try normal stop/kill first |