Scripting and data cheat sheetTux, the Linux penguin

Linux Commands cheat sheet

Linux commands on one page: files, permissions, find, grep, sed, awk, processes, disk, ssh, curl, apt and tar, each one run on Ubuntu 24.04 LTS.

Last updated

Linux commands are the small programs you run from a terminal to move around, find things, read and edit text, and look after a machine. The reference below is grouped by what you are trying to do, and the filter box searches all of it at once. Type port to see everything about ports, or macos to see where the Mac's versions differ.

Every command was run on Ubuntu 24.04 LTS, with GNU coreutils 9.4, grep 3.11, sed 4.9, findutils 4.9.0, tar 1.35, util-linux 2.39.3, procps-ng 4.0.4, iproute2 6.1.0, OpenSSH 9.6 and curl 8.5. The permission and user commands were also run on Debian 13, and the package rows say which distributions they were checked on. The shell language itself, meaning quoting, variables, loops and redirection, is on the Bash cheat sheet.

Everything on Linux lives in one tree that starts at /. Your home folder is /home/yourname, written ~ for short.

TaskCommandNotes
Where am IpwdPrint working directory: the full path of the current folder
Go to a foldercd /var/log
Go homecdcd ~ does the same
Go up one levelcd ..
Go back to the previous foldercd -Prints the folder it went back to
List filesls
List with detailsls -lPermissions, links, owner, group, size, date modified, name. Reading the permissions is explained below the reference
Include hidden filesls -AHidden files are the ones starting with a dot. ls -a also shows . and ..
Sizes in K, M and Gls -lh
Newest firstls -ltls -ltr puts the newest at the bottom, next to your prompt
Largest firstls -lS
Details of a folder itselfls -ld /etcWithout -d, ls lists what is inside it
Show the treetree -L 2Two levels deep. Not installed by default: sudo apt install tree. find . -maxdepth 2 works everywhere
Full path of a filerealpath notes.txtAlso resolves symbolic links
Which program a command runscommand -v python3type -a python3 lists every match on the PATH, plus aliases and builtins
Read the manualman lsq quits, / searches. man -k copy searches every manual's summary
Quick helpls --helpGNU tools only. The BSD tools on macOS reject --help

Files and folders

TaskCommandNotes
Create an empty filetouch notes.txtOn an existing file, it updates the modified time and leaves the contents alone
Make a foldermkdir photos
Make nested foldersmkdir -p projects/site/cssCreates each missing parent, and does not complain if it all exists already
Copy a filecp notes.txt notes-backup.txt
Copy a foldercp -r src backupIf backup already exists, the copy lands inside it, as backup/src
Copy, keeping permissions and timescp -a src backupArchive mode. Also copies symbolic links as links
Copy, never overwritingcp -n notes.txt backup/Coreutils 9.4, the Ubuntu 24.04 version, warns about -n but still skips. cp --update=none says the same without the warning
Ask before overwritingcp -i notes.txt backup/mv -i and rm -i ask too
Move or renamemv draft.txt final.txtOverwrites final.txt without asking if it exists
Move several into a foldermv *.jpg photos/
Delete a filerm notes.txtThere is no bin. It is gone
Delete a folder and everything in itrm -r buildrm -rf also skips every prompt and error about missing files. Read it twice before pressing Enter
Delete an empty folderrmdir photosRefuses if anything is inside, which makes it the safe choice
Symbolic linkln -s /opt/app/releases/v2 currentTarget first, then the link name. ls -l shows current -> /opt/app/releases/v2
Repoint a symbolic linkln -sfn /opt/app/releases/v3 current-n stops ln following the old link into the folder it points to
Hard linkln notes.txt notes-link.txtA second name for the same file. ls -li shows both with one inode number
What kind of file is thisfile photo.jpgReads the contents, not the extension
Size, owner and datesstat notes.txtstat -c '%s' notes.txt prints only the size in bytes. macOS spells it stat -f '%z'
File name from a pathbasename /var/log/syslogsyslog. basename report.tar.gz .gz removes the suffix too
Folder from a pathdirname /var/log/syslog/var/log
Temporary file or foldermktempCreates it under /tmp and prints its name. mktemp -d for a folder

Renaming many files at once is a job for a loop. The Bash cheat sheet has one that copes with spaces in names.

Reading and comparing files

TaskCommandNotes
Print a filecat notes.txtcat -n numbers the lines
Page through a fileless /var/log/syslogSpace for the next page, / to search, n for the next match, G for the end, q to quit
First lineshead -n 20 app.log10 without -n
Last linestail -n 20 app.log
Everything from line 30tail -n +30 app.log
Follow a growing logtail -f app.logCtrl+C stops it. tail -F keeps going when the log is rotated or recreated
Count lineswc -l app.logwc -l < app.log prints the number without the file name. -w counts words, -c bytes
Compare two filesdiff -u old.conf new.confLines starting - were removed, + were added. Exit status 1 means they differ
Compare side by sidediff -y old.conf new.conf
Checksumsha256sum ubuntu.isomacOS: shasum -a 256 ubuntu.iso
Check a download against its checksum filesha256sum -c SHA256SUMSPrints OK or FAILED for each file listed
Show invisible charactersod -c notes.txtA \r before each \n means Windows line endings

