Software Engineering WikiSE Wiki

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.

Reviewed MarkdownEdit

On this page

Cheatsheet#

TaskCommand
What is filling this diskdu -xh --max-depth=1 / 2>/dev/null | sort -rh | head -20
What is listening on which portss -tulpn
Who holds this portss -tlnp 'sport = :8080'
Files still open after deletion (space not freed)lsof +L1
Top memory consumersps -eo pid,rss,comm --sort=-rss | head
Follow the log of a unitjournalctl -u my-app -f
Boot errorsjournalctl -b -p err
Find large recently changed filesfind / -xdev -type f -size +100M -mtime -1
Grep recursively, code onlygrep -rnI --exclude-dir=.git 'pattern' .
Replace in files, in placegrep -rlZ 'old' . | xargs -0 sed -i 's/old/new/g'
HTTP status and timingcurl -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://example.com/
Certificate expiry of a hostopenssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -enddate
Resolve through a specific serverdig +short @192.0.2.53 example.com A
Copy a key to a hostssh-copy-id -i ~/.ssh/id_ed25519.pub user@host
Run a command on many hostsxargs -a hosts.txt -P8 -I{} ssh -o BatchMode=yes {} uptime
Pods not runningkubectl get pods -A --field-selector 'status.phase!=Running,status.phase!=Succeeded'
Container disk usagedocker system df -v
Pretty-print and query JSONcurl -s https://api.example.com/v1 | jq '.items[] | .name'
Tar a directory, excluding junktar --exclude='.git' --exclude='node_modules' -czf app.tgz app/
Epoch to datedate -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/null

Processes#

# 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 -rh

Network 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_max

DNS 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.1

Users 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 today

systemd 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 -n1

Git#

# 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~1

See 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_map

Kubernetes#

# 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).yaml

SSH#

# 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 | head

See 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 -sha256

See 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.

SymptomCauseFix
date: illegal option -- dBSD or macOS date has no -ddate -j -f '%Y-%m-%d' 2026-09-24 +%s on macOS, or install coreutils and use gdate
sed: 1: "...": invalid command codeBSD sed -i needs an explicit suffix argumentsed -i '' 's/a/b/' file on macOS; sed -i.bak works on both
readlink: illegal option -- fBSD readlink lacks -frealpath file, or greadlink -f from coreutils
find: -printf: unknown primary-printf is GNU onlyfind ... -exec stat -f '%z %N' {} + on BSD, or use gfind
xargs: illegal option -- a-a file is GNU onlyxargs ... < hosts.txt
ss: command not foundNot on macOS or BSD; iproute2 is Linux onlylsof -iTCP -sTCP:LISTEN -n -P
numfmt: command not foundcoreutils not installedInstall coreutils, or awk '{printf "%.1fM\n", $1/1048576}'
[[: not found or Syntax error: "(" unexpectedRunning under sh (dash on Debian and Ubuntu) rather than BashAdd #!/usr/bin/env bash, run with bash script.sh, or rewrite to POSIX [ ]
{1..5} printed literallyBrace expansion is a Bash feature, not POSIXUse seq 1 5 in sh
process substitution <(...) failssh does not support itUse a temporary file, or run under Bash
grep: invalid option -- Pgrep -P (PCRE) missing on BSD, busybox and some minimal imagesUse grep -E, or perl -ne
tar: Option --zstd is not supportedOld tar, or built without libzstdzstd -dc file.tar.zst | tar -xf -
Argument list too longExpansion exceeds ARG_MAXfind ... -exec cmd {} + or xargs
Filenames with spaces or newlines break a pipelineNewline-delimited outputfind -print0 | xargs -0, grep -Z, sort -z
Works interactively, not from cronDifferent PATH, no HOME, no TTYUse 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.

Further reading#