- -P
- print the physical path with symlinks resolved
- -L
- print the logical path, symlinks kept (default)
pwdwhere am I?
pwd -Preal path when you got here through a symlink
Linux commands by task, from files and permissions to networking, systemd and text processing. Every entry has flags, copyable examples and macOS notes.
Last verified: 2026-10-18 against GNU coreutils 9.x, util-linux, procps-ng, iproute2, systemd and bash 5.2 manuals; macOS notes against the macOS 26 man pages.
Read files, page through logs, edit in the terminal and compare versions.
cat /etc/os-releaseprint a file
cat part1.txt part2.txt > whole.txtconcatenate
cat -A script.sh | headspot hidden tabs and Windows ^M line endings
macOS/BSD: macOS cat has no -A; use cat -vet for the same view.
tac access.log | headnewest log lines first
macOS/BSD: Not on macOS; use tail -r file.
less /var/log/syslogread a log
journalctl -u nginx | less -Swide lines without wrapping
head -n 20 data.csvheader and first rows
head -c 512 file.bin | xxdpeek at the start of a binary
macOS/BSD: macOS head does not accept negative counts.
tail -n 50 app.loglast 50 lines
tail -F /var/log/nginx/access.logwatch a log that gets rotated
tail -n +2 data.csvskip a CSV header line
sudo nano /etc/hostsedit a system file
The shortcut bar at the bottom of the screen shows the keys; ^ means Ctrl and M- means Alt.
vim +42 app.pyopen at line 42
diff -u old.conf new.confreadable change view
diff -rq dir1/ dir2/which files differ between two trees
diff <(sort a.txt) <(sort b.txt)compare after sorting, via process substitution
make 2>&1 | tee build.logwatch a build and keep a log
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.confappend to a root-owned file (sudo echo >> fails because the shell does the redirect)
printf '%s\n' "$var"print a variable safely, even if it starts with -n
printf '%-12s %6.2f\n' disk 93.5aligned columns
echo -e and echo -n behave differently between shells; printf behaves the same everywhere.
column -t -s, data.csv | less -Sread a simple CSV as a table
mount | column -tline up whitespace-separated output
xxd image.png | headhex and ASCII side by side
hexdump -C file.bin | headcanonical hex dump
xxd is part of the vim packages; hexdump is in util-linux (bsdextrautils on newer Debian/Ubuntu).
strings -n 8 firmware.bin | lessfind version strings, URLs, paths
Part of binutils on Linux.
Read and change who can read, write and execute a file.
ls -l deploy.sh-rwxr-x--- 1 alice devs 812 Oct 18 09:12 deploy.sh
First character is the type (- file, d directory, l symlink). Then three triplets: owner, group, everyone else. r=4, w=2, x=1, so rwxr-x--- is 750.
chmod 644 index.htmlowner read/write, everyone read: normal files
chmod 755 deploy.showner all, others read/execute: scripts and directories
chmod 600 ~/.ssh/id_ed25519private key readable only by you; ssh refuses looser keys
chmod +x script.shmake a script executable
chmod go-w fileremove write for group and others
chmod -R u=rwX,go=rX site/capital X sets execute only on directories and files that already had it
find site/ -type d -exec chmod 755 {} +the right fix, part 1: directories 755
find site/ -type f -exec chmod 644 {} +part 2: files 644
Danger: Gives every user and every compromised service on the machine write access to the whole tree, marks all files executable, and makes OpenSSH refuse keys under a 777 ~/.ssh. Applied to /etc or /usr it can break sudo and the system. Fix ownership with chown instead, or use the two find commands.
sudo chown -R www-data:www-data /var/www/sitegive a web root to the web server user
sudo chown "$USER": file.txttake ownership of a file root created
Danger: sudo chown -R on /, /usr or /etc wipes setuid bits and ownership that sudo, passwd and services rely on; recovery usually means a reinstall. Double-check the path.
chgrp -R developers /srv/sharedhand a shared folder to a team group
umaskshow the current mask, commonly 0022
umask 077new files 600 and directories 700 in this shell
The mask removes bits: files start from 666 and directories from 777, so 022 gives 644 and 755.
sudo systemctl restart nginxone admin command
sudo !!rerun the previous command with sudo
sudo -u postgres psqlrun as a service user
su - deploylog in as deploy with its environment
sudo su -root shell when root has no password (sudo -i does the same)
setfacl -m u:alice:rw report.xlsxalice can read and write, without changing group
getfacl report.xlsxshow all entries
setfacl -R -m g:devs:rwX /srv/appa group gets access to a whole tree
A + at the end of the ls -l permission string means the file has an ACL.
sudo chattr +i /etc/resolv.confnobody, not even root, can change or delete it until -i
lsattr /etc/resolv.confshow attributes; i means immutable
Supported on ext4, XFS, Btrfs. If "Operation not permitted" appears even as root, check lsattr.
Find what is running, stop it, and run things in the background.
ps aux | grep [n]ginxthe [n] trick keeps grep itself out of the results
ps aux --sort=-%mem | headtop memory users
ps -o pid,ppid,etime,cmd -p 1234how long a process has been running
macOS/BSD: macOS ps has no --sort; use ps aux -m (by memory) or ps aux -r (by CPU).
pstree -pwith PIDs
pstree -p 1234what a process has spawned
top -b -n 1 | head -20snapshot you can paste into a ticket
macOS/BSD: macOS top is a different program: sort with top -o cpu or top -o mem.
htop -u www-dataonly one user's processes
Install with apt install htop / dnf install htop.
pgrep -a nginxPIDs and command lines
pgrep -f 'python manage.py runserver'match a script by its arguments
macOS/BSD: On macOS, -a means "include ancestors"; use pgrep -lf to see names and arguments.
pkill -f "node server.js"stop one Node app
pkill -HUP nginxsend a reload signal
pgrep -af nodepreview what -f would match before killing
Patterns are regular expressions matched anywhere in the name; pkill node also hits nodejs-helper. Preview with pgrep first.
kill 1234polite stop
kill -9 1234last resort when TERM is ignored
kill %1kill job 1 of this shell
Try plain kill first and give it a few seconds: -9 skips cleanup, so temp files, locks and half-written data can be left behind.
killall firefoxall Firefox processes
On Linux this is in the psmisc package.
jobslist jobs in this shell with their numbers
bg %1resume job 1 in the background
fg %1bring it back to the foreground
Ctrl+Z suspends the foreground program (it stops running until bg or fg).
./long-export.sh > export.log 2>&1 &start it and get the prompt back
waitin a script, wait for background jobs to finish
nohup ./server > server.log 2>&1 &survives closing the SSH session
For anything long-lived, a systemd service (see Services & logs) or tmux is more robust.
disown -h %1you forgot nohup: background it with bg, then disown it
tmux new -s deploystart a named session
tmux attach -t deployreconnect after SSH drops
Install with your package manager. GNU screen is the older alternative (Ctrl+A d to detach, screen -r to reattach).
nice -n 19 tar -czf backup.tgz /databackground-friendly backup
sudo renice -n -5 -p 1234raise priority of a running process
Niceness runs from -20 (highest priority) to 19 (lowest). Only root can go below 0.
timeout 30s curl -s https://example.com/healthgive up after 30 seconds
timeout -s KILL 5m ./job.shhard kill after five minutes
Exits with status 124 when the time limit was hit.
watch -n 2 df -hwatch disk space fill up
watch -d 'ss -s'-d highlights what changed
sudo lsof -i :8080what is listening on port 8080
sudo lsof +L1deleted files still held open (disk full but du shows nothing)
lsof -p 1234every file one process has open
sudo fuser -vm /mnt/usbwhy umount says "target is busy"
sudo fuser -v 8080/tcpprocess on a TCP port
Danger: fuser -k kills every process using the target with no prompt; on a mount point that can include your own shell.
time ./build.shreal = wall clock, user + sys = CPU time
strace -f -e trace=openat ./app 2>&1 | grep -v ENOENTwhich files does it actually open?
sudo strace -p 1234what is a hung process stuck on?
OS version, kernel, CPU, memory, hardware, time and load.
uname -re.g. 6.8.0-45-generic
uname -mwhich binary to download: x86_64 or aarch64
cat /etc/os-releaseNAME, VERSION_ID, ID for every systemd-era distro
lsb_release -aDebian/Ubuntu alternative, if installed
macOS/BSD: macOS: sw_vers.
hostnamectlhostname, OS, kernel, virtualisation
sudo hostnamectl set-hostname web01rename the machine
uptimeload average over 1, 5 and 15 minutes
Load is the number of processes running or waiting. Compare it to the CPU count (nproc): a load of 4 on 4 cores is fully busy.
free -hlook at the "available" column, not "free"
macOS/BSD: macOS: vm_stat, or top -l 1 | grep PhysMem.
Linux uses spare RAM as disk cache, so "free" looks low on a healthy machine. "available" is what programs can still get.
lscpumodel, sockets, cores, threads, flags
make -j"$(nproc)"use every available core
macOS/BSD: macOS: sysctl -n hw.ncpu and sysctl -n machdep.cpu.brand_string.
lspci -k | grep -A3 -i vgagraphics card and the driver in use
lsusbis the USB device detected at all?
sudo dmidecode -t memory | grep -i sizeinstalled RAM per slot
sudo lshw -shortone line per device
sudo dmesg -T | tail -30what just happened (USB plugged in, disk errors)
sudo dmesg -T | grep -i 'out of memory'did the OOM killer stop your app?
grep MemAvailable /proc/meminfoavailable memory in kB
grep -c ^processor /proc/cpuinfological CPU count
cat /proc/1234/statusstate and memory of one process
vmstat 1 5r = run queue, si/so = swapping, wa = waiting on I/O
iostat -xz 1per-disk utilisation (%util) and latency (await)
iostat comes from the sysstat package.
date '+%Y-%m-%d %H:%M:%S'timestamp for logs
date +%sseconds since 1970-01-01 UTC
date -d @1700000000timestamp to date (GNU)
date -d '2 days ago' +%Frelative date (GNU)
macOS/BSD: macOS date has no -d. Use date -r 1700000000 for a timestamp, date -v-2d for "2 days ago", and date -j -f '%Y-%m-%d' 2026-10-18 +%s to parse.
timedatectllocal time, UTC, time zone, NTP synchronized?
timedatectl list-timezones | grep Americafind the zone name
sudo timedatectl set-timezone America/Torontochange it
Free space, what is using it, partitions, mounting and formatting.
df -hall mounted filesystems
df -h .the filesystem this directory is on
df -i"No space left" with free bytes? You may be out of inodes
macOS/BSD: On macOS, -T filters by type rather than showing it.
du -sh /var/logsize of one folder
du -h -d 1 | sort -hsubfolders, biggest last (works on GNU and macOS)
sudo du -xh -d 1 / | sort -h | tailthe biggest top-level directories
macOS/BSD: macOS du has -d but not --max-depth.
sudo ncdu -x /navigate with arrows, d to delete
Install with apt install ncdu / dnf install ncdu.
lsblkdisks, partitions, sizes, mount points
lsblk -fwhich partition is ext4/xfs, and its UUID
macOS/BSD: macOS: diskutil list.
sudo blkid /dev/sdb1UUID and TYPE, for /etc/fstab
findmntmount tree with options
findmnt -T /var/lib/dockerwhich filesystem holds this path
sudo findmnt --verifycheck /etc/fstab for errors before rebooting
sudo mount /dev/sdb1 /mnt/usbmount a USB stick (directory must exist)
sudo mount -o remount,rw /make a read-only root writable
sudo mount -t nfs nas:/export /mnt/nasmount an NFS share
sudo umount /mnt/usbsafe to unplug after this
sudo umount -l /mnt/naslazy: detach now, clean up when no longer busy
"target is busy" means a process (often your own shell sitting in that directory) is using it: find it with fuser -vm or lsof.
UUID=3f6c... /data ext4 defaults,nofail 0 2one line per filesystem; nofail lets boot continue if the disk is missing
sudo mount -amount everything in fstab now: test before rebooting
A typo in fstab can drop the next boot into emergency mode. Always run sudo mount -a (or findmnt --verify) after editing.
sudo fdisk -levery disk and its partitions
sudo parted /dev/sdb printpartition table type (gpt/msdos) and layout
Danger: Writing a partition table (w in fdisk, mklabel in parted) makes existing data on that disk unreachable. Confirm the device name with lsblk first; /dev/sda and /dev/sdb swap places easily.
sudo mkfs.ext4 -L data /dev/sdb1ext4 with a label
sudo mkfs.vfat -F 32 /dev/sdc1FAT32 for a USB stick other devices can read
Danger: Erases the filesystem on the target immediately, with no confirmation if the device is not mounted. Check the device with lsblk -f; formatting /dev/sda instead of /dev/sda1, or the wrong disk, destroys it.
sudo fsck -f /dev/sdb1force a full check of an unmounted partition
Danger: Never run fsck on a mounted read-write filesystem: it can corrupt it. Unmount first, or for the root filesystem boot from rescue media.
sudo dd if=ubuntu.iso of=/dev/sdX bs=4M status=progress conv=fsyncmake a bootable USB (replace sdX with the whole disk, not a partition)
dd if=/dev/zero of=test.img bs=1M count=100create a 100 MB file
Danger: dd overwrites the of= target without asking. Swapping if= and of=, or picking the wrong /dev/sdX, wipes that disk. Check with lsblk right before running it.
macOS/BSD: macOS: target /dev/rdiskN (from diskutil list) after diskutil unmountDisk, use bs=4m, and press Ctrl+T for progress.
sync && sudo umount /mnt/usbbefore pulling a USB drive
swapon --showcurrent swap
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfileadd a 2 GB swap file (add it to /etc/fstab to keep it)
On Btrfs a swap file needs extra steps (btrfs filesystem mkswapfile on recent btrfs-progs).
shred -u -n 3 secrets.txtoverwrite three times, then delete
Danger: Irreversible. Also note it is not reliable on SSDs, journaling or copy-on-write filesystems (Btrfs, ZFS) or anything with snapshots; use full-disk encryption for real protection.
IP addresses, routes, open ports, HTTP requests, DNS, SSH and file transfer.
ip -br aone line per interface: name, state, addresses
ip a show eth0one interface in detail
hostname -Ijust the IP addresses
macOS/BSD: macOS: ifconfig, or ipconfig getifaddr en0.
ip replaces the old ifconfig (net-tools), which many distributions no longer install.
ip rroutes; "default via" is the gateway
ip route get 1.1.1.1which interface and gateway a destination uses
sudo ip route add 10.20.0.0/16 via 192.168.1.1add a route (lost on reboot)
ip linkinterfaces and MAC addresses
sudo ip link set eth0 down && sudo ip link set eth0 upbounce an interface
Danger: Taking down the interface you are connected through over SSH cuts your session.
sudo ss -tulpnevery listening port and its program
ss -tn state established '( dport = :443 )'established connections to port 443
ss -sconnection totals
macOS/BSD: macOS: lsof -iTCP -sTCP:LISTEN -n -P, or netstat -an.
ss replaces netstat; netstat -tulpn still works where net-tools is installed.
curl -I https://example.comstatus line and headers
curl -LO https://example.com/file.tar.gzdownload, following redirects
curl -s -o /dev/null -w '%{http_code}\n' https://example.comjust the status code
curl -fsSL https://example.com/install.sh -o install.shdownload a script to read before running it
curl -H 'Content-Type: application/json' -d '{"name":"Ada"}' https://api.example.com/usersPOST JSON
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/medouble quotes so $TOKEN expands
curl -X DELETE https://api.example.com/users/42other methods
wget -c https://example.com/big.isoresume after a dropped connection
wget -qO- https://example.com/data.json | jq .pipe straight into another command
macOS/BSD: Not on stock macOS; use curl -LO (curl -C - -LO to resume).
dig +short example.comjust the A records
dig MX example.commail servers
dig @1.1.1.1 example.comask a specific resolver (bypass local cache)
dig -x 8.8.8.8reverse lookup
dig +trace example.comfollow delegation from the root
Package name: dnsutils (Debian/Ubuntu) or bind-utils (Fedora/RHEL).
host example.comA, AAAA and MX in one short answer
nslookup example.com 8.8.8.8the same command exists on Windows
resolvectl statusDNS servers per interface
sudo resolvectl flush-cachesflush systemd-resolved cache
macOS/BSD: macOS: sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder.
Only on systems using systemd-resolved (Ubuntu, Fedora by default).
ping -c 4 example.comfour packets, then a summary
On Linux and macOS ping runs until Ctrl+C unless you pass -c (Windows stops after 4). No reply does not always mean down: many hosts block ICMP.
traceroute -n example.comhops without DNS lookups
tracepath example.comno root needed, also reports MTU
macOS/BSD: macOS has traceroute but not tracepath.
traceroute may need installing on Linux; tracepath (iputils) usually is there.
mtr -rw -c 100 example.comreport after 100 probes per hop
Combines ping and traceroute. Loss at one middle hop only is usually that router de-prioritising ICMP; loss that continues to the end is real.
ssh alice@203.0.113.10log in
ssh -p 2222 alice@hostnon-standard port
ssh alice@host 'df -h'run one command and return
ssh -J bastion.example.com alice@10.0.0.5hop through a bastion host
ssh -N -L 5432:localhost:5432 alice@db-hostconnect your local DB client to a remote Postgres
ssh -N -D 1080 alice@hostbrowse through the server
ssh-keygen -t ed25519 -C "you@example.com"new key pair in ~/.ssh
ssh-copy-id alice@hostappend your public key to the server's authorized_keys
ssh-keygen -R hostremove an old host key after a server rebuild ("REMOTE HOST IDENTIFICATION HAS CHANGED")
Host web
HostName 203.0.113.10
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519then just: ssh web (scp and rsync use it too)
scp report.pdf alice@host:/tmp/upload
scp alice@host:/var/log/app.log .download
scp -r -P 2222 site/ alice@host:~/upload a folder on another port
Since OpenSSH 9.0, scp uses the SFTP protocol underneath. For large or repeated copies rsync is faster because it only sends changes.
rsync -avzP site/ deploy@host:/srv/site/trailing slash on site/ copies its contents, not the folder itself
rsync -av --exclude 'node_modules' ./ /backup/app/local backup without the junk
macOS/BSD: Recent macOS ships openrsync instead of rsync; common flags work, some rarer ones do not.
src/ means "the contents of src"; src without a slash means "the folder src". Getting this wrong nests folders (dest/src/…).
rsync -avn --delete site/ /srv/site/dry run first: lines starting "deleting" show what would go
rsync -av --delete site/ /srv/site/then the real run
Danger: Deletes everything in dest that is not in source. Swapping source and dest, or an empty/wrong source path, wipes the destination. Always run with -n first.
nc -zv db.internal 5432"succeeded" or "Connection refused"/timeout
nc -l 9000listen on a port to test a firewall from the other side
Flags differ between netcat variants: the traditional one needs nc -l -p 9000.
sudo ufw status verboserules and default policy
sudo ufw allow 22/tcpallow SSH (do this before enable)
sudo ufw allow from 10.0.0.0/8 to any port 5432one port, one network
Danger: sudo ufw enable on a remote server without first allowing SSH (ufw allow OpenSSH or 22/tcp) locks you out.
sudo firewall-cmd --list-allactive zone and rules
sudo firewall-cmd --permanent --add-service=https && sudo firewall-cmd --reloadopen HTTPS permanently
sudo firewall-cmd --permanent --add-port=8080/tcpopen a port
Without --permanent a rule lasts until the next reload or reboot.
nmcli device statusinterfaces and whether they are connected
nmcli device wifi listnearby networks
nmcli device wifi connect 'MyWiFi' password 'secret'join a network
The same task in the four main package managers: Debian/Ubuntu (apt), Fedora/RHEL (dnf), Arch (pacman), openSUSE (zypper). Prefix with sudo for anything that changes the system.
| Task | apt (Debian, Ubuntu) | dnf (Fedora, RHEL) | pacman (Arch) | zypper (openSUSE) |
|---|---|---|---|---|
| Refresh package lists # dnf refreshes automatically when metadata is stale. On Arch, never run pacman -Sy followed by -S pkg: partial upgrades are unsupported; use -Syu. | apt update | dnf makecache | pacman -Sy | zypper refresh |
| Upgrade everything # openSUSE Tumbleweed uses zypper dup. apt full-upgrade also removes packages when needed to complete the upgrade. | apt update && apt upgrade | dnf upgrade | pacman -Syu | zypper update |
| Install a package # | apt install nginx | dnf install nginx | pacman -S nginx | zypper install nginx |
| Install a downloaded package file # The ./ tells apt it is a file, not a package name, so dependencies get resolved (dpkg -i does not). | apt install ./app.deb | dnf install ./app.rpm | pacman -U app.pkg.tar.zst | zypper install ./app.rpm |
| Remove a package # | apt remove nginx | dnf remove nginx | pacman -R nginx | zypper remove nginx |
| Remove with config files / unused dependencies # | apt purge nginx && apt autoremove | dnf remove nginx && dnf autoremove | pacman -Rns nginx | zypper remove --clean-deps nginx |
| Search for a package # | apt search keyword | dnf search keyword | pacman -Ss keyword | zypper search keyword |
| Show package details # | apt show nginx | dnf info nginx | pacman -Si nginx (installed: -Qi) | zypper info nginx |
| List installed packages # | apt list --installed | dnf list --installed | pacman -Q | zypper search --installed-only |
| List available updates # | apt list --upgradable | dnf check-update | checkupdates (pacman-contrib) | zypper list-updates |
| Which package owns a file # For files not installed yet: dnf provides /path, apt-file search path (needs apt-file), pacman -F path. | dpkg -S /usr/bin/curl | rpm -qf /usr/bin/curl | pacman -Qo /usr/bin/curl | rpm -qf /usr/bin/curl |
| List files a package installed # | dpkg -L nginx | rpm -ql nginx | pacman -Ql nginx | rpm -ql nginx |
| Hold a package at its current version # On DNF 4, versionlock needs the versionlock plugin package. | apt-mark hold nginx | dnf versionlock add nginx | IgnorePkg = nginx in /etc/pacman.conf | zypper addlock nginx |
| Clear the package cache # | apt clean | dnf clean all | pacman -Sc | zypper clean |
Who you are, who is logged in, and managing accounts.
iduid, gid and every group you belong to
id aliceanother user
wwho is on and what they are running
last -n 10recent logins and reboots
sudo lastb -n 10recent failed logins (Linux)
sudo useradd -m -s /bin/bash alice && sudo passwd alicecreate and set a password
On Debian/Ubuntu, adduser alice is the friendlier interactive wrapper that sets up the home directory and password.
sudo usermod -aG docker alicelet alice run docker
sudo usermod -aG sudo aliceadmin on Debian/Ubuntu (the group is wheel on Fedora/RHEL/Arch)
Danger: Without -a, usermod -G replaces all of the user's supplementary groups, which can remove sudo access. Group changes apply at the next login (or run newgrp docker).
sudo userdel -r olduseralso removes the home directory and mail spool
Danger: -r deletes the home directory and its files permanently. Back it up first, or lock the account instead (passwd -l).
passwdchange your own
sudo passwd alicereset someone else's
sudo passwd -l alicelock password login (SSH keys may still work)
sudo groupadd developersnew group
sudo gpasswd -d alice developersremove alice from it
groups alicealice's groups
getent group sudomembers of sudo (works with LDAP too)
getent passwd aliceaccount entry: UID, home, shell
sudo visudo -f /etc/sudoers.d/deployseparate file per rule
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myappone command without a password
visudo checks the syntax before saving. Editing /etc/sudoers with a normal editor and making a typo can lock everyone out of sudo.
sudo chage -l alicelast change and expiry dates
sudo chage -M 90 aliceexpire every 90 days
tar, gzip, xz, zip and checksums.
tar -czf site-2026-10-18.tar.gz site/gzip-compressed archive of a folder
tar -cJf logs.tar.xz --exclude='*.tmp' logs/smaller xz archive, skipping temp files
tar -xf archive.tar.gzGNU tar and macOS tar detect the compression themselves
tar -xzf app.tar.gz -C /opt/app --strip-components=1into a folder, without the wrapper directory
tar -tf backup.tar.gz | lesssee what is inside without extracting
tar -xf backup.tar.gz etc/nginx/nginx.confpull out one file (path as shown by -t)
tar --zstd -cf data.tar.zst data/much faster than xz, similar ratio to gzip -9 or better
zstd -d file.zstdecompress a single file
Needs GNU tar 1.31+ (or a recent bsdtar) and the zstd program.
gzip -k access.logcreates access.log.gz, keeps the original
zcat access.log.gz | grep 404read without decompressing to disk
zgrep 500 access.log.*.gzgrep rotated logs directly
Without -k, gzip replaces the file with the .gz version.
xz -T0 -k big.sqlall CPU cores, keep original
unxz big.sql.xzdecompress
zip -r site.zip site/zip a folder
zip -r site.zip site/ -x '*.git*' '*/node_modules/*'exclude paths
zip -e encrypts with the legacy ZipCrypto scheme, which is weak. Use 7z or gpg for real secrecy.
unzip files.zip -d files/into a folder
unzip -l files.ziplist contents only
unzip -o files.zipoverwrite without prompting
7z x archive.7zextract with full paths
7z a -p backup.7z docs/AES-256 encrypted archive (prompts for a password)
7z l archive.rarlist contents of many formats
Package: p7zip-full / 7zip on Debian/Ubuntu, p7zip or 7zip on Fedora/Arch.
sha256sum ubuntu.isocompare with the hash on the download page
sha256sum -c SHA256SUMS --ignore-missingcheck every file listed that you actually have
macOS/BSD: macOS: shasum -a 256 file (and shasum -a 256 -c SUMSFILE).
split -b 100M backup.tar.gz backup.part.backup.part.aa, .ab, …
cat backup.part.* > backup.tar.gzjoin them again
Find files by name, age or size, and find text inside files.
find . -name '*.log'quote the pattern so the shell does not expand it first
find / -iname 'nginx.conf' 2>/dev/nullsearch everywhere, hide permission errors
find . -maxdepth 1 -type dimmediate subdirectories
find . -type l -xtype lbroken symlinks (GNU)
find . -type f -emptyempty files
find /var/log -type f -mtime +30logs older than 30 days
find . -type f -mmin -15what changed in the last 15 minutes
sudo find / -xdev -type f -size +500M -exec ls -lh {} + 2>/dev/nullfiles over 500 MB on the root filesystem
find . -name '*.sh' -exec chmod +x {} +{} + passes many files per command (fast)
find . -name '*.py' -exec grep -l 'TODO' {} +which Python files contain TODO
find . -name '*.jpg' -print0 | xargs -0 -P4 -n 20 jpegoptimparallel processing, safe with spaces in names
find /tmp/app -name '*.tmp' -mtime +7 -printstep 1: list what matches
find /tmp/app -name '*.tmp' -mtime +7 -deletestep 2: same command with -delete at the end
Danger: Order matters: -delete runs wherever it appears, so find . -delete -name "*.tmp" deletes everything under . before the name test is ever applied. Put -delete last and run with -print first.
find /srv -type f -perm -o+wworld-writable files
sudo find / -xdev -perm -4000 -type f 2>/dev/nullsetuid programs
locate -i nginx.confmuch faster than find /
sudo updatedbrefresh the index now
Provided by plocate or mlocate; the index is rebuilt daily, so files created today may be missing until updatedb runs.
grep -n 'ERROR' app.logmatching lines with numbers
grep -v '^#' nginx.conf | grep -v '^$'config without comments and blank lines
grep -c 'Failed password' /var/log/auth.logcount failed SSH logins
grep -rn 'TODO' src/every TODO with file and line
grep -rl --include='*.php' 'mysql_connect' .which PHP files still use an old API
grep -rn --exclude-dir={.git,node_modules} 'apiKey' .skip noisy folders
grep -C 3 'Traceback' app.log3 lines before and after
grep -A 20 'Exception' app.logthe stack trace after the error
grep -E 'error|warn' app.logeither word (extended regex)
grep -Eo '[0-9]{1,3}(\.[0-9]{1,3}){3}' access.log | sort | uniq -c | sort -rn | headtop IP addresses in a log
grep -P '\d{4}-\d{2}-\d{2}' filePerl-style \d (GNU grep)
macOS/BSD: -E works everywhere; -P (PCRE) is a GNU feature and is not available in every macOS release.
type lltells you it is an alias, function, builtin or file
command -v docker >/dev/null || echo "docker missing"portable check in scripts
which -a python3every python3 on the PATH, in order
Cut, filter, sort, count, transform and reshape text and JSON.
sed 's/http:/https:/g' links.txtprint the result; the file is unchanged
sed 's|/usr/local|/opt|g' paths.txtany delimiter, handy for paths
sed -E 's/([0-9]+)px/\1rem/g' style.cssreuse a captured group
sed -i 's/^#Port 22/Port 2222/' /etc/ssh/sshd_configGNU sed
sed -i.bak 's/foo/bar/g' config.inikeeps config.ini.bak; works on GNU and macOS
macOS/BSD: macOS sed requires an argument after -i: sed -i '' 's/foo/bar/g' file. GNU sed reads '' as the script, so the only form that works on both is -i.bak.
sed -n '100,120p' big.loglines 100 to 120
sed '/^\s*#/d; /^$/d' app.confstrip comments and blank lines (GNU \s)
sed -n '/BEGIN CERT/,/END CERT/p' bundle.pemeverything between two markers
awk '{print $1}' access.logfirst column (client IP)
awk -F: '{print $1, $7}' /etc/passwduser and shell
awk -F, 'NR>1 {print $2}' data.csvsecond CSV column, skip header (simple CSV only: no quoted commas)
awk '$9 >= 500' access.loglines where field 9 (status) is 5xx
awk '{s += $2} END {print s}' sales.txtsum of column 2
awk '{c[$1]++} END {for (k in c) print c[k], k}' access.log | sort -rn | headcount per value, top 10
cut -d, -f1,3 data.csvcolumns 1 and 3
cut -d: -f1 /etc/passwdall user names
cut -c1-10 filefirst 10 characters of each line
cut treats every delimiter as a field break, so two spaces mean an empty field. For space-aligned output use awk.
du -sh * | sort -hfolders by size
sort -t, -k3,3nr data.csvCSV by column 3, numeric, descending
sort -u emails.txt > unique.txtsorted and de-duplicated
sort ips.txt | uniq -c | sort -rn | headtop 10 most frequent lines
uniq only collapses adjacent duplicates, so sort first.
wc -l access.lognumber of lines
ls | wc -lhow many entries in this folder
grep -c '' fileline count that includes a last line without a newline
tr 'a-z' 'A-Z' < in.txtuppercase
tr -d '\r' < windows.txt > unix.txtstrip Windows CRLF line endings
tr -s ' ' < filesqueeze repeated spaces
tr '\n' ',' < list.txtjoin lines with commas
tr only reads standard input, so redirect the file in with <.
find . -name '*.log' -print0 | xargs -0 rm -fdelete found files safely
cat urls.txt | xargs -n 1 -P 4 curl -sOdownload 4 at a time
ls *.png | xargs -I {} cp {} /backup/{}one command per item
macOS/BSD: GNU xargs runs the command once even with empty input unless you pass -r; macOS xargs skips empty input by default and accepts -r for compatibility.
jq . response.jsonpretty-print
jq -r '.items[].name' data.jsonone name per line
jq -r '.[] | select(.active) | .email' users.jsonfilter, then pick a field
curl -s https://api.github.com/repos/jqlang/jq | jq '.stargazers_count'straight from an API
Install with apt/dnf install jq. Ships with recent macOS.
paste -sd, ids.txtall lines joined with commas
paste names.txt emails.txttwo files as two columns
comm -12 <(sort a.txt) <(sort b.txt)in both
comm -23 <(sort a.txt) <(sort b.txt)only in a.txt
seq 1 51 to 5, one per line
seq -w 1 10zero-padded: 01 … 10
for i in $(seq 1 3); do echo "run $i"; doneloop counter
printf '%s' 'user:pass' | base64encode (printf avoids a trailing newline)
echo dXNlcjpwYXNz | base64 -ddecode
base64 -w 0 image.png > image.b64no line wrapping (GNU wraps at 76 chars)
macOS/BSD: macOS base64 does not wrap by default and has no -w. Use -d to decode (older macOS used -D).
iconv -f WINDOWS-1252 -t UTF-8 old.csv > new.csvfix mojibake from Excel exports
iconv -llist supported encodings
shuf -n 1 names.txtone random line
shuf -i 1-100 -n 5five random numbers from 1 to 100
systemd services: start, stop, enable at boot, and read their logs with journalctl.
systemctl status nginxstate, PID, memory and the last log lines
systemctl is-active --quiet nginx && echo upfor scripts
sudo systemctl restart nginxfull restart
sudo systemctl reload nginxre-read config without dropping connections (if the service supports it)
Test config before restarting where possible, e.g. sudo nginx -t, so a typo does not take the site down.
sudo systemctl enable --now dockerenable at boot and start now
sudo systemctl disable --now cupsstop now and at boot
sudo systemctl mask bluetoothblock it from being started at all (unmask to undo)
systemctl --failedwhat broke
systemctl list-units --type=service --state=runningwhat is running
systemctl list-unit-files --state=enabledwhat starts at boot
sudo systemctl edit nginxdrop-in override that survives package upgrades
systemctl cat nginxthe unit plus all overrides
sudo systemctl daemon-reloadneeded after editing unit files by hand
[Unit]
Description=My app
After=network.target
[Service]
User=app
WorkingDirectory=/srv/app
ExecStart=/srv/app/bin/server
Restart=on-failure
[Install]
WantedBy=multi-user.targetthen: sudo systemctl daemon-reload && sudo systemctl enable --now myapp
journalctl -u nginx -ffollow one service
journalctl -u myapp --since '10 min ago'recent window
journalctl -b -1 -p errerrors from the boot before the crash
journalctl --disk-usagehow much space logs take
sudo journalctl --vacuum-size=500Mkeep at most 500 MB of archived logs
sudo journalctl --vacuum-time=14ddrop archived logs older than two weeks
For a permanent limit set SystemMaxUse= in /etc/systemd/journald.conf.
sudo tail -f /var/log/sysloggeneral log on Debian/Ubuntu (/var/log/messages on Fedora/RHEL)
sudo tail -f /var/log/auth.loglogins and sudo on Debian/Ubuntu (/var/log/secure on RHEL)
Some distributions (Fedora, Arch) keep logs only in the journal unless rsyslog is installed.
sudo rebootreboot now
sudo shutdown -r +10 'Kernel update'reboot in 10 minutes, warning logged-in users
sudo shutdown -ccancel a scheduled shutdown
sudo shutdown -h nowpower off
Run commands on a schedule or once later.
crontab -eopen your crontab in $EDITOR
crontab -llist it
crontab -l > crontab.bakback it up
Build and explain the schedule in the cron expression generator →
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1every day at 02:30, keep output
*/15 * * * * /usr/local/bin/check.shevery 15 minutes
0 9 * * 1-5 /usr/local/bin/report.sh09:00 Monday to Friday
@reboot /usr/local/bin/startup.shonce at boot
Cron runs with a minimal PATH and no shell profile: use absolute paths. A % in the command must be escaped as \%.
crontab -l > ~/crontab.bak && crontab -rback up before removing
crontab ~/crontab.bakrestore from a file
Danger: Deletes every one of your cron jobs with no prompt, and r sits right next to e on the keyboard. Some cron implementations offer -i to ask first (crontab -ri).
0 3 * * * root /usr/local/bin/cleanup.shline in /etc/cron.d/cleanup: note the extra user field
ls /etc/cron.daily/scripts run once a day by run-parts
On Debian and Ubuntu, files in /etc/cron.d whose names contain a dot are ignored, so name it backup, not backup.cron.
echo 'systemctl restart myapp' | sudo at 02:00tonight at 2 am
echo './report.sh' | at now + 30 minutesin half an hour
atqpending jobs
atrm 3cancel job 3
Needs the at package and the atd service running.
systemctl list-timersevery timer and when it next fires
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.targetbackup.timer next to backup.service; Persistent catches up after downtime
systemd-analyze calendar 'Mon..Fri 09:00'check what an OnCalendar expression means
sleep 5 && curl -s localhost:8080/healthgive a service time to start
sleep 2msuffixes work on GNU and current macOS
Pipes, redirection, globbing, variables, history and the keys that save the most time. macOS uses zsh by default: nearly all of this works there too, except shopt, help and bash 4 features (macOS ships bash 3.2).
ps aux | grep nginxfilter a listing
history | grep 'docker run'find a command you ran before
set -o pipefailin scripts: a pipeline fails if any part fails, not just the last
ls -l > listing.txtoverwrite (creates the file)
echo 'done' >> progress.logappend
set -o noclobbermake > refuse to overwrite existing files (>| forces)
The shell truncates the file before the command runs, so sort file > file leaves you with an empty file. Write to a new name, then mv.
find / -name x 2>/dev/nullthrow away error messages
./build.sh > build.log 2>&1output and errors in one file (order matters: > first, then 2>&1)
./build.sh &> build.logbash shorthand for the same
mysql mydb < dump.sqlfile as input
cat > config.yml <<'EOF'
port: 8080
host: $HOST stays literal
EOFquoting 'EOF' stops $variables expanding
grep -c a <<< "banana"here-string
make && sudo make installinstall only if the build succeeded
cd /srv/app || exit 1stop a script if cd fails
(cd frontend && npm run build)parentheses: a subshell, so your directory does not change
echo "Today is $(date +%A)"command substitution
echo $(( 1024 * 4 ))integer arithmetic
diff <(ls dir1) <(ls dir2)process substitution: output as a temporary file
Backticks `command` are the old form of $(command); $( ) nests and reads better.
ls *.jpgall JPGs in this folder
rm photo-??.pngphoto-01.png … photo-99.png
cp config.{yml,yml.bak}brace expansion: cp config.yml config.yml.bak
shopt -s globstar; ls **/*.md** matches any depth (bash 4+)
Globs do not match dotfiles by default (shopt -s dotglob changes that). If nothing matches, bash passes the pattern literally unless shopt -s nullglob is set.
echo 'Cost: $5'single quotes: nothing expands
echo "Home is $HOME"double quotes: variables expand, spaces are kept
cp "$file" "$dest/"always quote variables that hold paths: spaces break unquoted ones
name="Ada"; echo "Hi $name"set and use
echo "${PORT:-8080}"default when unset or empty
file=report.tar.gz; echo "${file%%.*}" "${file##*/}"strip extension / strip directory
echo "${#name}"string length
export NODE_ENV=productionvisible to every program started from this shell
DEBUG=1 ./apponly for that one command
printenv | sortlist them
unset DEBUGremove
Lost when the shell exits. Put exports in ~/.bashrc (interactive shells) or ~/.profile (login shells) to keep them.
echo "$PATH" | tr : '\n'one entry per line
export PATH="$HOME/.local/bin:$PATH"prepend your own bin folder (add to ~/.bashrc)
hash -rforget cached command locations after moving binaries
alias ll='ls -alF'add to ~/.bashrc to keep
source ~/.bashrcapply changes without opening a new terminal (same as . ~/.bashrc)
\lsrun the real ls, bypassing an alias
grep -q root /etc/passwd; echo $?0 found, 1 not found, 2 error
set -euo pipefailtop of a script: stop on errors, unset variables and failed pipes
sudo !!rerun the last command with sudo
cd !$use the last argument of the previous command
!42rerun line 42 of history
^foo^barrerun the last command with foo replaced by bar
With HISTCONTROL=ignorespace (default on Ubuntu as part of ignoreboth), a command typed with a leading space is not saved: useful for commands with secrets.
bind -P | lesslist every key binding
man rsyncfull manual; / to search, q to quit
man 5 crontabsection 5: the file format, not the command
help cdbash builtins have no man page of their own
apropos partitionwhich commands mention a word
#!/usr/bin/env bash
set -euo pipefail
name="${1:-world}"
if [[ -f "$name.txt" ]]; then
echo "found $name.txt"
fi
for f in *.log; do
echo "$f"
donearguments, a test, a loop
chmod +x script.sh && ./script.sh Adamake executable and run
bash -x script.shtrace each line as it runs
$1 … $9 are arguments, "$@" is all of them, $# is how many. [[ ]] is bash; use [ ] in POSIX sh scripts.
resetafter cat-ing a binary file left the terminal printing junk
Found a problem, or something that could be better? We read every message and we fix things quickly.
This Linux commands cheat sheet groups more than 200 commands by the job you are trying to do: moving around and managing files, permissions, processes, disk space, networking, packages, users, archives, searching, text processing, systemd services and logs, cron, and the bash basics that tie them together. Each entry gives the syntax, the flags you will actually use with a one-line explanation, copyable examples and, where it matters, a warning. Search by task (“free disk space”, “what is using port 8080”) or by command name, and link straight to any entry.
Flags follow the GNU coreutils, util-linux, procps-ng, iproute2, systemd and bash manuals, which is what Ubuntu, Debian, Fedora, RHEL, Arch and openSUSE ship. Where macOS behaves differently, a blue note says how, because sed -i, date -d, stat -c and xargs are the classic places where a script written on one breaks on the other. It is a reference for the common 90%, not a replacement for man pages: for rarely used options, man <command> is still the authority.
Start with navigation and files: pwd, ls, cd, mkdir, cp, mv, rm, cat and less. Then man (help), sudo, grep and find for searching, chmod and chown for permissions, ps, top and kill for processes, df and du for disk space, and ssh and scp for remote machines. Learn pipes (|) and redirection (> and >>) early, because they let you combine everything else.
Run df -h to see free space on every mounted filesystem, in human-readable units. To find what is using the space, run du -h -d 1 | sort -h inside a directory: it lists subfolders with the largest last. If df says the disk is full but du finds nothing, a deleted file is probably still held open by a process; sudo lsof +L1 lists them.
Run sudo ss -tulpn to list every listening TCP and UDP port with the program that owns it, or sudo lsof -i :8080 for one port. The sudo matters: without it you only see the names of your own processes. ss replaced netstat, which still works on systems that have the net-tools package installed.
kill PID sends SIGTERM, which asks the process to exit and lets it clean up: flush files, release locks, close connections. kill -9 PID sends SIGKILL, which the kernel enforces immediately and the process cannot catch or ignore, so no cleanup happens. Use plain kill first, wait a few seconds, and keep -9 for processes that ignore it.
macOS ships BSD sed, where -i requires a backup extension argument. On macOS write sed -i '' 's/old/new/g' file; on Linux GNU sed the same command fails because it reads '' as the script. The form that works on both is sed -i.bak 's/old/new/g' file, which keeps a backup copy you can delete afterwards.
Almost all of them are: the core tools come from the same GNU and util-linux projects everywhere. What differs is the package manager (apt on Debian and Ubuntu, dnf on Fedora and RHEL, pacman on Arch, zypper on openSUSE, shown side by side in the Packages table), some log file paths, the admin group (sudo vs wheel) and the firewall front end (ufw vs firewalld). Alpine and BusyBox-based systems use smaller versions of the tools with fewer flags.
A short list of commands does almost all of the damage: rm -rf with a wrong or empty path, dd or mkfs pointed at the wrong disk, chmod -R 777 or chown -R on a system directory, rsync --delete or robocopy-style mirrors with source and destination swapped, crontab -r instead of crontab -e, usermod -G without -a, and find with -delete in the wrong position. Each is marked Dangerous in the sheet with the specific way it goes wrong.
The habit that prevents most of it is to run the harmless version first. Before rm -rf, run ls on the same path. Before find … -delete, run the same find with -print. Before rsync --delete, add -n for a dry run. Before dd or mkfs, run lsblk and read the device name twice. In scripts, write ${DIR:?} instead of $DIR so an empty variable aborts the script instead of turning a path into /.
Two shell settings help too: set -euo pipefail at the top of scripts stops on the first error instead of continuing with bad state, and set -o noclobber makes > refuse to overwrite an existing file.
macOS is Unix, but its command-line tools come from FreeBSD rather than GNU, and the differences show up exactly when you copy a command from a Linux tutorial:
• sed -i needs an argument on macOS (sed -i '' …); -i.bak works on both.
• date has no -d on macOS; use date -r <epoch> to convert a timestamp and date -v-2d for relative dates.
• stat uses -f with different format letters instead of GNU -c.
• xargs on macOS skips empty input by default, while GNU xargs runs the command once unless given -r.
• du takes -d 1 on both, but only GNU accepts --max-depth; ps has no --sort on macOS (use ps aux -m or -r).
• head -n -5, tac, free, ip, ss, timeout and watch are missing from stock macOS.
If you need GNU behaviour on a Mac, Homebrew's coreutils, gnu-sed and findutils packages install the GNU versions with a g prefix (gsed, gdate, gstat, gxargs).
ls -l shows permissions as ten characters, such as -rwxr-x---. The first is the type (- file, d directory, l link); the next nine are three groups of read, write and execute for the owner, the group and everyone else. As numbers, r is 4, w is 2 and x is 1, added per group, so rwxr-x--- is 750.
Typical modes: 644 for ordinary files, 755 for directories and scripts, 600 for private keys and files holding secrets, 700 for a private directory such as ~/.ssh. On a directory, x means “may enter”, which is why directories need it and plain files do not. The chmod calculator converts between the symbolic and octal forms if you want to check a mode before applying it.