Permissions and ownership

Every file has an owner, a group and three sets of permissions: for the owner, for the group, and for everyone else. The section below the reference explains how to read them.

TaskCommandNotes
See permissionsls -l deploy.sh-rwxr-xr-x means the owner can read, write and run it; everyone else can read and run it
Permissions as a numberstat -c '%a %A' deploy.sh755 -rwxr-xr-x
Make a script runnablechmod +x deploy.sh
Owner writes, everyone readschmod 644 notes.txt-rw-r--r--. The usual mode for a file
Owner writes, everyone runschmod 755 deploy.sh-rwxr-xr-x. The usual mode for scripts and folders
Owner onlychmod 600 ~/.ssh/id_ed25519-rw-------. SSH refuses to use a private key that others can read
Owner only, for a folderchmod 700 ~/.ssh
Add or remove single permissionschmod u+x,g-w,o-rwx notes.txtu owner, g group, o others, a all of them. + adds, - removes, = sets exactly
Everything inside a folderchmod -R g+w shared/-R applies the same change to files and folders alike
Only the folders insidefind shared -type d -exec chmod 755 {} +And -type f with 644 for the files. Safer than chmod -R 755, which makes every file runnable
Change the ownersudo chown ada notes.txt
Change owner and groupsudo chown ada:developers notes.txtchown :developers notes.txt changes only the group, as does chgrp developers notes.txt
Change the owner of a whole foldersudo chown -R www-data:www-data /var/www/site
Default permissions for new filesumask0022 gives new files 644 and folders 755. 0002 gives 664 and 775. umask 027 changes it for this shell
Shared folder where people only delete their own fileschmod 1777 /srv/dropboxThe sticky bit, shown as t at the end: drwxrwxrwt. /tmp works this way
New files take the folder's groupchmod g+s /srv/teamsetgid on a folder, shown as s in the group's x position

Users, groups and sudo

TaskCommandNotes
Who am Iwhoami
My user and group IDsidid ada shows someone else's
Which groups I am ingroupsgroups ada for someone else. id -nG gives the same list
Run one command as rootsudo apt updateAsks for your own password, not root's, and remembers it for a few minutes
Run a command as another usersudo -u postgres psql
Open a root shellsudo -iexit returns to your own user. Better to sudo single commands
Edit a file that needs rootsudoedit /etc/hostsEdits a copy in your own editor, as you, then puts it back as root. Same as sudo -e
What may I run with sudosudo -l
Add a user to a groupsudo usermod -aG docker adaThe -a is essential: without it, -G replaces every other group ada was in. Takes effect at the next login
Create a usersudo useradd -m -s /bin/bash ada-m creates the home folder, -s sets the shell. On Debian and Ubuntu, sudo adduser ada asks for the rest interactively
Create a groupsudo groupadd developers
Set a passwordpasswdYour own. sudo passwd ada sets someone else's
Every user on the systemcut -d: -f1 /etc/passwdMost of them are system accounts that nobody logs in as

Finding files with find

find walks a folder tree and prints every path that passes all of its tests. The first argument is where to start; . means here.

TaskCommandNotes
By namefind . -name '*.log'Quote the pattern, or the shell expands it before find sees it. See the gotchas
By name, any casefind . -iname '*.jpg'Finds .JPG too
Files onlyfind . -type f -name '*.md'-type d for folders, -type l for symbolic links
Only this folder, no deeperfind . -maxdepth 1 -type fPut -maxdepth before the other tests
Changed in the last 7 daysfind . -type f -mtime -7-mtime +7 is more than 7 days ago
Changed in the last 30 minutesfind . -mmin -30
Newer than another filefind . -newer deploy.log
Bigger than 100 MBfind . -type f -size +100Mk, M and G suffixes. Sizes round up to whole units, so -size -1k matches only empty files
Empty files and foldersfind . -empty
Owned by someonefind /srv -user ada
Either of two namesfind . -name '*.log' -o -name '*.tmp'-o is or. Tests next to each other are and
Skip a folderfind . -name node_modules -prune -o -name '*.js' -printThe -print at the end is needed, or the pruned folder is printed too
Run a command on everything foundfind . -name '*.log' -exec gzip {} +{} is replaced by the paths. + passes many at once; \; runs the command once per file
Delete what it findsfind . -name '*.tmp' -deleteRun it without -delete first to see the list. -delete goes last
Hand the results to xargs safelyfind . -name '*.tmp' -print0 | xargs -0 rm-print0 and -0 separate names with a null byte, so spaces and newlines in names survive
Count themfind . -type f | wc -l
Stay on one diskfind / -xdev -type f -size +500M-xdev does not cross into other mounted file systems such as /proc

