Shell one-liners
Copy-pasteable one-line commands for files, processes, disk, network, DNS, systemd, Git, containers, Kubernetes, SSH, TLS, JSON and archives on a Linux host.
On this page
Cheatsheet#
| Task | Command |
|---|---|
| What is filling this disk | du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -20 |
| What is listening on which port | ss -tulpn |
| Who holds this port | ss -tlnp 'sport = :8080' |
| Files still open after deletion (space not freed) | lsof +L1 |
| Top memory consumers | ps -eo pid,rss,comm --sort=-rss | head |
| Follow the log of a unit | journalctl -u my-app -f |
| Boot errors | journalctl -b -p err |
| Find large recently changed files | find / -xdev -type f -size +100M -mtime -1 |
| Grep recursively, code only | grep -rnI --exclude-dir=.git 'pattern' . |
| Replace in files, in place | grep -rlZ 'old' . | xargs -0 sed -i 's/old/new/g' |
| HTTP status and timing | curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://example.com/ |
| Certificate expiry of a host | openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -enddate |
| Resolve through a specific server | dig +short @192.0.2.53 example.com A |
| Copy a key to a host | ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host |
| Run a command on many hosts | xargs -a hosts.txt -P8 -I{} ssh -o BatchMode=yes {} uptime |
| Pods not running | kubectl get pods -A --field-selector 'status.phase!=Running,status.phase!=Succeeded' |
| Container disk usage | docker system df -v |
| Pretty-print and query JSON | curl -s https://api.example.com/v1 | jq '.items[] | .name' |
| Tar a directory, excluding junk | tar --exclude='.git' --exclude='node_modules' -czf app.tgz app/ |
| Epoch to date | date -d @1727136000 |
Everything here assumes GNU coreutils, util-linux, iproute2 and Bash 5 on a Linux host. macOS and BSD differences are covered under Troubleshooting. For the shell constructs themselves see Bash; for complete scripts built from these pieces see Operations scripts.
Files and text#
# Largest directories one level down, staying on this filesystem (-x)
du -xh --max-depth=1 /var 2>/dev/null | sort -rh | head -20
# Largest 20 files under a path
find /var/log -xdev -type f -printf '%s\t%p\n' | sort -rn | head -20 | numfmt --field=1 --to=iec
# Files modified in the last 24 hours, newest first
find . -type f -mmin -1440 -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort -r
# Files not accessed in 90 days, with their size
find /data -type f -atime +90 -printf '%s %p\n' | sort -rn | head
# Count files per directory, one level down
for d in */; do printf '%8d %s\n' "$(find "$d" -type f | wc -l)" "$d"; done | sort -rn
# Delete files older than 30 days; run without -delete first to see the list
find /var/tmp/my-app -type f -mtime +30 -print
find /var/tmp/my-app -type f -mtime +30 -delete# Recursive grep, skip binaries and VCS metadata, show line numbers
grep -rnI --exclude-dir={.git,node_modules,vendor} 'TODO' .
# Files containing a pattern, one name per line
grep -rlZ 'deprecated' src/ | xargs -0 ls -l
# Lines in a that are not in b (both must be sorted)
comm -23 <(sort a.txt) <(sort b.txt)
# Deduplicate without sorting, keeping first occurrence
awk '!seen[$0]++' file
# Most frequent lines
sort file | uniq -c | sort -rn | head
# Print lines between two markers, inclusive
sed -n '/^BEGIN/,/^END/p' file
# Print a line range
sed -n '120,140p' file
# Replace in place, keeping a backup (GNU sed)
sed -i.bak 's/old/new/g' config.ini
# Replace across many files, only those that match
grep -rl --exclude-dir=.git 'registry.example.com' . | xargs sed -i 's#registry.example.com#registry.example.net#g'
# Columns 1 and 3 of a colon-separated file
cut -d: -f1,3 /etc/passwd
# Sum a column, average a column
awk '{s += $3} END {print s}' file
awk '{s += $3} END {if (NR) print s / NR}' file
# Strip trailing whitespace and CRLF
sed -i 's/[[:space:]]*$//' file
# Convert tabs to spaces
expand -t 4 file > file.new
# Show non-printable characters and line endings
cat -A file | head
# Rename *.jpeg to *.jpg
for f in *.jpeg; do mv -- "$f" "${f%.jpeg}.jpg"; done
# Split a file into 100 MB chunks and rejoin it
split -b 100M big.bin big.bin.part_ && cat big.bin.part_* > big.bin.joined
# Checksums for a tree, and verify them later
find . -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS
sha256sum -c SHA256SUMS --quiet
# Watch a file for changes and rerun a command (inotify-tools)
while inotifywait -qe close_write config.yaml; do systemctl reload my-app; done
# Find world-writable files and setuid binaries
find / -xdev -type f -perm -0002 2>/dev/null
find / -xdev -type f -perm -4000 2>/dev/nullProcesses#
# Top CPU and memory users
ps -eo pid,ppid,user,%cpu,%mem,rss,etime,comm --sort=-%cpu | head
ps -eo pid,rss,comm --sort=-rss | head | numfmt --header --field=2 --from-unit=1024 --to=iec
# Process tree of one service
pstree -ap "$(systemctl show -p MainPID --value my-app)"
# Full command line of a process, NUL-separated args made readable
tr '\0' ' ' < /proc/1234/cmdline; echo
# Environment of a running process (needs the same user or root)
tr '\0' '\n' < /proc/1234/environ
# What a process has open, what it is waiting on
ls -l /proc/1234/fd | wc -l
cat /proc/1234/wchan; echo
cat /proc/1234/status | grep -E 'State|Threads|VmRSS|voluntary_ctxt'
# Processes in uninterruptible sleep (usually stuck on I/O)
ps -eo pid,stat,wchan:32,comm | awk '$2 ~ /D/'
# Zombies and their parents
ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/'
# Kill every process matching a name, politely then firmly
pkill -TERM -f 'my-app --worker'; sleep 5; pkill -KILL -f 'my-app --worker'
# Processes started by a user, oldest first
ps -u deploy -o pid,lstart,comm --sort=start_time
# Count threads per process
ps -eo nlwp,pid,comm --sort=-nlwp | head
# Run a command with lower priority and limited I/O
nice -n 19 ionice -c3 tar -czf backup.tgz /data
# Timeout a command, kill it 10 seconds after the TERM if it ignores it
timeout -k 10s 5m ./long-job.sh
# Trace system calls of a running process, summarise
strace -c -p 1234 -f
strace -p 1234 -e trace=network -s 200
# Watch a command's output every two seconds, highlighting changes
watch -d -n2 'ss -s'Disk and memory#
# Filesystems, human-readable, only real ones
df -hT -x tmpfs -x devtmpfs -x overlay
# Inodes exhausted (df says space is free but writes fail)
df -i
# Deleted files still held open, the usual cause of "df disagrees with du"
lsof +L1 | awk 'NR == 1 || $7 > 1048576'
# Block devices with filesystem and mountpoint
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,LABEL,UUID
# Memory in one line, and the detailed view
free -h
grep -E 'MemTotal|MemAvailable|SwapFree|Dirty|Committed_AS' /proc/meminfo
# Recent OOM kills
journalctl -k -g 'Out of memory' --since '24 hours ago'
dmesg -T | grep -i 'killed process'
# Per-process swap usage, largest first
for p in /proc/[0-9]*; do awk -v p="${p##*/}" '/VmSwap/ {if ($2 > 0) print $2, p}' "$p/status" 2>/dev/null; done | sort -rn | head
# I/O load per device and per process
iostat -xz 1 5
pidstat -d 1 5
# SMART health of a disk (smartmontools)
smartctl -H /dev/sda; smartctl -A /dev/sda | grep -E 'Reallocated|Pending|Uncorrectable|Power_On'
# Drop page cache to measure cold reads (does not free application memory)
sync && echo 3 > /proc/sys/vm/drop_caches
# Test sequential write and read throughput
dd if=/dev/zero of=/data/test.bin bs=1M count=1024 oflag=direct status=progress && rm /data/test.bin
# Grow a filesystem after extending the volume (xfs, ext4)
xfs_growfs /data
resize2fs /dev/mapper/vg-data
# Space per user under /home
du -sh /home/* 2>/dev/null | sort -rhNetwork and ports#
# Listening sockets with the owning process
ss -tulpn
# Established connections by remote address, most first
ss -tn state established | awk 'NR > 1 {split($5, a, ":"); print a[1]}' | sort | uniq -c | sort -rn | head
# Connections by state
ss -tan | awk 'NR > 1 {print $1}' | sort | uniq -c | sort -rn
# Who is connected to port 5432
ss -tn 'sport = :5432'
# Addresses, routes, default gateway
ip -br addr
ip route
ip route get 192.0.2.10 # which interface and source address reach a destination
# Test a TCP port without netcat (Bash /dev/tcp)
timeout 3 bash -c 'echo > /dev/tcp/192.0.2.10/443' && echo open || echo closed
# Test a port with nc, TCP and UDP
nc -zv -w3 192.0.2.10 443
nc -zuv -w3 192.0.2.10 53
# Scan a range for one open port (nmap)
nmap -p 22 --open 192.0.2.0/24 -oG - | awk '/22\/open/ {print $2}'
# Packet capture for one host and port, readable, first 100 packets
tcpdump -ni eth0 -c 100 host 192.0.2.10 and port 443
# Capture to a file for later analysis
tcpdump -ni eth0 -w /var/tmp/capture.pcap port 53
# Interface counters and errors
ip -s link show eth0
cat /proc/net/dev
# Bandwidth per connection (iftop) and per process (nethogs)
iftop -ni eth0
nethogs eth0
# Path and per-hop loss
mtr -rwzc 50 example.com
# ARP/neighbour table
ip neigh
# Firewall rules on a Fedora or RHEL host
firewall-cmd --list-all
nft list ruleset
# Conntrack table size and usage
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_maxDNS and HTTP#
# Short answer, specific record, specific server
dig +short example.com A
dig +short example.com MX
dig +short @192.0.2.53 example.com AAAA
# Reverse lookup
dig +short -x 192.0.2.10
# Follow the delegation from the root
dig +trace example.com
# Who is authoritative, and the SOA serial each server holds
dig +short example.com NS
for ns in $(dig +short example.com NS); do printf '%s ' "$ns"; dig +short @"$ns" example.com SOA | awk '{print $3}'; done
# Which resolver this host actually uses (systemd-resolved)
resolvectl status | grep -E 'Current DNS|DNS Servers'
# TTL remaining on a cached answer
dig example.com A | awk '$4 == "A" {print $1, $2}'# Status code only
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/
# Timing breakdown: DNS, connect, TLS, first byte, total
curl -sS -o /dev/null -w 'dns %{time_namelookup} connect %{time_connect} tls %{time_appconnect} ttfb %{time_starttransfer} total %{time_total}\n' https://example.com/
# Headers only, following redirects
curl -sSIL https://example.com/
# Force a hostname to an IP without touching /etc/hosts (test a new backend)
curl -sS --resolve example.com:443:192.0.2.10 https://example.com/
# POST JSON with a bearer token read from the environment
curl -sS -H "Authorization: Bearer $API_TOKEN" -H 'Content-Type: application/json' -d @payload.json https://api.example.com/v1/items
# Retry on transient failures, fail on HTTP errors, with a total time limit
curl -sS --fail --retry 5 --retry-all-errors --retry-delay 2 --max-time 60 https://api.example.com/health
# Download resuming a partial file
curl -sS -C - -O https://example.com/big.iso
# Every redirect hop
curl -sSIL -o /dev/null -w '%{url_effective} %{http_code}\n' https://example.com/old
# Download a whole directory listing recursively (wget)
wget -r -np -nH --cut-dirs=1 -R 'index.html*' https://example.com/files/
# Serve the current directory on port 8000 for a quick transfer
python3 -m http.server 8000 --bind 127.0.0.1Users and sessions#
# Who is logged in, from where, doing what
w
who -a
# Last logins and failed logins
last -n 20
lastb -n 20 # needs root
# Users with a login shell
awk -F: '$7 !~ /(nologin|false)$/ {print $1, $3, $7}' /etc/passwd
# Accounts with UID 0 (should be root only)
awk -F: '$3 == 0' /etc/passwd
# Password ageing and lock status of a user
chage -l deploy
passwd -S deploy
# Groups of a user, members of a group
id deploy
getent group wheel
# Sudo rules that apply to a user
sudo -l -U deploy
# Every authorised SSH key on the box, by user
for h in /home/* /root; do [ -s "$h/.ssh/authorized_keys" ] && { echo "== $h"; cut -d' ' -f1,3- "$h/.ssh/authorized_keys"; }; done
# Sessions and their processes (logind)
loginctl list-sessions
loginctl session-status 42
# Lock an account and expire its sessions
usermod -L deploy && loginctl terminate-user deploy
# Recent sudo usage
journalctl _COMM=sudo --since todaysystemd and journal#
# Units that failed, and why
systemctl --failed
systemctl status my-app --no-pager -l
# Every enabled unit, and what is running now
systemctl list-unit-files --state=enabled
systemctl list-units --type=service --state=running
# Timers and their next run
systemctl list-timers --all
# Log of one unit since the last boot, then follow it
journalctl -u my-app -b
journalctl -u my-app -f
# Errors and worse since yesterday, any unit
journalctl -p err --since yesterday
# Kernel messages this boot
journalctl -k -b
# Logs from the previous boot (after a crash)
journalctl -b -1 -p warning
# Journal disk usage and cleanup
journalctl --disk-usage
journalctl --vacuum-time=14d
# JSON output for one unit, ready for jq
journalctl -u my-app -o json --since '1 hour ago' | jq -r '.MESSAGE'
# Show the effective unit file with overrides applied
systemctl cat my-app
# One property, script-friendly
systemctl show -p ActiveState,SubState,MainPID,NRestarts --value my-app
# How long each unit took at boot
systemd-analyze blame | head -20
systemd-analyze critical-chain
# Verify a unit file before enabling it
systemd-analyze verify /etc/systemd/system/my-app.service
# Restart and watch it come up
systemctl restart my-app && journalctl -u my-app -f -n 50
# Resource usage per unit (cgroups)
systemd-cgtop --order=memory -n1Git#
# Compact log with graph
git log --oneline --graph --decorate -20
# What changed in the last week, by author
git log --since='1 week ago' --format='%h %an %s'
git shortlog -sn --since='1 month ago'
# Which commit touched a line (with moves and copies detected)
git blame -C -C -L 40,60 path/to/file
# Find the commit that introduced or removed a string
git log -S 'function_name' --oneline -- path/
# Files changed between two refs
git diff --stat main..feature
git diff --name-only HEAD~5
# Branches merged into main that can be deleted
git branch --merged main | grep -vE '^\*|main$'
# Delete local branches whose upstream is gone
git fetch -p && git branch -vv | awk '/: gone]/ {print $1}' | xargs -r git branch -D
# Recover a commit you lost
git reflog | head -20
# Biggest objects in the repository
git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | awk '$1 == "blob"' | sort -k3 -rn | head | numfmt --field=3 --to=iec
# Untracked files that would be removed by clean (dry run), then remove them
git clean -nd
git clean -fd # deletes untracked files and directories
# Show a file at a revision
git show main:path/to/file
# Pull every repository one level down
for d in */.git; do (cd "${d%/.git}" && printf '%s: ' "$PWD" && git pull --ff-only 2>&1 | tail -1); done
# Current branch name, and whether the tree is dirty
git rev-parse --abbrev-ref HEAD
git status --porcelain | grep -q . && echo dirty || echo clean
# Commits on this branch not on main
git log main..HEAD --oneline
# Undo the last commit but keep the changes staged
git reset --soft HEAD~1See Git for the model behind these and for conflict recovery.
Docker and Podman#
Every command below works with podman in place of docker unless noted.
# Running containers with ports and status
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
# Containers that exited non-zero
docker ps -a --filter 'status=exited' --format '{{.Names}}\t{{.Status}}' | grep -v 'Exited (0)'
# Why did a container stop
docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}' my-app
# Follow logs with timestamps, last 200 lines
docker logs -f --tail 200 -t my-app
# Resource use right now, without streaming
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}'
# Shell into a container, or run one command
docker exec -it my-app sh
docker exec my-app cat /etc/hosts
# Container IP address
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-app
# Image history and layer sizes
docker history --no-trunc --format '{{.Size}}\t{{.CreatedBy}}' my-app:latest
# Disk usage by type, then reclaim unused data
docker system df
docker system prune -f # stopped containers, unused networks, dangling images, build cache
docker system prune -af --volumes # also unused images and volumes: data loss if a volume is not attached
# Images not used by any container
docker images -f dangling=true -q | xargs -r docker rmi
# Copy a file out of a container
docker cp my-app:/app/config.yaml ./config.yaml
# Save an image to a tarball and load it elsewhere
docker save my-app:1.2.3 | gzip > my-app-1.2.3.tar.gz
gunzip -c my-app-1.2.3.tar.gz | docker load
# Diff of the container filesystem against its image
docker diff my-app
# Processes inside a container from the host's view
docker top my-app
# Build with a tag and no cache, then check the digest
docker build --no-cache -t registry.example.com/my-app:1.2.3 .
docker inspect -f '{{index .RepoDigests 0}}' registry.example.com/my-app:1.2.3
# Podman only: generate a systemd Quadlet-friendly listing and check rootless mappings
podman ps --format '{{.Names}} {{.Image}}'
podman unshare cat /proc/self/uid_mapKubernetes#
# Context and namespace you are about to act on
kubectl config current-context; kubectl config view --minify -o jsonpath='{..namespace}{"\n"}'
# Pods not Running or Succeeded, cluster-wide
kubectl get pods -A --field-selector 'status.phase!=Running,status.phase!=Succeeded'
# Restart counts, highest first
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.status.containerStatuses[0].restartCount}{"\n"}{end}' | sort -k3 -nr | head
# Recent events, newest last
kubectl get events -A --sort-by=.lastTimestamp | tail -30
# Logs from every pod of a Deployment, prefixed
kubectl logs deploy/my-app -n my-namespace --all-containers --prefix --since=10m
# Previous container's logs after a crash
kubectl logs my-pod -n my-namespace --previous
# Images per pod
kubectl get pods -n my-namespace -o custom-columns='POD:.metadata.name,IMAGES:.spec.containers[*].image'
# Node resource requests versus allocatable
kubectl describe nodes | awk '/^Name:/ {n=$2} /Allocated resources/,/Events/ {if ($1 == "cpu" || $1 == "memory") print n, $0}'
# Top pods by memory and CPU (metrics-server)
kubectl top pod -A --sort-by=memory | head
kubectl top node
# Pods on one node
kubectl get pods -A -o wide --field-selector spec.nodeName=node-1
# Secret keys without values
kubectl get secret db -n my-namespace -o jsonpath='{.data}' | jq 'keys'
# Decode one secret value (prints a credential)
kubectl get secret db -n my-namespace -o jsonpath='{.data.password}' | base64 -d; echo
# Wait for a rollout, with a timeout, exit non-zero if it fails
kubectl rollout status deploy/my-app -n my-namespace --timeout=5m
# Delete evicted pods (objects only; the pods are already dead)
kubectl get pods -A --field-selector status.phase=Failed -o json | jq -r '.items[] | select(.status.reason == "Evicted") | "\(.metadata.namespace) \(.metadata.name)"' | xargs -r -n2 sh -c 'kubectl delete pod -n "$0" "$1"'
# Completed Jobs older than a day
kubectl get jobs -A -o json | jq -r --arg cutoff "$(date -u -d '1 day ago' +%FT%TZ)" '.items[] | select(.status.succeeded == 1 and .status.completionTime < $cutoff) | "\(.metadata.namespace)/\(.metadata.name)"'
# Namespaces stuck Terminating, and what is holding them
kubectl get ns --field-selector status.phase=Terminating
kubectl api-resources --verbs=list --namespaced -o name | xargs -n1 kubectl get -n my-namespace --ignore-not-found --show-kind
# Run a throwaway debugging pod
kubectl run tmp --rm -it --image=nicolaka/netshoot -n my-namespace -- sh
# Copy a file out of a pod without tar in the image
kubectl exec my-pod -n my-namespace -- cat /app/report.csv > report.csv
# Which ServiceAccount can do what
kubectl auth can-i --list --as system:serviceaccount:my-namespace:my-app -n my-namespace
# Live diff before applying
kubectl diff -f manifest.yaml
# Dump every resource in a namespace for a backup
kubectl get all,cm,secret,ing,pvc -n my-namespace -o yaml > my-namespace-$(date +%F).yamlSSH#
# Generate a modern key and install it
ssh-keygen -t ed25519 -C "deploy@example.com" -f ~/.ssh/id_ed25519
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@host.example.com
# Keys in the agent, and their fingerprints
ssh-add -l
ssh-keygen -lf ~/.ssh/id_ed25519.pub
# Fingerprint of a server's host key before trusting it
ssh-keyscan -t ed25519 host.example.com 2>/dev/null | ssh-keygen -lf -
# Remove a stale host key after a rebuild
ssh-keygen -R host.example.com
# Verbose connection for diagnosing auth failures
ssh -vvv -o BatchMode=yes deploy@host.example.com true 2>&1 | grep -E 'Offering|Authentications|denied|Accepted'
# Jump through a bastion
ssh -J bastion.example.com deploy@10.0.0.5
# Local port forward: reach a remote database on localhost:5432
ssh -N -L 5432:db.internal.example.com:5432 bastion.example.com
# Remote port forward: expose local 8080 on the remote host
ssh -N -R 8080:localhost:8080 host.example.com
# SOCKS proxy through a host
ssh -N -D 1080 bastion.example.com
# Run a local script on a remote host
ssh host.example.com 'bash -s' < local-script.sh
# Run a script that needs sudo on many hosts
xargs -a hosts.txt -P8 -I{} ssh -o BatchMode=yes -o ConnectTimeout=5 {} 'sudo systemctl is-active my-app'
# Copy a directory tree, preserving attributes, resumable
rsync -aHAX --partial --info=progress2 -e 'ssh -o ConnectTimeout=10' ./data/ host.example.com:/data/
# Mount a remote directory (sshfs)
sshfs host.example.com:/var/log /mnt/remote-logs -o reconnect
# Reuse one connection for many commands (ControlMaster)
ssh -o ControlMaster=auto -o ControlPath=~/.ssh/cm-%r@%h:%p -o ControlPersist=10m host.example.com true
# Server-side: see who is trying to log in
journalctl -u sshd --since today | grep -E 'Failed|Accepted' | awk '{print $NF, $0}' | sort | uniq -c | sort -rn | headSee SSH for configuration and SCP for file transfer.
TLS certificates#
# Expiry, subject and issuer of a live endpoint (SNI required for most hosts)
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
# Days until expiry, as a number
d=$(openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2); echo $(( ($(date -d "$d" +%s) - $(date +%s)) / 86400 ))
# Fail if the certificate expires within 30 days
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -checkend $((30 * 86400))
# Whole chain as presented by the server
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | awk '/BEGIN CERT/,/END CERT/'
# Verify the chain against the system trust store
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | grep -E 'Verify return code|verify error'
# SANs of a certificate file
openssl x509 -in cert.pem -noout -ext subjectAltName
# Whole certificate, decoded
openssl x509 -in cert.pem -noout -text
# Does this key match this certificate
diff <(openssl x509 -in cert.pem -noout -pubkey) <(openssl pkey -in key.pem -pubout) && echo match
# Does this CSR match the key and what does it ask for
openssl req -in request.csr -noout -subject -text | grep -A1 'Subject Alternative Name'
# Self-signed certificate for a test host, valid one year
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=test.example.com' -addext 'subjectAltName=DNS:test.example.com'
# Convert PEM to PKCS#12 for Java or Windows (prompts for an export password)
openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile chain.pem -out bundle.p12
# Certificate from a PKCS#12 bundle
openssl pkcs12 -in bundle.p12 -nokeys -clcerts -out cert.pem
# Test with a specific protocol or cipher suite
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null 2>&1 | grep -E 'Protocol|Cipher'
# Certificates expiring within 30 days under a directory
find /etc/pki/tls/certs -name '*.pem' -exec sh -c 'openssl x509 -in "$1" -noout -checkend 2592000 >/dev/null || echo "$1"' _ {} \;
# Serial and SHA-256 fingerprint for comparison with a CT log or pin
openssl x509 -in cert.pem -noout -serial -fingerprint -sha256See TLS and certificates for the chain of trust and server configuration.
Dates and maths#
# Now, in ISO 8601 UTC, and as epoch seconds
date -u +%FT%TZ
date +%s
# Epoch to local time, and to UTC
date -d @1727136000
date -u -d @1727136000
# Relative dates (GNU date)
date -d 'yesterday' +%F
date -d '30 days ago' +%F
date -d 'next monday' +%F
date -d '2026-09-24 + 90 days' +%F
# Days between two dates
echo $(( ($(date -d 2026-12-25 +%s) - $(date -d 2026-09-24 +%s)) / 86400 ))
# Week number, day of year, weekday name
date +'%V %j %A'
# A timestamp for filenames
date +%Y%m%dT%H%M%S
# Parse a log timestamp into epoch
date -d '2026-09-24 14:03:11' +%s
# Uptime as a number of seconds, and in words
cut -d' ' -f1 /proc/uptime
uptime -p
# Integer arithmetic in the shell
echo $(( (1024 * 1024 * 1024) / 4096 ))
# Floating point with bc and awk
echo 'scale=3; 1234 / 7' | bc
awk 'BEGIN {printf "%.2f\n", 1234 / 7}'
# Percentage of a total
awk -v used=743 -v total=1024 'BEGIN {printf "%.1f%%\n", used * 100 / total}'
# Hex, binary, decimal conversions
printf '%x\n' 255; printf '%d\n' 0xff; echo 'obase=2; 255' | bc
# Bytes to human-readable and back
numfmt --to=iec 1234567890
numfmt --from=iec 1.2G
# Random port in the ephemeral range, random hex string, random password
shuf -i 32768-60999 -n1
openssl rand -hex 16
tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32; echo
# Sequence with padding
seq -w 1 10
printf 'host%02d\n' {1..5}
# Time a command precisely
/usr/bin/time -v ./job.sh 2>&1 | grep -E 'Elapsed|Maximum resident'JSON and YAML#
# Pretty-print, compact, sort keys
jq . file.json
jq -c . file.json
jq -S . file.json
# Extract a field, raw string output for shell use
jq -r '.items[].name' file.json
# Filter objects by a field, output selected fields as TSV
jq -r '.items[] | select(.status == "active") | [.name, .id] | @tsv' file.json
# Group and count
jq -r 'group_by(.region) | map({region: .[0].region, n: length}) | .[] | "\(.region)\t\(.n)"' file.json
# Update a field in place
jq '.spec.replicas = 3' deploy.json > deploy.new.json
# Build JSON safely from shell variables
jq -n --arg name "$NAME" --argjson port "$PORT" '{name: $name, port: $port}'
# Keys of an object, paths of every leaf
jq -r 'keys[]' file.json
jq -r 'paths(scalars) | join(".")' file.json
# Read JSON Lines and aggregate
jq -s 'map(.bytes) | add' events.jsonl
# YAML to JSON and back (yq, Mike Farah's Go version)
yq -o=json '.' config.yaml
yq -P '.' config.json
# Read a nested value from YAML
yq '.spec.template.spec.containers[0].image' deploy.yaml
# Set a value in place
yq -i '.spec.replicas = 3' deploy.yaml
# Every document in a multi-document YAML, one line each
yq -N '.kind + "/" + .metadata.name' manifests.yaml
# Validate YAML syntax with Python only
python3 -c 'import sys, yaml; yaml.safe_load_all(sys.stdin) and print("ok")' < file.yaml
# JSON to CSV
jq -r '.items[] | [.name, .cpu, .memory] | @csv' file.json
# CSV to JSON (Python, header row becomes keys)
python3 -c 'import csv, json, sys; print(json.dumps(list(csv.DictReader(sys.stdin)), indent=2))' < file.csv
# Diff two JSON files ignoring key order
diff <(jq -S . a.json) <(jq -S . b.json)
# Environment variables as JSON
jq -n 'env | with_entries(select(.key | startswith("APP_")))'See jq for the language behind these filters.
Archives#
# Create, list, extract a gzip tarball
tar -czf app.tgz app/
tar -tzf app.tgz | head
tar -xzf app.tgz -C /opt/
# zstd is faster and smaller than gzip; xz is smaller and slower
tar --zstd -cf app.tar.zst app/
tar -cJf app.tar.xz app/
# Exclude paths, and strip the leading directory on extract
tar -czf app.tgz --exclude='.git' --exclude='*/node_modules' app/
tar -xzf app.tgz --strip-components=1 -C /opt/app/
# Extract one file from a tarball
tar -xzf app.tgz app/config.yaml
# Tar over SSH without a temporary file, both directions
tar -czf - /data | ssh host.example.com 'tar -xzf - -C /backup/'
ssh host.example.com 'tar -czf - /var/log/my-app' > my-app-logs.tgz
# Parallel compression with pigz (drop-in for gzip)
tar -cf - /data | pigz -p 8 > data.tgz
# Zip and unzip, listing first
zip -r site.zip site/ -x '*.DS_Store'
unzip -l site.zip
unzip -q site.zip -d /var/www/
# Compress a log file in place and keep the original's timestamps
gzip -k access.log
zstd --rm access.log
# Read compressed files without extracting
zcat access.log.gz | grep ' 500 '
zgrep -c ' 500 ' access.log.*.gz
zstdcat access.log.zst | tail
# Compare an archive with what is on disk
tar -df app.tgz -C /opt/
# Verify integrity of a compressed file
gzip -t app.tgz && echo ok
xz -t app.tar.xz && echo ok
# Archive a directory with a dated name and remove archives older than 14 days
tar -czf "/backup/app-$(date +%F).tgz" -C /opt app && find /backup -name 'app-*.tgz' -mtime +14 -delete
# cpio-style copy that preserves everything, for moving a tree across filesystems
(cd /src && tar -cf - .) | (cd /dst && tar -xpf -)Troubleshooting#
Most breakages of a one-liner come from running it on a different userland than it was written for.
| Symptom | Cause | Fix |
|---|---|---|
date: illegal option -- d | BSD or macOS date has no -d | date -j -f '%Y-%m-%d' 2026-09-24 +%s on macOS, or install coreutils and use gdate |
sed: 1: "...": invalid command code | BSD sed -i needs an explicit suffix argument | sed -i '' 's/a/b/' file on macOS; sed -i.bak works on both |
readlink: illegal option -- f | BSD readlink lacks -f | realpath file, or greadlink -f from coreutils |
find: -printf: unknown primary | -printf is GNU only | find ... -exec stat -f '%z %N' {} + on BSD, or use gfind |
xargs: illegal option -- a | -a file is GNU only | xargs ... < hosts.txt |
ss: command not found | Not on macOS or BSD; iproute2 is Linux only | lsof -iTCP -sTCP:LISTEN -n -P |
numfmt: command not found | coreutils not installed | Install coreutils, or awk '{printf "%.1fM\n", $1/1048576}' |
[[: not found or Syntax error: "(" unexpected | Running under sh (dash on Debian and Ubuntu) rather than Bash | Add #!/usr/bin/env bash, run with bash script.sh, or rewrite to POSIX [ ] |
{1..5} printed literally | Brace expansion is a Bash feature, not POSIX | Use seq 1 5 in sh |
process substitution <(...) fails | sh does not support it | Use a temporary file, or run under Bash |
grep: invalid option -- P | grep -P (PCRE) missing on BSD, busybox and some minimal images | Use grep -E, or perl -ne |
tar: Option --zstd is not supported | Old tar, or built without libzstd | zstd -dc file.tar.zst | tar -xf - |
Argument list too long | Expansion exceeds ARG_MAX | find ... -exec cmd {} + or xargs |
| Filenames with spaces or newlines break a pipeline | Newline-delimited output | find -print0 | xargs -0, grep -Z, sort -z |
| Works interactively, not from cron | Different PATH, no HOME, no TTY | Use absolute paths and set PATH in the crontab or unit; see systemd |
Test portability with shellcheck -s sh for POSIX scripts and shellcheck -s bash for Bash ones; each warns about constructs the chosen shell does not support. When a one-liner has to run on both Linux and macOS, prefer awk, sort, cut and printf, which behave the same, and avoid GNU-specific flags on date, sed -i, find, readlink, stat and xargs.