Cheatsheet

Linux and bash, grouped by what you are actually doing

Eighty commands a developer who deploys things needs on a server: moving files around, the quoting rules that bite everyone, text processing with grep and sed and awk, processes, networking, permissions, and the four lines that make a shell script safe to run unattended. Every entry is paste-ready.

Files and directories

The navigation and file-moving layer. If you only learn one thing here, learn find with -exec, because it replaces about six other tools.

Command What it does
ls -lah Long listing with human-readable sizes, including dotfiles.
ls -lt | head Newest files first. The fastest way to find what a deploy just wrote.
cd - Jump back to the previous directory. Toggles between two paths.
mkdir -p a/b/c Create nested directories and do not error if they already exist.
cp -a src/ dst/ Archive copy: recursive, preserves permissions, timestamps, and symlinks.
mv -n old new Move or rename, but never clobber an existing destination.
rm -rf ./build Delete a tree with no prompts. Always write the leading dot-slash.
ln -s /opt/app/current /srv/app Symlink. The classic atomic-deploy trick: build elsewhere, then flip the link.
readlink -f ./link Resolve a symlink chain to the real absolute path.
find . -name "*.log" -mtime +7 -delete Delete every log file older than seven days, recursively.
find . -type f -size +100M Locate the files that filled the disk.
find . -name "*.ts" -exec grep -l TODO {} + Run one command over the whole match set. The trailing plus batches arguments.
find . -print0 | xargs -0 -n1 basename Null-separated piping, the only form that survives spaces in filenames.
du -sh * | sort -h Size of every entry in this directory, smallest to largest.
df -h Free space per mounted filesystem. Check this before blaming the app.
df -i Free inodes. A disk can be "full" at 40 percent usage if inodes ran out.
tar -czf app.tar.gz app/ Create a gzipped archive of a directory.
tar -xzf app.tar.gz -C /srv Extract into a specific directory instead of the current one.
rsync -avz --delete src/ host:/dst/ Mirror a directory over SSH, removing files that no longer exist locally.
stat -c '%a %U %G %n' file Octal mode, owner, group, and name in one line.

Gotcha: rsync src/ dst/ and rsync src dst/ mean different things. With the trailing slash you copy the contents of src; without it you copy the directory itself, producing dst/src/. Add -n for a dry run any time --delete is in the command.

Quoting, variables, and parameter expansion

This is the section that separates a script that works on your laptop from one that survives a path with a space in it. Bash expands variables in double quotes and leaves them literal in single quotes, and almost every mysterious shell bug traces back to that one sentence.