Searching text with grep

TaskCommandNotes
Lines containing a wordgrep error app.log
Ignore casegrep -i error app.logMatches ERROR, Error and error
Every file in a foldergrep -r TODO src-R also follows symbolic links
With line numbersgrep -rn TODO src
Only the file namesgrep -rl TODO src-L lists the files with no match
Whole words onlygrep -w log app.logSkips login and catalog
Lines that do not matchgrep -v DEBUG app.log
Count matching linesgrep -c error app.log
Lines around each matchgrep -C 3 Traceback app.log3 before and after. -B for before only, -A for after only
Several patternsgrep -E 'error|warn' app.log-E turns on extended regular expressions. grep -e error -e warn does the same
A fixed string, no regexgrep -F '[ERROR]' app.logBrackets, dots and stars are matched literally
Only the matching partgrep -oE '[0-9]+ ms' app.log
Stop after the first matchgrep -m 1 error app.log
Just test for a matchif grep -q error app.log; thenPrints nothing. Exit status 0 found, 1 not found, 2 an error such as a missing file
Only some filesgrep -r --include='*.py' TODO .Quote the pattern
Skip a foldergrep -r --exclude-dir=node_modules TODO .
Perl-style regexgrep -P '\d{3}-\d{4}' contacts.txtGNU grep only. macOS and BusyBox grep have no -P; use [0-9] with -E there

ripgrep (rg) is a faster grep -r that skips files listed in .gitignore. It is a separate install, usually called ripgrep in the package manager.

Editing text with sed

sed edits text as it streams past, one line at a time, and prints the result. The file is only changed with -i.

TaskCommandNotes
Replace the first match on each linesed 's/cat/dog/' pets.txt
Replace every matchsed 's/cat/dog/g' pets.txt
Edit the file in placesed -i 's/cat/dog/g' pets.txtGNU sed. On macOS write sed -i '' 's/cat/dog/g' pets.txt
In place, keeping a backupsed -i.bak 's/cat/dog/g' pets.txtLeaves the original in pets.txt.bak. Works in GNU and macOS sed alike
Ignore casesed 's/cat/dog/gI' pets.txtGNU sed only
Paths with slashes in themsed 's|/usr/local|/opt|g' paths.txtAny character after the s can be the separator
Reorder with groupssed -E 's/([0-9]+)-([0-9]+)/\2-\1/' dates.txt2024-10 becomes 10-2024. -E so the brackets and + need no backslashes
Delete matching linessed '/^#/d' config.iniDrops comment lines
Delete blank linessed '/^$/d' notes.txt
Delete lines 2 to 5sed '2,5d' notes.txt
Print only lines 10 to 20sed -n '10,20p' app.log-n stops sed printing every line, so only p prints
Print the last linesed -n '$p' app.log
Strip trailing spacessed 's/[[:space:]]*$//' notes.txt
Indent every linesed 's/^/ /' notes.txt
Add a line at the topsed '1i # generated' config.iniGNU syntax. macOS sed needs 1i\ and the text on the next line
Several edits in one passsed -e 's/cat/dog/g' -e '/^#/d' pets.txt

Columns and totals with awk

awk splits every line into fields, $1, $2 and so on, on runs of spaces unless you say otherwise. $0 is the whole line, NR the line number and NF the number of fields.

TaskCommandNotes
Print a columnawk '{print $1}' access.logSingle quotes, so the shell leaves $1 alone
Several columnsawk '{print $1, $9}' access.logThe comma puts a space between them
Split on commasawk -F, '{print $2}' people.csv-F: for /etc/passwd. Quoted CSV fields with commas inside are not handled
Last columnawk '{print $NF}' access.log
Rows where a column matchesawk '$9 >= 500' access.logNo action means print the line
Rows where a column equals a stringawk '$3 == "london" {print $1}' people.txt
Skip the header rowawk -F, 'NR > 1 {print $1}' people.csv
Lines 10 to 20awk 'NR >= 10 && NR <= 20' app.log
Lines longer than 80 charactersawk 'length > 80' main.py
Sum a columnawk '{sum += $10} END {print sum}' access.logEND runs once, after the last line
Average a columnawk '{sum += $2} END {print sum / NR}' scores.txt
Count by a columnawk '{count[$1]++} END {for (k in count) print k, count[k]}' access.logThe output is in no particular order. Pipe it to sort
Remove duplicate lines, keeping orderawk '!seen[$0]++' names.txtUnlike sort -u, the first of each line stays where it was
Change the output separatorawk -F: -v OFS=, '{print $1, $7}' /etc/passwdroot,/bin/bash
Format the outputawk '{printf "%-10s %5d\n", $1, $2}' scores.txtprintf needs its own \n
Decimalsawk 'BEGIN {printf "%.2f\n", 10 / 3}'3.33. BEGIN runs before any input, so no file is needed

