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.
Searches the task, the command and the third column. Press / from anywhere on the page.
287 commands
Navigating the file system
Everything on Linux lives in one tree that starts at /. Your home folder is /home/yourname, written ~ for short.
| Task | Command | Notes |
|---|---|---|
| Where am I | pwd | Print working directory: the full path of the current folder |
| Go to a folder | cd /var/log | |
| Go home | cd | cd ~ does the same |
| Go up one level | cd .. | |
| Go back to the previous folder | cd - | Prints the folder it went back to |
| List files | ls | |
| List with details | ls -l | Permissions, links, owner, group, size, date modified, name. Reading the permissions is explained below the reference |
| Include hidden files | ls -A | Hidden files are the ones starting with a dot. ls -a also shows . and .. |
| Sizes in K, M and G | ls -lh | |
| Newest first | ls -lt | ls -ltr puts the newest at the bottom, next to your prompt |
| Largest first | ls -lS | |
| Details of a folder itself | ls -ld /etc | Without -d, ls lists what is inside it |
| Show the tree | tree -L 2 | Two levels deep. Not installed by default: sudo apt install tree. find . -maxdepth 2 works everywhere |
| Full path of a file | realpath notes.txt | Also resolves symbolic links |
| Which program a command runs | command -v python3 | type -a python3 lists every match on the PATH, plus aliases and builtins |
| Read the manual | man ls | q quits, / searches. man -k copy searches every manual's summary |
| Quick help | ls --help | GNU tools only. The BSD tools on macOS reject --help |
Files and folders
| Task | Command | Notes |
|---|---|---|
| Create an empty file | touch notes.txt | On an existing file, it updates the modified time and leaves the contents alone |
| Make a folder | mkdir photos | |
| Make nested folders | mkdir -p projects/site/css | Creates each missing parent, and does not complain if it all exists already |
| Copy a file | cp notes.txt notes-backup.txt | |
| Copy a folder | cp -r src backup | If backup already exists, the copy lands inside it, as backup/src |
| Copy, keeping permissions and times | cp -a src backup | Archive mode. Also copies symbolic links as links |
| Copy, never overwriting | cp -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 overwriting | cp -i notes.txt backup/ | mv -i and rm -i ask too |
| Move or rename | mv draft.txt final.txt | Overwrites final.txt without asking if it exists |
| Move several into a folder | mv *.jpg photos/ | |
| Delete a file | rm notes.txt | There is no bin. It is gone |
| Delete a folder and everything in it | rm -r build | rm -rf also skips every prompt and error about missing files. Read it twice before pressing Enter |
| Delete an empty folder | rmdir photos | Refuses if anything is inside, which makes it the safe choice |
| Symbolic link | ln -s /opt/app/releases/v2 current | Target first, then the link name. ls -l shows current -> /opt/app/releases/v2 |
| Repoint a symbolic link | ln -sfn /opt/app/releases/v3 current | -n stops ln following the old link into the folder it points to |
| Hard link | ln notes.txt notes-link.txt | A second name for the same file. ls -li shows both with one inode number |
| What kind of file is this | file photo.jpg | Reads the contents, not the extension |
| Size, owner and dates | stat notes.txt | stat -c '%s' notes.txt prints only the size in bytes. macOS spells it stat -f '%z' |
| File name from a path | basename /var/log/syslog | syslog. basename report.tar.gz .gz removes the suffix too |
| Folder from a path | dirname /var/log/syslog | /var/log |
| Temporary file or folder | mktemp | Creates 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
| Task | Command | Notes |
|---|---|---|
| Print a file | cat notes.txt | cat -n numbers the lines |
| Page through a file | less /var/log/syslog | Space for the next page, / to search, n for the next match, G for the end, q to quit |
| First lines | head -n 20 app.log | 10 without -n |
| Last lines | tail -n 20 app.log | |
| Everything from line 30 | tail -n +30 app.log | |
| Follow a growing log | tail -f app.log | Ctrl+C stops it. tail -F keeps going when the log is rotated or recreated |
| Count lines | wc -l app.log | wc -l < app.log prints the number without the file name. -w counts words, -c bytes |
| Compare two files | diff -u old.conf new.conf | Lines starting - were removed, + were added. Exit status 1 means they differ |
| Compare side by side | diff -y old.conf new.conf | |
| Checksum | sha256sum ubuntu.iso | macOS: shasum -a 256 ubuntu.iso |
| Check a download against its checksum file | sha256sum -c SHA256SUMS | Prints OK or FAILED for each file listed |
| Show invisible characters | od -c notes.txt | A \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.
| Task | Command | Notes |
|---|---|---|
| See permissions | ls -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 number | stat -c '%a %A' deploy.sh | 755 -rwxr-xr-x |
| Make a script runnable | chmod +x deploy.sh | |
| Owner writes, everyone reads | chmod 644 notes.txt | -rw-r--r--. The usual mode for a file |
| Owner writes, everyone runs | chmod 755 deploy.sh | -rwxr-xr-x. The usual mode for scripts and folders |
| Owner only | chmod 600 ~/.ssh/id_ed25519 | -rw-------. SSH refuses to use a private key that others can read |
| Owner only, for a folder | chmod 700 ~/.ssh | |
| Add or remove single permissions | chmod u+x,g-w,o-rwx notes.txt | u owner, g group, o others, a all of them. + adds, - removes, = sets exactly |
| Everything inside a folder | chmod -R g+w shared/ | -R applies the same change to files and folders alike |
| Only the folders inside | find 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 owner | sudo chown ada notes.txt | |
| Change owner and group | sudo chown ada:developers notes.txt | chown :developers notes.txt changes only the group, as does chgrp developers notes.txt |
| Change the owner of a whole folder | sudo chown -R www-data:www-data /var/www/site | |
| Default permissions for new files | umask | 0022 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 files | chmod 1777 /srv/dropbox | The sticky bit, shown as t at the end: drwxrwxrwt. /tmp works this way |
| New files take the folder's group | chmod g+s /srv/team | setgid on a folder, shown as s in the group's x position |
Users, groups and sudo
| Task | Command | Notes |
|---|---|---|
| Who am I | whoami | |
| My user and group IDs | id | id ada shows someone else's |
| Which groups I am in | groups | groups ada for someone else. id -nG gives the same list |
| Run one command as root | sudo apt update | Asks for your own password, not root's, and remembers it for a few minutes |
| Run a command as another user | sudo -u postgres psql | |
| Open a root shell | sudo -i | exit returns to your own user. Better to sudo single commands |
| Edit a file that needs root | sudoedit /etc/hosts | Edits a copy in your own editor, as you, then puts it back as root. Same as sudo -e |
| What may I run with sudo | sudo -l | |
| Add a user to a group | sudo usermod -aG docker ada | The -a is essential: without it, -G replaces every other group ada was in. Takes effect at the next login |
| Create a user | sudo 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 group | sudo groupadd developers | |
| Set a password | passwd | Your own. sudo passwd ada sets someone else's |
| Every user on the system | cut -d: -f1 /etc/passwd | Most 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.
| Task | Command | Notes |
|---|---|---|
| By name | find . -name '*.log' | Quote the pattern, or the shell expands it before find sees it. See the gotchas |
| By name, any case | find . -iname '*.jpg' | Finds .JPG too |
| Files only | find . -type f -name '*.md' | -type d for folders, -type l for symbolic links |
| Only this folder, no deeper | find . -maxdepth 1 -type f | Put -maxdepth before the other tests |
| Changed in the last 7 days | find . -type f -mtime -7 | -mtime +7 is more than 7 days ago |
| Changed in the last 30 minutes | find . -mmin -30 | |
| Newer than another file | find . -newer deploy.log | |
| Bigger than 100 MB | find . -type f -size +100M | k, M and G suffixes. Sizes round up to whole units, so -size -1k matches only empty files |
| Empty files and folders | find . -empty | |
| Owned by someone | find /srv -user ada | |
| Either of two names | find . -name '*.log' -o -name '*.tmp' | -o is or. Tests next to each other are and |
| Skip a folder | find . -name node_modules -prune -o -name '*.js' -print | The -print at the end is needed, or the pruned folder is printed too |
| Run a command on everything found | find . -name '*.log' -exec gzip {} + | {} is replaced by the paths. + passes many at once; \; runs the command once per file |
| Delete what it finds | find . -name '*.tmp' -delete | Run it without -delete first to see the list. -delete goes last |
| Hand the results to xargs safely | find . -name '*.tmp' -print0 | xargs -0 rm | -print0 and -0 separate names with a null byte, so spaces and newlines in names survive |
| Count them | find . -type f | wc -l | |
| Stay on one disk | find / -xdev -type f -size +500M | -xdev does not cross into other mounted file systems such as /proc |
Searching text with grep
| Task | Command | Notes |
|---|---|---|
| Lines containing a word | grep error app.log | |
| Ignore case | grep -i error app.log | Matches ERROR, Error and error |
| Every file in a folder | grep -r TODO src | -R also follows symbolic links |
| With line numbers | grep -rn TODO src | |
| Only the file names | grep -rl TODO src | -L lists the files with no match |
| Whole words only | grep -w log app.log | Skips login and catalog |
| Lines that do not match | grep -v DEBUG app.log | |
| Count matching lines | grep -c error app.log | |
| Lines around each match | grep -C 3 Traceback app.log | 3 before and after. -B for before only, -A for after only |
| Several patterns | grep -E 'error|warn' app.log | -E turns on extended regular expressions. grep -e error -e warn does the same |
| A fixed string, no regex | grep -F '[ERROR]' app.log | Brackets, dots and stars are matched literally |
| Only the matching part | grep -oE '[0-9]+ ms' app.log | |
| Stop after the first match | grep -m 1 error app.log | |
| Just test for a match | if grep -q error app.log; then | Prints nothing. Exit status 0 found, 1 not found, 2 an error such as a missing file |
| Only some files | grep -r --include='*.py' TODO . | Quote the pattern |
| Skip a folder | grep -r --exclude-dir=node_modules TODO . | |
| Perl-style regex | grep -P '\d{3}-\d{4}' contacts.txt | GNU 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.
| Task | Command | Notes |
|---|---|---|
| Replace the first match on each line | sed 's/cat/dog/' pets.txt | |
| Replace every match | sed 's/cat/dog/g' pets.txt | |
| Edit the file in place | sed -i 's/cat/dog/g' pets.txt | GNU sed. On macOS write sed -i '' 's/cat/dog/g' pets.txt |
| In place, keeping a backup | sed -i.bak 's/cat/dog/g' pets.txt | Leaves the original in pets.txt.bak. Works in GNU and macOS sed alike |
| Ignore case | sed 's/cat/dog/gI' pets.txt | GNU sed only |
| Paths with slashes in them | sed 's|/usr/local|/opt|g' paths.txt | Any character after the s can be the separator |
| Reorder with groups | sed -E 's/([0-9]+)-([0-9]+)/\2-\1/' dates.txt | 2024-10 becomes 10-2024. -E so the brackets and + need no backslashes |
| Delete matching lines | sed '/^#/d' config.ini | Drops comment lines |
| Delete blank lines | sed '/^$/d' notes.txt | |
| Delete lines 2 to 5 | sed '2,5d' notes.txt | |
| Print only lines 10 to 20 | sed -n '10,20p' app.log | -n stops sed printing every line, so only p prints |
| Print the last line | sed -n '$p' app.log | |
| Strip trailing spaces | sed 's/[[:space:]]*$//' notes.txt | |
| Indent every line | sed 's/^/ /' notes.txt | |
| Add a line at the top | sed '1i # generated' config.ini | GNU syntax. macOS sed needs 1i\ and the text on the next line |
| Several edits in one pass | sed -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.
| Task | Command | Notes |
|---|---|---|
| Print a column | awk '{print $1}' access.log | Single quotes, so the shell leaves $1 alone |
| Several columns | awk '{print $1, $9}' access.log | The comma puts a space between them |
| Split on commas | awk -F, '{print $2}' people.csv | -F: for /etc/passwd. Quoted CSV fields with commas inside are not handled |
| Last column | awk '{print $NF}' access.log | |
| Rows where a column matches | awk '$9 >= 500' access.log | No action means print the line |
| Rows where a column equals a string | awk '$3 == "london" {print $1}' people.txt | |
| Skip the header row | awk -F, 'NR > 1 {print $1}' people.csv | |
| Lines 10 to 20 | awk 'NR >= 10 && NR <= 20' app.log | |
| Lines longer than 80 characters | awk 'length > 80' main.py | |
| Sum a column | awk '{sum += $10} END {print sum}' access.log | END runs once, after the last line |
| Average a column | awk '{sum += $2} END {print sum / NR}' scores.txt | |
| Count by a column | awk '{count[$1]++} END {for (k in count) print k, count[k]}' access.log | The output is in no particular order. Pipe it to sort |
| Remove duplicate lines, keeping order | awk '!seen[$0]++' names.txt | Unlike sort -u, the first of each line stays where it was |
| Change the output separator | awk -F: -v OFS=, '{print $1, $7}' /etc/passwd | root,/bin/bash |
| Format the output | awk '{printf "%-10s %5d\n", $1, $2}' scores.txt | printf needs its own \n |
| Decimals | awk 'BEGIN {printf "%.2f\n", 10 / 3}' | 3.33. BEGIN runs before any input, so no file is needed |
cut, sort, uniq, tr and xargs
| Task | Command | Notes |
|---|---|---|
| Cut out fields | cut -d, -f1,3 people.csv | -d sets the separator, one character only. -f2- means field 2 onwards |
| Cut out characters | cut -c1-10 app.log | |
| Sort lines | sort names.txt | The order depends on your locale. LC_ALL=C sort sorts by byte, capitals first |
| Sort numbers | sort -n sizes.txt | Without -n, 10 sorts before 9 |
| Largest first | sort -rn sizes.txt | |
| Sort sizes like 1K, 2M, 3G | du -sh * | sort -h | |
| Sort by a column | sort -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 numbers | sort -V versions.txt | v1.2, v1.9, v1.10, not v1.10 first |
| Sort and remove duplicates | sort -u names.txt | |
| Sort a file in place | sort -o names.txt names.txt | Not sort names.txt > names.txt, which empties the file first |
| Remove repeated lines | sort names.txt | uniq | uniq only removes duplicates that are next to each other, so sort first |
| Count each distinct line | sort names.txt | uniq -c | sort -rn | The most common first |
| Only the duplicated lines | sort names.txt | uniq -d | uniq -u for the lines that appear once |
| Lines in both files | comm -12 a.txt b.txt | Both files must be sorted. comm -23 gives lines only in the first |
| Upper case | tr 'a-z' 'A-Z' < notes.txt | tr only reads its input, so it takes < or a pipe, never a file name |
| Delete characters | tr -d '\r' < windows.txt > unix.txt | Removes Windows line endings |
| Squeeze repeated spaces | tr -s ' ' < table.txt | |
| One item per line | echo "$PATH" | tr ':' '\n' | |
| Join lines with commas | paste -sd, names.txt | |
| Line up columns | column -t -s, people.csv | In the bsdextrautils package on Debian and Ubuntu |
| Turn lines into arguments | cat urls.txt | xargs -n 1 curl -O | -n 1 runs curl once per line |
| Put each line in a set place | xargs -I{} cp {} {}.bak < files.txt | |
| Several at a time | xargs -P 4 -n 1 gzip < files.txt | Four gzips at once |
| Do nothing when there is no input | xargs -r kill | GNU 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.
| Task | Command | Notes |
|---|---|---|
| Every process | ps aux | ps -ef is the same list in another layout |
| Top memory users | ps aux --sort=-%mem | head | --sort=-%cpu for CPU. GNU procps only |
| Pick the columns | ps -eo pid,ppid,%cpu,%mem,cmd --sort=-%cpu | |
| As a tree | ps -ef --forest | pstree -p is more compact. It is in the psmisc package |
| Find a process ID by name | pgrep -a nginx | -a shows the command line. -f matches against the whole command line instead of the name |
| Live view | top | q quits, M sorts by memory, P by CPU. htop is friendlier, and a separate install |
| Ask a process to stop | kill 4127 | Sends TERM (15). A well-behaved program cleans up and exits |
| Force it to stop | kill -9 4127 | KILL (9) cannot be caught, so there is no clean-up. Try plain kill first |
| Ask it to reload | kill -HUP 4127 | Many daemons, nginx among them, reread their config on HUP |
| Stop by name | pkill -f 'python app.py' | Run pgrep -af with the same pattern first to see what it matches |
| Stop every process with a name | killall node | psmisc package. On some other Unix systems killall kills everything, so prefer pkill |
| List the signals | kill -l | kill -l 15 prints TERM |
| Stop the program in front | Ctrl+C | Sends INT (2). Exit status 130 |
| Pause it | Ctrl+Z | Then bg to carry on in the background, fg to bring it back, jobs to list them |
| Keep running after you log out | nohup ./long-task.sh > task.log 2>&1 & | For anything longer, tmux or screen lets you come back to it |
| Run at lower priority | nice -n 10 ./backup.sh | 19 is the lowest priority. renice -n 5 -p 4127 changes a running one |
| Give up after a time limit | timeout 30s ./check.sh | Exit status 124 if the time ran out |
| Time a command | time ./build.sh | |
| Rerun every 2 seconds | watch -n 2 'df -h' | Ctrl+C stops it |
| Load and uptime | uptime | The three load averages are for 1, 5 and 15 minutes. Compare them with nproc |
| Which process has a file open | lsof /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.
| Task | Command | Notes |
|---|---|---|
| Is it running | systemctl status nginx | Shows the state, the PID and the last few log lines |
| Start, stop, restart | sudo systemctl restart nginx | Or start, stop |
| Reload the config without a restart | sudo systemctl reload nginx | Only for services that support it. reload-or-restart falls back |
| Start at boot | sudo systemctl enable --now nginx | --now also starts it straight away. disable --now undoes both |
| Just the state, for a script | systemctl is-active nginx | Prints active or inactive, with a matching exit status |
| Everything that failed | systemctl --failed | |
| Every running service | systemctl list-units --type=service --state=running | |
| After editing a unit file | sudo systemctl daemon-reload | |
| A service's logs | journalctl -u nginx | -e jumps to the end |
| Follow its logs | journalctl -u nginx -f | |
| Logs since a time | journalctl -u nginx --since '1 hour ago' | |
| Errors since the last boot | journalctl -b -p err | |
| The last 50 lines | journalctl -n 50 |
Disk, memory and system
| Task | Command | Notes |
|---|---|---|
| Free space on every disk | df -h | df -h . for the disk the current folder is on |
| Free inodes | df -i | A disk can have space left and still be full, if it runs out of inodes. See the gotchas |
| File system types | df -hT | |
| Size of a folder | du -sh ~/Downloads | |
| Size of each folder in here | du -h -d 1 | sort -h | -d 1 works in GNU and macOS du. GNU also spells it --max-depth=1 |
| Biggest things in here | du -sh * | sort -rh | head | Skips hidden files and folders |
| Memory | free -h | Look at available, not free. Linux uses spare memory as a disk cache and hands it back when needed |
| Memory and CPU over time | vmstat 1 5 | Five samples, one second apart |
| Swap | swapon --show | Prints nothing when there is no swap |
| Disks and partitions | lsblk | lsblk -f adds file systems, labels and UUIDs |
| What is mounted where | findmnt | findmnt / for one mount point. mount with no arguments lists everything, less tidily |
| Mount a disk | sudo mount /dev/sdb1 /mnt/usb | The folder must exist. For a permanent mount, add it to /etc/fstab |
| Unmount it | sudo umount /mnt/usb | umount, not unmount |
| Which distribution | cat /etc/os-release | |
| Kernel version | uname -r | uname -a for everything, including the architecture |
| CPU count | nproc | lscpu 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.
| Task | Command | Notes |
|---|---|---|
| Log in | ssh ada@203.0.113.10 | exit or Ctrl+D logs out |
| On another port | ssh -p 2222 ada@203.0.113.10 | |
| With a particular key | ssh -i ~/.ssh/deploy_key ada@203.0.113.10 | |
| Run one command there | ssh web1 'df -h /' | Quote it, so the pipes and variables run there, not here |
| Run a local script there | ssh web1 'bash -s' < setup.sh | |
| Make a key pair | ssh-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 server | ssh-copy-id ada@203.0.113.10 | Asks for the password one last time. Add -p 2222 for another port |
| Key fingerprint | ssh-keygen -lf ~/.ssh/id_ed25519.pub | |
| Reach a port on the server as if it were local | ssh -N -L 8080:localhost:80 web1 | Then open localhost:8080. -N means no shell, just the tunnel |
| Through a jump host | ssh -J bastion web1 | The same as ProxyJump in the config file |
| Copy a file up | scp report.pdf web1:/tmp/ | |
| Copy a file down | scp web1:/var/log/nginx/error.log . | |
| Copy a folder | scp -r site web1:/tmp/ | |
| Sync a folder | rsync -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 exactly | rsync -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 progress | rsync -av --progress big.iso web1:/tmp/ | rsync also works between two local folders |
| Copy a folder through a pipe | tar -czf - project | ssh web1 'tar -xzf - -C /srv' | No temporary archive on either side |
curl, ports and network checks
| Task | Command | Notes |
|---|---|---|
| Fetch a URL | curl https://example.com | |
| Save it to a file | curl -o page.html https://example.com | -O saves it under the name in the URL |
| Follow redirects | curl -L https://example.com/old | Without -L, curl prints the redirect response and stops |
| Only the headers | curl -I https://example.com | |
| Fail on HTTP errors | curl -fsS https://example.com/health | Without -f, a 404 or 500 still exits 0. -s hides the progress bar, -S still shows errors |
| Just the status code | curl -s -o /dev/null -w '%{http_code}\n' https://example.com | |
| POST JSON | curl --json '{"name":"Ada"}' https://api.example.com/users | curl 7.82 and later. Sets the Content-Type and Accept headers for you |
| POST JSON on an older curl | curl -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 form | curl -d 'name=Ada' https://example.com/signup | |
| Another method | curl -X DELETE https://api.example.com/users/42 | |
| With a token | curl -H "Authorization: Bearer $TOKEN" https://api.example.com/me | |
| With a user name and password | curl -u ada https://example.com/private | Asks for the password, so it stays out of your shell history |
| See the whole conversation | curl -v https://example.com | Lines starting > are what curl sent, < what came back |
| Download, resuming if it stops | wget -c https://example.com/big.iso | curl -C - -O does the same |
| Which ports are listening | sudo ss -tulpn | t TCP, u UDP, l listening, p the process, n numbers not names. sudo shows other users' processes |
| What is using port 8080 | sudo lsof -i :8080 | lsof -ti :8080 prints only the PID |
| Is a port open | nc -zv db1 5432 | Connects and closes. Exit status 0 if it is open |
| My IP addresses | ip -br a | ip a for the full detail. ifconfig is older and often not installed |
| Routes and the default gateway | ip r | |
| Is a host reachable | ping -c 4 example.com | -c 4 stops after four. Without it, Ctrl+C stops it |
| Look up a domain | dig +short example.com | dig +short MX example.com for mail servers. In bind9-dnsutils on Debian and Ubuntu |
| Reverse lookup | dig -x 1.1.1.1 +short | |
| Ask a particular DNS server | dig @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.
| Task | Debian, Ubuntu (apt) | Fedora (dnf) and Arch (pacman) |
|---|---|---|
| Refresh the package list | sudo apt update | dnf check-upgrade, though dnf refreshes on its own. pacman: sudo pacman -Sy, but only ever as part of -Syu |
| Upgrade everything | sudo apt upgrade | sudo dnf upgrade. sudo pacman -Syu |
| Install | sudo apt install nginx | sudo dnf install nginx. sudo pacman -S nginx |
| Remove | sudo apt remove nginx | sudo dnf remove nginx. sudo pacman -R nginx |
| Remove with its config files | sudo apt purge nginx | dnf remove leaves edited config as .rpmsave. sudo pacman -Rns nginx also removes unneeded dependencies |
| Remove what nothing needs any more | sudo apt autoremove | sudo dnf autoremove. sudo pacman -Rns $(pacman -Qdtq) |
| Search | apt search ripgrep | dnf search ripgrep. pacman -Ss ripgrep |
| Details of a package | apt show nginx | dnf info nginx. pacman -Si nginx, or -Qi once installed |
| List what is installed | apt list --installed | dnf list --installed. pacman -Q |
| Which package a file came from | dpkg -S /usr/bin/curl | rpm -qf /usr/bin/curl. pacman -Qo /usr/bin/curl |
| Files a package installed | dpkg -L curl | rpm -ql curl. pacman -Ql curl |
| Install a downloaded package | sudo apt install ./app.deb | The ./ 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 install | apt policy nginx | dnf list nginx |
| Clear the download cache | sudo apt clean | sudo 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.
| Task | Command | Notes |
|---|---|---|
| Create a .tar.gz | tar -czf project.tar.gz project/ | |
| Extract one | tar -xf project.tar.gz | GNU tar spots the compression by itself, so x and f are enough |
| Extract into a folder | tar -xf project.tar.gz -C /opt | The folder has to exist already |
| List what is inside | tar -tf project.tar.gz | tar -tvf adds sizes, owners and dates |
| Extract one file | tar -xf project.tar.gz project/src/main.py | The path exactly as tar -tf lists it |
| Drop the top folder when extracting | tar -xf project.tar.gz --strip-components=1 | |
| Leave something out | tar --exclude=node_modules -czf project.tar.gz project/ | |
| Smaller, slower: .tar.xz | tar -cJf project.tar.xz project/ | |
| Fast and small: .tar.zst | tar --zstd -cf project.tar.zst project/ | Needs zstd installed. tar -caf project.tar.zst project/ picks the compression from the name |
| Compress one file | gzip app.log | Replaces it with app.log.gz. gzip -k keeps the original |
| Decompress it | gunzip app.log.gz | Or gzip -d. xz and unxz, zstd and unzstd work the same way |
| Read or search it without decompressing | zcat app.log.gz | zgrep error app.log.gz and zless work too |
| Create a .zip | zip -r site.zip site/ | -x 'site/node_modules/*' leaves a folder out |
| Extract a .zip | unzip site.zip -d site-copy | unzip -l site.zip lists it |
Reading ls -l and permission numbers
ls -l starts every line with ten characters. Take -rwxr-xr--:
| Characters | Who | Here | Means |
|---|---|---|---|
- | - | A file. d for a folder, l for a symbolic link | |
rwx | The owner | rwx | Read, write and run it |
r-x | The file's group | r-x | Read and run it, not change it |
r-- | Everyone else | r-- | 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.
| Number | Shown as | Use it for |
|---|---|---|
644 | -rw-r--r-- | Ordinary files |
600 | -rw------- | Private keys, passwords, .env files |
755 | -rwxr-xr-x | Scripts, programs and most folders |
700 | -rwx------ | Private folders, such as ~/.ssh |
775 | -rwxrwxr-x | Folders a team shares through a group |
777 | -rwxrwxrwx | Almost 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.10sort | 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.10Then 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 web1ssh 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 addresschmod 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.
| Task | Linux (GNU) | macOS (BSD) |
|---|---|---|
| Edit a file in place | sed -i 's/a/b/' f | sed -i '' 's/a/b/' f |
| Edit in place, portable | sed -i.bak 's/a/b/' f | sed -i.bak 's/a/b/' f |
| Perl-style regex | grep -P '\d+' | Not available. grep -E '[0-9]+' |
| Folder sizes, one level | du -h -d 1 | du -h -d 1 |
| File size in bytes | stat -c '%s' f | stat -f '%z' f |
| Yesterday's date | date -d yesterday +%F | date -v-1d +%F |
| Processes by memory | ps aux --sort=-%mem | ps aux -m |
| Memory | free -h | vm_stat, or Activity Monitor |
| Listening ports | ss -tulpn | lsof -iTCP -sTCP:LISTEN -n -P |
| IP addresses | ip -br a | ifconfig |
| SHA-256 checksum | sha256sum f | shasum -a 256 f |
| Services | systemctl | launchctl |
| Packages | apt, dnf, pacman | brew, 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 right | What actually happens | Do this instead |
|---|---|---|
find . -name *.txt | The shell expands *.txt first. With one match here, find looks only for that name; with two, paths must precede expression | find . -name '*.txt' |
sort names.txt > names.txt | The file is empty: > truncates it before sort reads it | sort -o names.txt names.txt |
uniq names.txt on unsorted input | Only removes duplicates that sit next to each other | sort names.txt | uniq, or sort -u |
sort on numbers | 10 sorts before 9 | sort -n, or sort -h for 1K, 2M |
The same sort on two machines | Different orders: LC_ALL=C puts capitals first, en_GB.UTF-8 mixes the cases | LC_ALL=C sort when the order has to match |
sudo usermod -G docker ada | Removes ada from every other group | sudo usermod -aG docker ada |
| Adding yourself to a group | id still does not show it | Log out and back in |
chmod 644 ~/.ssh/id_ed25519 | SSH refuses the key: UNPROTECTED PRIVATE KEY FILE | chmod 600 |
chmod -R 755 site/ | Every file becomes runnable | find site -type d -exec chmod 755 {} + and -type f with 644 |
pkill -f deploy | Also kills anything else whose command line mentions deploy, which can include the shell that ran it | pgrep -af deploy first, then a narrower pattern |
kill -9 first | The program gets no chance to remove lock files or finish writing | Plain kill, and -9 only if it ignores that |
No space left on device, but df -h shows space | The disk has run out of inodes, usually from millions of tiny files | df -i, then find and delete the small files |
Deleted a huge log, df shows no change | A running program still has it open, so the space is not freed | sudo lsof +L1, then restart that program |
rsync -a site web1:/var/www/site/ | Creates /var/www/site/site | rsync -a site/ web1:/var/www/site/ |
tar -xf backup.tar.gz -C restore | Cannot open: No such file or directory when restore is missing | mkdir -p restore first |
curl -s "$url" in a script | A 404 or 500 still exits 0, so the script carries on | curl -fsS |
apt in a script or Dockerfile | Warns that its output is not stable | apt-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.
