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.

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

CommandWhy it is riskySafer habit
rm -rfIrreversible deletionPrint target; use narrow path/-i
chmod -RCan break all permissionsApply to one path, inspect first
`curlsh`Executes unreviewed network content
sudoSystem-wide authorityUse the exact command only
docker system pruneDeletes unused images/volumesInspect docker system df; avoid volumes unless intentional
kill -9Prevents graceful cleanupTry normal stop/kill first