cut, sort, uniq, tr and xargs

TaskCommandNotes
Cut out fieldscut -d, -f1,3 people.csv-d sets the separator, one character only. -f2- means field 2 onwards
Cut out characterscut -c1-10 app.log
Sort linessort names.txtThe order depends on your locale. LC_ALL=C sort sorts by byte, capitals first
Sort numberssort -n sizes.txtWithout -n, 10 sorts before 9
Largest firstsort -rn sizes.txt
Sort sizes like 1K, 2M, 3Gdu -sh * | sort -h
Sort by a columnsort -t, -k2,2n people.csv-k2,2 means column 2 only. Without the ,2 it sorts from column 2 to the end of the line
Sort version numberssort -V versions.txtv1.2, v1.9, v1.10, not v1.10 first
Sort and remove duplicatessort -u names.txt
Sort a file in placesort -o names.txt names.txtNot sort names.txt > names.txt, which empties the file first
Remove repeated linessort names.txt | uniquniq only removes duplicates that are next to each other, so sort first
Count each distinct linesort names.txt | uniq -c | sort -rnThe most common first
Only the duplicated linessort names.txt | uniq -duniq -u for the lines that appear once
Lines in both filescomm -12 a.txt b.txtBoth files must be sorted. comm -23 gives lines only in the first
Upper casetr 'a-z' 'A-Z' < notes.txttr only reads its input, so it takes < or a pipe, never a file name
Delete characterstr -d '\r' < windows.txt > unix.txtRemoves Windows line endings
Squeeze repeated spacestr -s ' ' < table.txt
One item per lineecho "$PATH" | tr ':' '\n'
Join lines with commaspaste -sd, names.txt
Line up columnscolumn -t -s, people.csvIn the bsdextrautils package on Debian and Ubuntu
Turn lines into argumentscat urls.txt | xargs -n 1 curl -O-n 1 runs curl once per line
Put each line in a set placexargs -I{} cp {} {}.bak < files.txt
Several at a timexargs -P 4 -n 1 gzip < files.txtFour gzips at once
Do nothing when there is no inputxargs -r killGNU xargs otherwise runs the command once with no arguments

Processes and signals

Every running program is a process with a number, its PID. You stop or nudge one by sending it a signal.

TaskCommandNotes
Every processps auxps -ef is the same list in another layout
Top memory usersps aux --sort=-%mem | head--sort=-%cpu for CPU. GNU procps only
Pick the columnsps -eo pid,ppid,%cpu,%mem,cmd --sort=-%cpu
As a treeps -ef --forestpstree -p is more compact. It is in the psmisc package
Find a process ID by namepgrep -a nginx-a shows the command line. -f matches against the whole command line instead of the name
Live viewtopq quits, M sorts by memory, P by CPU. htop is friendlier, and a separate install
Ask a process to stopkill 4127Sends TERM (15). A well-behaved program cleans up and exits
Force it to stopkill -9 4127KILL (9) cannot be caught, so there is no clean-up. Try plain kill first
Ask it to reloadkill -HUP 4127Many daemons, nginx among them, reread their config on HUP
Stop by namepkill -f 'python app.py'Run pgrep -af with the same pattern first to see what it matches
Stop every process with a namekillall nodepsmisc package. On some other Unix systems killall kills everything, so prefer pkill
List the signalskill -lkill -l 15 prints TERM
Stop the program in frontCtrl+CSends INT (2). Exit status 130
Pause itCtrl+ZThen bg to carry on in the background, fg to bring it back, jobs to list them
Keep running after you log outnohup ./long-task.sh > task.log 2>&1 &For anything longer, tmux or screen lets you come back to it
Run at lower prioritynice -n 10 ./backup.sh19 is the lowest priority. renice -n 5 -p 4127 changes a running one
Give up after a time limittimeout 30s ./check.shExit status 124 if the time ran out
Time a commandtime ./build.sh
Rerun every 2 secondswatch -n 2 'df -h'Ctrl+C stops it
Load and uptimeuptimeThe three load averages are for 1, 5 and 15 minutes. Compare them with nproc
Which process has a file openlsof /var/log/syslog

Services and logs (systemd)

Most current distributions, Ubuntu, Debian, Fedora and Arch among them, manage background services with systemd. These rows were checked against systemd 255's own help text rather than run.

TaskCommandNotes
Is it runningsystemctl status nginxShows the state, the PID and the last few log lines
Start, stop, restartsudo systemctl restart nginxOr start, stop
Reload the config without a restartsudo systemctl reload nginxOnly for services that support it. reload-or-restart falls back
Start at bootsudo systemctl enable --now nginx--now also starts it straight away. disable --now undoes both
Just the state, for a scriptsystemctl is-active nginxPrints active or inactive, with a matching exit status
Everything that failedsystemctl --failed
Every running servicesystemctl list-units --type=service --state=running
After editing a unit filesudo systemctl daemon-reload
A service's logsjournalctl -u nginx-e jumps to the end
Follow its logsjournalctl -u nginx -f
Logs since a timejournalctl -u nginx --since '1 hour ago'
Errors since the last bootjournalctl -b -p err
The last 50 linesjournalctl -n 50