Expression Result
"$var" Expanded, but kept as ONE word. Quote every variable unless you have a reason not to.
'$var' Literal. Single quotes suppress every expansion, including backslashes.
"${arr[@]}" Every array element as a separate correctly quoted word. Use this, never $*.
${var:-fallback} Use fallback when var is unset or empty. Does not assign.
${var:=fallback} Same, but also assigns the fallback back into var.
${var:?must be set} Abort with that message if var is unset. Cheapest possible input validation.
${var:+--flag} Emit the replacement only when var IS set. Great for optional CLI flags.
${#var} Length of the value in characters.
${var#prefix} Strip the shortest matching prefix. Double the hash for the longest.
${file%.tar.gz} Strip a suffix. Percent for shortest match, double percent for longest.
${var/old/new} Replace the first occurrence. Use a double slash to replace all of them.
${var^^} Uppercase the whole value. Two commas lowercase it.
${var:2:5} Substring: five characters starting at offset two.
$(command) Command substitution. Nests cleanly, unlike backticks.
$((3 * 7)) Integer arithmetic. Bash has no float math without bc or awk.
<(command) Process substitution: presents output as a file, so diff <(a) <(b) works.
export API_URL=https://x Put a variable into the environment of child processes.
NODE_ENV=test npm run x Set a variable for one command only, without polluting the shell.
set -a; . ./.env; set +a Load a dotenv file and export everything it defines.
printf '%s\n' "$var" Print exactly what is in the variable. Safer than echo with unknown input.

Gotcha: an unquoted $var is split on whitespace and then glob-expanded, in that order. A variable holding my file.txt becomes two arguments, and one holding * becomes your entire directory listing. Quote it.

Conditionals, loops, and functions

Bash control flow is small enough to fit on one screen. Use the double-bracket test form in bash scripts; it does not word-split, it supports pattern matching, and it has fewer sharp edges than the POSIX single bracket.

#!/usr/bin/env bash

# String and file tests
if [[ -z "$1" ]]; then echo "usage: deploy <env>" >&2; exit 64; fi
if [[ -f ./config.yml ]]; then echo "config found"; fi
if [[ -d /srv/app && -w /srv/app ]]; then echo "writable"; fi
if [[ "$branch" == release/* ]]; then echo "release branch"; fi
if [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "semver"; fi

# Numeric comparison
if (( count > 10 )); then echo "too many"; fi

# Multi-way
case "$1" in
  prod)      target=production ;;
  stage|dev) target="$1" ;;
  *)         echo "unknown env: $1" >&2; exit 64 ;;
esac

# Loops
for f in ./src/*.ts; do echo "checking $f"; done
for i in {1..5}; do echo "attempt $i"; done
while IFS= read -r line; do echo "[$line]"; done < ./hosts.txt
until curl -fsS http://localhost:3000/health; do sleep 2; done

# Functions: locals, arguments, return codes
retry() {
  local attempts="$1"; shift
  local n=0
  until "$@"; do
    n=$(( n + 1 ))
    (( n >= attempts )) && return 1
    sleep $(( n * 2 ))
  done
}
retry 5 curl -fsS https://api.example.com/health

Gotcha: while read in a pipeline runs in a subshell, so variables you set inside it are lost when the loop ends. Redirect from a file or use process substitution (while read ...; done < <(cmd)) when you need the values afterwards. Also set IFS= and pass -r or read will strip leading whitespace and eat backslashes.

Text processing with grep, sed, awk, and friends

Logs, CSVs, and config files. Reach for grep to select lines, cut or awk to select columns, sed to rewrite, and sort with uniq to count.

Command What it does
grep -rn "TODO" src/ Recursive search with file names and line numbers.
grep -i -w error app.log Case-insensitive whole-word match, so "errors" does not hit.
grep -v "^#" config.conf Invert the match: everything that is not a comment line.
grep -C3 "panic" app.log Three lines of context either side of every hit.
grep -E "5[0-9]{2}" access.log Extended regex without backslash soup. Handy for HTTP status hunting.
grep -c "" file Count matching lines. With an empty pattern, count every line.
sed -n '10,20p' file Print only lines 10 through 20.
sed 's/old/new/g' file Replace every occurrence on every line and print to stdout.
sed -i.bak 's/old/new/g' file Edit in place and keep the original as file.bak.
sed '/^$/d' file Delete blank lines.
awk '{print $1, $7}' access.log Print the first and seventh whitespace-separated fields.
awk -F, '$3 > 100 {print $1}' data.csv Comma-separated input, filtered on a numeric column.
awk '{s+=$2} END {print s}' f Sum a column. The END block runs once after the last record.
awk '!seen[$0]++' file Deduplicate lines while preserving original order.
cut -d: -f1 /etc/passwd First colon-delimited field of every line.
sort -u file Sort and drop duplicates in a single pass.
sort -k2 -n -r file Sort by the second field, numerically, descending.
sort file | uniq -c | sort -rn The top-N idiom: count occurrences and rank them.
tr -d '\r' < win.txt > unix.txt Strip carriage returns from a file that came off Windows.
tail -f -n100 app.log Last hundred lines, then follow as new ones arrive.
head -n5 file First five lines. Use a negative count to drop the last N instead.
wc -l < file Line count with no filename in the output, which is easier to parse.

Gotcha: sed -i is not portable. GNU sed accepts a bare -i; BSD sed on macOS demands a suffix argument, so sed -i '' 's/a/b/' there and sed -i 's/a/b/' on Linux. Writing -i.bak works on both. Regex syntax is covered in more depth in the regex cheatsheet.

Processes, jobs, and services

Command What it does
ps aux | grep [n]ode Every process, filtered. The bracket keeps grep from matching itself.
ps -eo pid,rss,comm --sort=-rss Processes ranked by resident memory. Finds the leak fast.
pgrep -af node PIDs plus full command lines, without the ps and grep dance.
kill -TERM 1234 Ask a process to shut down cleanly. This is the default signal.
kill -9 1234 SIGKILL. Cannot be trapped, so no cleanup runs. Last resort only.
pkill -f "node dist/server" Kill by full command line pattern rather than by PID.
jobs -l Background jobs of this shell, with their PIDs.
bg %1 / fg %1 Resume a stopped job in the background or the foreground.
nohup ./worker & disown Detach a process so it survives the terminal closing.
wait -n Block until any one background job finishes. Cheap parallelism control.
timeout 30s ./slow-task Kill a command that overruns. Exits 124 on timeout.
lsof -i :3000 Which process is holding the port your dev server wants.
systemctl status app.service State, PID, and the last log lines for a unit.
systemctl restart app.service Restart a service. Use reload when the unit supports it.
journalctl -u app -f --since "1h ago" Follow one unit's logs from the last hour.
free -h Memory in use. Read the "available" column, not "free".
uptime Load averages for 1, 5, and 15 minutes. Compare against core count.
nproc Number of available cores. Feed it to make -j or a worker count.

Gotcha: reaching for kill -9 first is how you end up with corrupt files and orphaned lock files. Send TERM, wait a few seconds, and only escalate if the process is genuinely wedged. Inside a container the same logic applies to PID 1 - see the Docker cheatsheet for signal handling in entrypoints.

Networking, curl, and DNS

Command What it does
curl -fsS https://api.example.com Fail on HTTP errors, stay silent, but still print real errors. The scripting default.
curl -I https://example.com Headers only. Fastest way to check a redirect or a cache header.
curl -L -o out.tar.gz URL Follow redirects and save to a named file.
curl -X POST -H 'Content-Type: application/json' -d '{"a":1}' URL JSON POST with an explicit content type.
curl -w '%{http_code} %{time_total}\n' -o /dev/null -s URL Status code and total time, discarding the body. A one-line probe.
curl --resolve app.test:443:127.0.0.1 https://app.test Test a vhost and its certificate before DNS points anywhere.
ss -tulpn Listening TCP and UDP sockets with the owning process. Replaces netstat.
ss -s Socket summary counts, including how many are stuck in TIME-WAIT.
dig +short example.com A Just the answer records, nothing else.
dig @1.1.1.1 example.com TXT Query a specific resolver, bypassing local caching entirely.
dig +trace example.com Walk delegation from the root. Shows which nameserver is actually answering.
openssl s_client -connect host:443 -servername host Inspect the TLS chain a server actually presents for a given SNI name.
ssh -i ~/.ssh/id_ed25519 user@host Connect with an explicit key instead of whatever the agent offers first.
ssh -L 5432:localhost:5432 user@host Tunnel a remote database to a local port without exposing it publicly.
scp -r ./dist user@host:/srv/app Copy a directory over SSH. Prefer rsync for anything repeated.
ip -br a Brief interface and address listing, one line per interface.

Gotcha: plain curl exits 0 on a 404 or a 500, because it successfully fetched a page that happened to be an error. In CI that turns a broken deploy into a green build. Always pass -f when a non-2xx should fail the script, and check the HTTP status code reference when you need to decide which codes count as failure.

Permissions and ownership

Command What it does
chmod 644 file Owner reads and writes, everyone else reads. The default for config files.
chmod 755 script.sh Executable by everyone, writable only by the owner. Scripts and directories.
chmod 600 ~/.ssh/id_ed25519 Owner only. SSH refuses to use a private key that is any looser.
chmod +x deploy.sh Add the execute bit without touching the read and write bits.
chmod -R u+rwX,go-w dir/ Capital X sets execute on directories only, never on plain files.
chown -R app:app /srv/app Recursively set owner and group.
chgrp www-data /var/www Change only the group, leaving the owner alone.
chmod g+s /srv/shared Setgid on a directory: new files inherit the directory's group.
umask 027 Mask for newly created files: group read, no access for others.
sudo -u app ./task Run one command as the service account rather than as root.
id app UID, GID, and every supplementary group for a user.
usermod -aG docker "$USER" Append a group. Omitting the -a wipes every other group the user had.

Gotcha: chmod 777 is never the fix. It is what people try when the real problem is ownership or a missing execute bit on a parent directory, and it leaves a hole behind long after the incident is closed. Check ownership with ls -ld on each directory in the path first.

Safe scripting habits

A bash script is a program that runs unattended as some privileged user on a machine you cannot see. These six habits stop the majority of the ways that goes wrong.

#!/usr/bin/env bash
set -euo pipefail          # exit on error, on unset variable, on any pipe stage failing
IFS=$'\n\t'                # stop word-splitting on spaces

# 1. Always know where you are, regardless of how the script was invoked
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$script_dir"

# 2. Clean up on ANY exit path, including a failure or a Ctrl-C
workdir="$(mktemp -d)"
cleanup() { rm -rf "$workdir"; }
trap cleanup EXIT

# 3. Report where a failure happened instead of dying silently
trap 'echo "failed at line $LINENO: $BASH_COMMAND" >&2' ERR

# 4. Validate inputs at the top, exit with a usage code
: "${DEPLOY_ENV:?DEPLOY_ENV is required}"
command -v rsync >/dev/null || { echo "rsync not installed" >&2; exit 127; }

# 5. Opt out of -e deliberately when a non-zero exit is expected
if ! diff -q old new >/dev/null; then
  echo "files differ"
fi

# 6. Support a dry run so the dangerous version is never the only version
run() { if [[ -n "${DRY_RUN:-}" ]]; then echo "+ $*"; else "$@"; fi; }
run rsync -az --delete ./dist/ "deploy@$HOST:/srv/app/"

What each set flag buys you

  • set -e stops the script on the first command that returns non-zero, so step four never runs against the wreckage of step three.
  • set -u turns a typo in a variable name into an immediate error instead of an empty string. This is the flag that prevents rm -rf "$PREFIX/" from deleting the root of the disk.
  • set -o pipefail makes a pipeline fail when any stage fails. Without it, false | tee log succeeds.
  • set -x echoes each command before running it. Turn it on temporarily when a script misbehaves in CI.

Habits that pay for themselves

  • Run ShellCheck in CI. It catches unquoted variables and useless subshells before a human reviews the diff.
  • Use mktemp -d instead of a fixed path in /tmp. Predictable temp paths are a real symlink-attack vector on shared hosts.
  • Write errors to stderr with >&2 so a caller can separate diagnostics from output.
  • Exit with meaningful codes: 0 success, 64 usage error, 127 missing dependency. CI systems and systemd both act on them.
  • Keep scripts idempotent. Running twice should be safe, because at some point it will happen.

Gotcha: set -e is quieter than people expect. It does not trigger inside a condition, on the left side of &&, or in a function whose result is being tested, so a failure there is swallowed. That is exactly why the ERR trap above earns its place.

Where these commands get used

Most of this shows up inside a pipeline or on a box you rent. The self-hosting guide covers the VPS setup these commands run against, and the CI/CD pipeline walkthrough shows the same scripts running in an automated build.

Adjacent references: the kubectl cheatsheet for clusters, the Terraform and OpenTofu cheatsheet for the infrastructure those servers run on, and the Git cheatsheet for the version control side. The full set lives in the DevOps section.