Disk, memory and system

TaskCommandNotes
Free space on every diskdf -hdf -h . for the disk the current folder is on
Free inodesdf -iA disk can have space left and still be full, if it runs out of inodes. See the gotchas
File system typesdf -hT
Size of a folderdu -sh ~/Downloads
Size of each folder in heredu -h -d 1 | sort -h-d 1 works in GNU and macOS du. GNU also spells it --max-depth=1
Biggest things in heredu -sh * | sort -rh | headSkips hidden files and folders
Memoryfree -hLook at available, not free. Linux uses spare memory as a disk cache and hands it back when needed
Memory and CPU over timevmstat 1 5Five samples, one second apart
Swapswapon --showPrints nothing when there is no swap
Disks and partitionslsblklsblk -f adds file systems, labels and UUIDs
What is mounted wherefindmntfindmnt / for one mount point. mount with no arguments lists everything, less tidily
Mount a disksudo mount /dev/sdb1 /mnt/usbThe folder must exist. For a permanent mount, add it to /etc/fstab
Unmount itsudo umount /mnt/usbumount, not unmount
Which distributioncat /etc/os-release
Kernel versionuname -runame -a for everything, including the architecture
CPU countnproclscpu for the model, cores and caches

SSH, scp and rsync

SSH gives you a shell on another machine, encrypted. scp and rsync copy files over the same connection. The section below the reference sets up keys and a config file.

TaskCommandNotes
Log inssh ada@203.0.113.10exit or Ctrl+D logs out
On another portssh -p 2222 ada@203.0.113.10
With a particular keyssh -i ~/.ssh/deploy_key ada@203.0.113.10
Run one command theressh web1 'df -h /'Quote it, so the pipes and variables run there, not here
Run a local script theressh web1 'bash -s' < setup.sh
Make a key pairssh-keygen -t ed25519 -C 'ada@laptop'Writes ~/.ssh/id_ed25519 (private, never share it) and id_ed25519.pub (public)
Install your public key on a serverssh-copy-id ada@203.0.113.10Asks for the password one last time. Add -p 2222 for another port
Key fingerprintssh-keygen -lf ~/.ssh/id_ed25519.pub
Reach a port on the server as if it were localssh -N -L 8080:localhost:80 web1Then open localhost:8080. -N means no shell, just the tunnel
Through a jump hostssh -J bastion web1The same as ProxyJump in the config file
Copy a file upscp report.pdf web1:/tmp/
Copy a file downscp web1:/var/log/nginx/error.log .
Copy a folderscp -r site web1:/tmp/
Sync a folderrsync -avz site/ web1:/var/www/site/Only sends what changed. The trailing slash on site/ means its contents; without it you get /var/www/site/site
Make the copy match exactlyrsync -avz --delete site/ web1:/var/www/site/Deletes files at the far end that are not here. Add -n first for a dry run
Show progressrsync -av --progress big.iso web1:/tmp/rsync also works between two local folders
Copy a folder through a pipetar -czf - project | ssh web1 'tar -xzf - -C /srv'No temporary archive on either side

curl, ports and network checks

TaskCommandNotes
Fetch a URLcurl https://example.com
Save it to a filecurl -o page.html https://example.com-O saves it under the name in the URL
Follow redirectscurl -L https://example.com/oldWithout -L, curl prints the redirect response and stops
Only the headerscurl -I https://example.com
Fail on HTTP errorscurl -fsS https://example.com/healthWithout -f, a 404 or 500 still exits 0. -s hides the progress bar, -S still shows errors
Just the status codecurl -s -o /dev/null -w '%{http_code}\n' https://example.com
POST JSONcurl --json '{"name":"Ada"}' https://api.example.com/userscurl 7.82 and later. Sets the Content-Type and Accept headers for you
POST JSON on an older curlcurl -H 'Content-Type: application/json' -d '{"name":"Ada"}' https://api.example.com/users-d makes it a POST, so -X POST is not needed
Send a formcurl -d 'name=Ada' https://example.com/signup
Another methodcurl -X DELETE https://api.example.com/users/42
With a tokencurl -H "Authorization: Bearer $TOKEN" https://api.example.com/me
With a user name and passwordcurl -u ada https://example.com/privateAsks for the password, so it stays out of your shell history
See the whole conversationcurl -v https://example.comLines starting > are what curl sent, < what came back
Download, resuming if it stopswget -c https://example.com/big.isocurl -C - -O does the same
Which ports are listeningsudo ss -tulpnt TCP, u UDP, l listening, p the process, n numbers not names. sudo shows other users' processes
What is using port 8080sudo lsof -i :8080lsof -ti :8080 prints only the PID
Is a port opennc -zv db1 5432Connects and closes. Exit status 0 if it is open
My IP addressesip -br aip a for the full detail. ifconfig is older and often not installed
Routes and the default gatewayip r
Is a host reachableping -c 4 example.com-c 4 stops after four. Without it, Ctrl+C stops it
Look up a domaindig +short example.comdig +short MX example.com for mail servers. In bind9-dnsutils on Debian and Ubuntu
Reverse lookupdig -x 1.1.1.1 +short
Ask a particular DNS serverdig @1.1.1.1 example.com

Package managers: apt, dnf and pacman

The commands column is apt, for Debian and Ubuntu. The notes give dnf, for Fedora and Red Hat, and pacman, for Arch. The apt rows were run on Ubuntu 24.04; dnf rows that need no network were run on Fedora 44; pacman rows are from its manual and were not run.

TaskDebian, Ubuntu (apt)Fedora (dnf) and Arch (pacman)
Refresh the package listsudo apt updatednf check-upgrade, though dnf refreshes on its own. pacman: sudo pacman -Sy, but only ever as part of -Syu
Upgrade everythingsudo apt upgradesudo dnf upgrade. sudo pacman -Syu
Installsudo apt install nginxsudo dnf install nginx. sudo pacman -S nginx
Removesudo apt remove nginxsudo dnf remove nginx. sudo pacman -R nginx
Remove with its config filessudo apt purge nginxdnf remove leaves edited config as .rpmsave. sudo pacman -Rns nginx also removes unneeded dependencies
Remove what nothing needs any moresudo apt autoremovesudo dnf autoremove. sudo pacman -Rns $(pacman -Qdtq)
Searchapt search ripgrepdnf search ripgrep. pacman -Ss ripgrep
Details of a packageapt show nginxdnf info nginx. pacman -Si nginx, or -Qi once installed
List what is installedapt list --installeddnf list --installed. pacman -Q
Which package a file came fromdpkg -S /usr/bin/curlrpm -qf /usr/bin/curl. pacman -Qo /usr/bin/curl
Files a package installeddpkg -L curlrpm -ql curl. pacman -Ql curl
Install a downloaded packagesudo apt install ./app.debThe ./ matters: without it apt looks for a package by that name. sudo dnf install ./app.rpm. sudo pacman -U app.pkg.tar.zst
Which version would installapt policy nginxdnf list nginx
Clear the download cachesudo apt cleansudo dnf clean all. sudo pacman -Sc

In scripts, use apt-get rather than apt. apt warns that its command-line output is not stable and may change; apt-get's is.

Archives and compression

tar bundles many files into one; gzip, xz and zstd compress a single file. A .tar.gz is both. In tar, c creates, x extracts, t lists, f names the file, and z, J or --zstd picks the compression.

TaskCommandNotes
Create a .tar.gztar -czf project.tar.gz project/
Extract onetar -xf project.tar.gzGNU tar spots the compression by itself, so x and f are enough
Extract into a foldertar -xf project.tar.gz -C /optThe folder has to exist already
List what is insidetar -tf project.tar.gztar -tvf adds sizes, owners and dates
Extract one filetar -xf project.tar.gz project/src/main.pyThe path exactly as tar -tf lists it
Drop the top folder when extractingtar -xf project.tar.gz --strip-components=1
Leave something outtar --exclude=node_modules -czf project.tar.gz project/
Smaller, slower: .tar.xztar -cJf project.tar.xz project/
Fast and small: .tar.zsttar --zstd -cf project.tar.zst project/Needs zstd installed. tar -caf project.tar.zst project/ picks the compression from the name
Compress one filegzip app.logReplaces it with app.log.gz. gzip -k keeps the original
Decompress itgunzip app.log.gzOr gzip -d. xz and unxz, zstd and unzstd work the same way
Read or search it without decompressingzcat app.log.gzzgrep error app.log.gz and zless work too
Create a .zipzip -r site.zip site/-x 'site/node_modules/*' leaves a folder out
Extract a .zipunzip site.zip -d site-copyunzip -l site.zip lists it

Reading ls -l and permission numbers

ls -l starts every line with ten characters. Take -rwxr-xr--:

CharactersWhoHereMeans
--A file. d for a folder, l for a symbolic link
rwxThe ownerrwxRead, write and run it
r-xThe file's groupr-xRead and run it, not change it
r--Everyone elser--Read it only

Each of the three sets is one digit in a chmod number: read is 4, write is 2 and execute is 1, added together. So rwx is 7, r-x is 5, r-- is 4, and the line above is chmod 754.

NumberShown asUse it for
644-rw-r--r--Ordinary files
600-rw-------Private keys, passwords, .env files
755-rwxr-xr-xScripts, programs and most folders
700-rwx------Private folders, such as ~/.ssh
775-rwxrwxr-xFolders a team shares through a group
777-rwxrwxrwxAlmost never. Anyone on the machine can change it

On a folder, the letters mean something slightly different. r lets you list the names inside, w lets you create, rename and delete files in it, and x lets you go into it and reach the files at all. A folder with r but no x shows you names you cannot open. And deleting a file is controlled by the folder's w, not the file's own permissions, which is why /tmp sets the sticky bit.

Pipelines worth knowing

Most real jobs are two to five of these commands joined with |, each doing one small thing. How pipes and redirection work is on the Bash cheat sheet; these are the combinations that come up again and again, every one run on the versions above.

# The five IP addresses that sent the most requests (nginx or Apache access log)
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 5
 
# How many responses of each status code
awk '{print $9}' access.log | sort | uniq -c
 
# Rename a function across a whole project, safely with spaces in file names
grep -rlZ 'old_name' src | xargs -0 sed -i 's/old_name/new_name/g'
 
# Stop whatever is listening on port 3000, and do nothing if nothing is
lsof -ti :3000 | xargs -r kill
 
# Compress logs older than a week
find /var/log/myapp -name '*.log' -mtime +7 -exec gzip {} +
 
# The ten biggest folders under /var, and the ten biggest files on this disk
sudo du -h -d 1 /var | sort -rh | head -n 10
sudo find / -xdev -type f -size +100M -exec du -h {} + 2>/dev/null | sort -rh | head -n 10
 
# Watch a log for errors as they happen
tail -F app.log | grep --line-buffered -i error
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 5
      3 203.0.113.7
      2 198.51.100.2
      1 192.0.2.10

sort | uniq -c | sort -rn is the one to remember: sort so duplicates sit together, count each run, then sort the counts biggest first. GNU sort uses a merge sort, which is why it copes with files bigger than your memory. --line-buffered in the last one makes grep pass each match on straight away instead of saving them up. When the logic outgrows a one-liner, a short script in the Python cheat sheet is usually easier to read than a longer pipe. For data you want to query rather than scan, loading it into SQLite and using the SQL cheat sheet is often quicker than any of these.

SSH keys and a config file

Logging in with a key instead of a password is safer and saves typing. Three steps, on your own machine:

# 1. Make a key pair. Press Enter for the default location, then pick a passphrase.
ssh-keygen -t ed25519 -C "ada@laptop"
 
# 2. Copy the public half to the server. This is the last time it asks for the password.
ssh-copy-id -p 2222 ada@203.0.113.10
 
# 3. Log in with the key
ssh -p 2222 ada@203.0.113.10

Then give each server a short name in ~/.ssh/config, so every ssh, scp and rsync command can use it:

Host web1
    HostName 203.0.113.10
    User ada
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
 
Host db1
    HostName 10.0.0.5
    User ada
    ProxyJump web1
ssh web1                              # logs in with the right user, port and key
scp report.pdf web1:/tmp/
rsync -avz site/ web1:/var/www/site/
ssh db1                               # goes through web1 to reach a private address

chmod 600 ~/.ssh/config if SSH complains about its permissions, and the same for the private key. SSH refuses a private key that other users can read, with a WARNING: UNPROTECTED PRIVATE KEY FILE! banner. The same key works for pushing to GitHub or GitLab over SSH; add the .pub file in their settings, and the Git cheat sheet takes it from there.

Linux and macOS: GNU and BSD tools

The commands on a Mac come from BSD, not GNU. They agree on the basics and differ at the edges. Alpine Linux, common in Docker images, uses BusyBox instead, which is smaller again: grep -P, find -printf, ps --sort and tar --zstd all fail there. apk add coreutils grep findutils procps tar installs the GNU versions.

TaskLinux (GNU)macOS (BSD)
Edit a file in placesed -i 's/a/b/' fsed -i '' 's/a/b/' f
Edit in place, portablesed -i.bak 's/a/b/' fsed -i.bak 's/a/b/' f
Perl-style regexgrep -P '\d+'Not available. grep -E '[0-9]+'
Folder sizes, one leveldu -h -d 1du -h -d 1
File size in bytesstat -c '%s' fstat -f '%z' f
Yesterday's datedate -d yesterday +%Fdate -v-1d +%F
Processes by memoryps aux --sort=-%memps aux -m
Memoryfree -hvm_stat, or Activity Monitor
Listening portsss -tulpnlsof -iTCP -sTCP:LISTEN -n -P
IP addressesip -br aifconfig
SHA-256 checksumsha256sum fshasum -a 256 f
Servicessystemctllaunchctl
Packagesapt, dnf, pacmanbrew, installed separately

If a script has to run on both, stick to the options in the portable rows, or install the GNU tools on the Mac with brew install coreutils gnu-sed grep findutils and call them as gsed, ggrep and gfind.

Gotchas

The mistakes almost everyone makes once. Each row was reproduced on the versions above.

Looks rightWhat actually happensDo this instead
find . -name *.txtThe shell expands *.txt first. With one match here, find looks only for that name; with two, paths must precede expressionfind . -name '*.txt'
sort names.txt > names.txtThe file is empty: > truncates it before sort reads itsort -o names.txt names.txt
uniq names.txt on unsorted inputOnly removes duplicates that sit next to each othersort names.txt | uniq, or sort -u
sort on numbers10 sorts before 9sort -n, or sort -h for 1K, 2M
The same sort on two machinesDifferent orders: LC_ALL=C puts capitals first, en_GB.UTF-8 mixes the casesLC_ALL=C sort when the order has to match
sudo usermod -G docker adaRemoves ada from every other groupsudo usermod -aG docker ada
Adding yourself to a groupid still does not show itLog out and back in
chmod 644 ~/.ssh/id_ed25519SSH refuses the key: UNPROTECTED PRIVATE KEY FILEchmod 600
chmod -R 755 site/Every file becomes runnablefind site -type d -exec chmod 755 {} + and -type f with 644
pkill -f deployAlso kills anything else whose command line mentions deploy, which can include the shell that ran itpgrep -af deploy first, then a narrower pattern
kill -9 firstThe program gets no chance to remove lock files or finish writingPlain kill, and -9 only if it ignores that
No space left on device, but df -h shows spaceThe disk has run out of inodes, usually from millions of tiny filesdf -i, then find and delete the small files
Deleted a huge log, df shows no changeA running program still has it open, so the space is not freedsudo lsof +L1, then restart that program
rsync -a site web1:/var/www/site/Creates /var/www/site/sitersync -a site/ web1:/var/www/site/
tar -xf backup.tar.gz -C restoreCannot open: No such file or directory when restore is missingmkdir -p restore first
curl -s "$url" in a scriptA 404 or 500 still exits 0, so the script carries oncurl -fsS
apt in a script or DockerfileWarns that its output is not stableapt-get

Common questions

Which Linux versions does this cheat sheet cover?

Every command was run on Ubuntu 24.04 LTS, with GNU coreutils 9.4, grep 3.11, sed 4.9, findutils 4.9.0, tar 1.35, util-linux 2.39.3, procps-ng 4.0.4, iproute2 6.1.0, OpenSSH 9.6 and curl 8.5. The permission and user commands were also run on Debian 13, and the rpm and offline dnf commands on Fedora 44. The systemd rows were checked against systemd 255's help text, and the pacman rows against its manual, without being run. The same GNU tools ship on almost every distribution, so nearly everything here works the same on any of them.

What is the difference between Linux commands and Bash?

Bash is the shell: the program that reads what you type, expands variables and wildcards, and starts other programs. Most Linux commands, such as ls, grep, find and tar, are separate programs that any shell can start. This page covers those programs. Quoting, variables, loops and redirection belong to the shell and are on the Bash cheat sheet.

What does chmod 755 mean?

Each digit sets the permissions for one group of people: the owner, then the file's group, then everyone else. Each digit adds up read (4), write (2) and execute (1). So 7 is read, write and execute, and 5 is read and execute. chmod 755 lets the owner do anything and lets everyone else read and run the file, which is the usual setting for scripts and folders. 644 is the usual one for ordinary files.

How do I find which process is using a port?

Run sudo ss -tulpn to see every listening port with the process that owns it, or sudo lsof -i :8080 for one port. lsof -ti :8080 prints only the process ID, so lsof -ti :8080 | xargs -r kill stops whatever is holding it. Without sudo you only see your own processes.

How do I find what is filling up my disk?

Start with df -h to see which disk is full. Then go to the top of that disk and run sudo du -h -d 1 | sort -h to see which folder is biggest, and repeat inside it. To list single large files, use sudo find / -xdev -type f -size +500M. If df says the disk is full but du cannot find the space, check df -i for inodes, and sudo lsof +L1 for deleted files that a running program still holds open.

What is the difference between apt and apt-get?

They use the same package system underneath. apt is the newer command for people at a terminal: it has a progress bar and combines the common parts of apt-get and apt-cache. apt-get has a stable output format, so it is the one to use in scripts and Dockerfiles. apt prints a warning saying exactly that when its output is piped.

Do these commands work on macOS?

Most of them, with differences. macOS has the BSD versions of tools such as sed, grep, find, stat and date, which share the common options but not every GNU one. The table on this page lists the differences that catch people most often. Linux-only tools such as free, ss, ip and systemctl do not exist on macOS at all. brew install coreutils gnu-sed grep findutils installs the GNU versions, prefixed with g: gsed, ggrep, gfind.

Where can I practise Linux commands safely?

In a throwaway container, if you have Docker: docker run --rm -it ubuntu:24.04 bash gives you a fresh Ubuntu shell that is deleted when you exit, so nothing you break matters. On Windows, WSL runs a full Linux distribution alongside Windows. On a Mac, the Terminal app runs most of these commands already, with the differences listed on this page.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.