../

Linux commands

Command-first reference for the Linux shell (bash and zsh), for someone at a terminal who needs the exact command and flags: files, text, pipes, permissions, processes, networking, archives and packages, starting with a lookup table. Flags are the GNU versions on Ubuntu, Debian and Fedora; macOS ships BSD tools, and differences are noted where they bite. Administration in depth is in Linux sysadmin, SSH in SSH, keys & certificates.

Quick lookup

I want to…Command
see where I ampwd
go home / back to the previous directorycd / cd -
list everything with sizesls -lah
list newest files lastls -ltr
make nested directoriesmkdir -p a/b/c
copy a directory, keeping permissionscp -a src/ dst/
rename or movemv old new
delete a directory treerm -r dir (look first: ls dir)
make a shortcut (symlink)ln -s /path/to/target name
read a long fileless file
watch a log growtail -F app.log
count lineswc -l file
find files by namefind . -name '*.conf' or fd conf
find text inside filesgrep -rn 'text' . or rg text
replace text in a filesed -i 's/old/new/g' file
know which program runstype -a python3
save output and errors to a filecmd > out.log 2>&1
make a script executablechmod +x script.sh
change a file's ownersudo chown -R user:group dir
run the last command as rootsudo !!
see running processesps aux or htop
stop a processkill PID, pkill -f pattern
find what uses port 3000sudo ss -ltnp 'sport = :3000'
check free disk spacedf -h
see what is big heredu -sh * | sort -h
check memoryfree -h
see the OS and kernel versioncat /etc/os-release, uname -r
see my IP addressesip -br a
test if a host is up / a port is openping -c 3 host / nc -zv host 443
look up DNSdig +short example.com
download a filecurl -fLO URL
call a JSON APIcurl -fsS URL | jq .
copy files to a serverrsync -avP dir/ host:/path/
pack / unpack a tarballtar czf out.tgz dir/ / tar xf file.tgz
install a packagesudo apt install x / sudo dnf install x
restart a service and read its logssudo systemctl restart x, journalctl -u x -f
keep a job running after I log outtmux, or nohup cmd &
rerun a command every 2 secondswatch -n 2 cmd
get help for a commandman cmd, cmd --help, tldr cmd

Shell basics

The shell reads what you type and runs it: bash on most Linux servers, zsh on macOS and many desktops. The terminal is the window the shell runs in.

alice@web1:~/src$ ls -la --color=auto /etc
PartMeaning
alice@web1user and host name
~/srccurrent directory; ~ is your home (/home/alice)
$ / #ordinary user / root; slow down at a # prompt
lsthe command: a program on PATH, a shell builtin, an alias or a function
-lashort options, combined (-l -a)
--color=autolong option; values follow = or a space
/etcargument: what the command acts on

-- ends the options, so rm -- -weird-name treats -weird-name as a file. Every command returns an exit status: 0 is success, anything else is failure; echo $? prints the last one. a && b runs b only if a succeeded, a || b only if it failed, a; b runs both.

Getting help

CommandShows
man lsthe manual page; /word search, n next, q quit
man 5 crontaba specific section (5: file formats)
man -k compresssearch page names and descriptions (same as apropos)
ls --helpbuilt-in usage summary (most GNU tools)
help cdbash builtins such as cd, export, jobs (zsh: man zshbuiltins)
tldr tarshort community examples (install a tldr client)
type lswhether a name is an alias, builtin, function or file
whatis lsone-line description
SectionHolds
1user commands (ls, grep)
2, 3system calls, C library functions
5file formats (passwd, crontab, fstab)
7overviews (signal, regex, hier)
8admin commands (ip, mount, useradd)

Tab completion

Press Tab to complete a command, path or option. Press it twice to list candidates (bash) or open a menu you can cycle through (zsh). Completion of subcommands and flags (git che, systemctl res then Tab) comes from the bash-completion package on bash; zsh needs autoload -Uz compinit && compinit in ~/.zshrc, which frameworks such as Oh My Zsh add for you.

Keyboard shortcuts

These are the default emacs-style bindings in bash and zsh.

KeysDoes
Ctrl-A / Ctrl-Ejump to start / end of the line
Alt-B / Alt-Fback / forward one word (macOS Terminal: enable "Use Option as Meta key")
Ctrl-Ucut from the cursor to the start of the line (zsh: the whole line)
Ctrl-Kcut from the cursor to the end of the line
Ctrl-Wcut the previous word
Ctrl-Ypaste what you cut
Ctrl-_undo
Ctrl-Lclear the screen (like clear)
Ctrl-Cinterrupt the running command (SIGINT), or abandon the line
Ctrl-Dend of input; on an empty line, exit the shell
Ctrl-Zsuspend the foreground job; fg resumes it (jobs)
Ctrl-Rsearch history backward: type, Ctrl-R again for older, Enter runs, Ctrl-G cancels
Alt-.insert the last argument of the previous command
Ctrl-X Ctrl-Eedit the line in $EDITOR (bash)
Ctrl-S / Ctrl-Qfreeze / unfreeze output; if the terminal seems stuck, press Ctrl-Q
reset(a command) repair a garbled terminal

History

FormMeans
historynumbered list; history | grep ssh to search
!!the whole previous command: sudo !!
!$last argument of the previous command: mkdir -p a/b then cd !$
!*all arguments of the previous command
!42command number 42 from history
!sshthe most recent command starting with ssh
!ssh:pprint it without running
^old^newrerun the previous command with old replaced by new

bash keeps history in ~/.bash_history, written when the shell exits. With HISTCONTROL=ignoreboth (Ubuntu's default .bashrc), a command that starts with a space is not saved, which is handy for one-off secrets; zsh needs setopt HIST_IGNORE_SPACE.

pwd                 # print working directory
cd /var/log         # absolute path: starts with /
cd src              # relative to where you are
cd ..               # up one level
cd                  # home (same as cd ~)
cd -                # back to the previous directory
pushd /etc; popd    # visit, then return

Path shortcuts: ~ home, . this directory, .. the parent, / the root of the filesystem.

CommandShows
lsnames
ls -llong: mode, links, owner, group, size, modified time
ls -ainclude hidden dotfiles (names that start with .)
ls -Alike -a without . and ..
ls -lahlong, all, human-readable sizes (4.0K, 1.2G)
ls -lt / ls -ltrnewest first / newest last
ls -lSbiggest first
ls -d */directories only
ls -Rrecursive
ls -1one name per line
ls -iinode numbers
tree -L 2tree two levels deep (tree package); -a dotfiles, -d dirs only, -I 'node_modules' skip

Create, copy, move, delete

CommandDoes
mkdir logsmake a directory
mkdir -p a/b/cmake missing parents too; no error if it exists
touch notes.mdcreate an empty file, or update an existing file's modified time
cp a.txt b.txtcopy a file; overwrites b.txt without asking
cp -i a b / cp -n a bask before overwriting / never overwrite
cp -r src dstcopy a directory recursively
cp -a src dstarchive copy: recursive, keeps modes, owners, timestamps and symlinks
cp -vprint each file copied
mv old newrename
mv a b dir/move several files into a directory; -i ask, -n never overwrite
ln -s /opt/app/current appsymlink called app pointing at the target (target first, like cp)
ln -sfn releases/v2 currentrepoint an existing symlink to a directory
readlink -f appresolve a path to its real location
rm filedelete a file (no trash, no undo)
rm -r dirdelete a directory tree; -i asks for each file, -I asks once
rm -rf dirno questions, no errors for missing files
rmdir dirdelete only if empty
stat filesize, mode, owner, inode, access / modify / change times
stat -c '%a %U %s %n' filechosen fields (GNU; macOS uses stat -f)
file mysterywhat a file contains: ELF 64-bit, gzip compressed, ASCII text

If dst is an existing directory, cp -r src dst and mv src dst put src inside it (dst/src).

Viewing files

CommandUse
cat fileprint a whole (short) file
cat a b > cconcatenate files
cat -n filewith line numbers
less filescroll through a file (keys below)
head file / head -n 50 filefirst 10 / 50 lines
head -c 200 filefirst 200 bytes
tail -n 50 filelast 50 lines
tail -n +2 fileeverything from line 2 on (skip a header)
tail -f app.logkeep printing new lines as they arrive; Ctrl-C stops
tail -F app.logfollow by name, so it survives log rotation
wc -l filecount lines; -w words, -c bytes, -m characters
wc -l *.tsper file, plus a total
tac filelines in reverse order
xxd file | headbytes in hex (hexdump -C is similar)
bat filecat with syntax highlighting (Debian/Ubuntu name: batcat)
Key in lessDoes
Space / bpage down / up
j / k, arrowsline down / up
g / Gtop / bottom
/text / ?textsearch forward / backward; n next, N previous
&textshow only matching lines; & then Enter clears
Ffollow like tail -f; Ctrl-C stops following
-Stoggle wrapping of long lines (typed inside less)
vopen the file in $EDITOR
h / qhelp / quit

less -R keeps colors, less -N numbers lines, less +F app.log starts in follow mode. man pages open in less, so the same keys work there.

Finding files

find walks a directory tree: find WHERE TESTS ACTION.

CommandFinds
find . -name '*.log'by name, case-sensitive; quote the pattern
find . -iname 'readme*'case-insensitive
find . -type f / -type d / -type lfiles / directories / symlinks
find . -maxdepth 1 -type donly the top level
find . -mtime -1modified in the last 24 hours
find . -mtime +30modified more than 30 days ago (counted in whole days)
find . -mmin -15modified in the last 15 minutes
find . -newer ref.txtmodified after ref.txt
find . -size +100Mlarger than 100 MiB (k, M, G units)
find . -emptyempty files and directories
find . -user aliceowned by a user
find . -perm -o+w -type fworld-writable files
find . -name '*.js' -not -path '*/node_modules/*'exclude a path (! works like -not)
find . -path ./node_modules -prune -o -name '*.js' -printskip a directory without descending into it (faster)
find . -name '*.tmp' -deletedelete matches
find . -name '*.sh' -exec chmod +x {} +run a command on the matches, in batches
find . -name '*.sh' -exec chmod +x {} \;once per file (slower)
find . -type f -print0 | xargs -0 cmdpass names safely, even with spaces or newlines
find / -xdev -name corestay on one filesystem
find / -name x 2>/dev/nullhide "Permission denied" noise
CommandUse
locate nginx.confinstant search of a name database (plocate package); refresh with sudo updatedb
fd confa friendlier find: regex by default, skips hidden and .gitignored files; -e md, -t d, -H, -I, -x cmd (Debian/Ubuntu: package fd-find, command fdfind)
which python3first match on PATH
type -a python3every match, plus aliases, functions and builtins
command -v jqportable check for scripts: prints the path, exit status 1 if missing
whereis lsbinary, source and man page locations
# stop a script early if a tool is missing
command -v jq >/dev/null || { echo "need jq" >&2; exit 1; }

Searching text

grep prints the lines that match a regular expression (regex), a pattern language.

CommandDoes
grep 'error' app.loglines containing error
grep -iignore case
grep -nshow line numbers
grep -r 'x' dir/search a directory recursively (-R also follows symlinks)
grep -wwhole words only
grep -vinvert: lines that do not match
grep -ccount matching lines
grep -l / grep -Lnames of files that match / don't
grep -oprint only the matched text
grep -E 'a|b'extended regex: +, ?, |, () without backslashes
grep -F 'a.b*'fixed string, no regex
grep -P '\d+'Perl regex with \d, lookarounds (GNU grep)
grep -A 3 / -B 3 / -C 33 lines after / before / around each match
grep -h / -Hhide / show file names
grep -qquiet, exit status only: if grep -q x f; then …
--include='*.py'with -r: only these file names
--exclude-dir=node_moduleswith -r: skip directories

Exit status: 0 found, 1 not found, 2 error.

grep -rniE 'timeout|refused' /var/log/nginx/
grep -rl 'TODO' src/ --include='*.ts'
grep -v '^#' app.conf | grep -v '^$'  # no comments/blanks
grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' access.log \
  | sort | uniq -c | sort -rn | head    # top IPs
ps aux | grep '[n]ginx'   # [n] keeps grep out of the result
RegexMatches
.any one character
^ / $start / end of the line
[abc], [a-z], [^0-9]one of / a range / not these
*zero or more of the previous item
+, ?one or more / zero or one (with -E)
{3}, {2,5}exactly 3 / 2 to 5 repeats (with -E)
a|beither (with -E)
(ab)+group (with -E)
\b, \w, \sword boundary, word character, whitespace (GNU)
\.a literal dot

ripgrep

ripgrep (rg) searches recursively by default, skips .gitignored, hidden and binary files, and is much faster on big trees.

CommandDoes
rg 'pattern'search from here down
rg -i / rg -Signore case / smart case (case-sensitive only if the pattern has capitals)
rg -w, -F, -v, -c, -l, -o, -n, -C 3same meaning as in grep
rg -t py 'import'only one file type; -T js excludes; rg --type-list shows them
rg -g '*.tsx' -g '!*.test.tsx'include / exclude by glob
rg --hidden / rg -uualso hidden files / also ignored and hidden files
rg --files | rg configlist the files rg would search, then filter names
rg 'v(\d+)' -r 'version $1'print matches with a replacement (files are not changed)
rg -U 'start[\s\S]*?end'match across lines
rg -l -0 xNUL-separated file names for xargs -0

Text processing

Small tools that each read lines on stdin and write lines on stdout, chained with |.

cut, sort, uniq, tr

CommandDoes
cut -d: -f1 /etc/passwdfield 1, with : as the separator
cut -d, -f2,4 / cut -d, -f3-fields 2 and 4 / field 3 to the end
cut -c1-80characters 1 to 80
sort filealphabetical
sort -n / -h / -Vnumeric / human sizes (2K, 1G) / versions (1.10 after 1.9)
sort -rreverse
sort -t, -k2,2nby field 2 of comma-separated lines, numerically
sort -usorted and deduplicated
sort -o file filesort a file in place (sort file > file empties it)
LC_ALL=C sortplain byte order, faster and locale-independent
uniqcollapse adjacent duplicates, so sort first
uniq -cprefix each line with its count
uniq -d / uniq -uonly repeated / only unique lines
tr a-z A-Zuppercase; tr reads stdin only: tr a-z A-Z < file
tr -d '\r'strip Windows carriage returns
tr -s ' 'squeeze runs of spaces into one
tr ':' '\n'one item per line: echo "$PATH" | tr ':' '\n'
shuf -n 33 random lines
# frequency count: the most common lines first
sort access.log | uniq -c | sort -rn | head
sort ips.txt | uniq -c | sort -rn | head -n 2 ips.txt file sort waits for EOF uniq -c streams sort -rn waits for EOF head -n 2 stops after 2 reads pipe pipe pipe 10.0.0.7 10.0.0.3 10.0.0.7 10.0.0.9 10.0.0.7 10.0.0.3 10.0.0.3 10.0.0.3 10.0.0.7 10.0.0.7 10.0.0.7 10.0.0.9 2 10.0.0.3 3 10.0.0.7 1 10.0.0.9 3 10.0.0.7 2 10.0.0.3 1 10.0.0.9 3 10.0.0.7 2 10.0.0.3 on screen below each box: the lines it writes (for ips.txt, its contents)

The same pipeline on a six-line file. All four programs run at once, joined by pipes; sort can't write a line until it has read all of its input, while uniq and head pass lines on as they arrive.

sed

sed (stream editor) applies edits to each line and prints the result.

CommandDoes
sed 's/old/new/' freplace the first match on each line
sed 's/old/new/g' freplace every match
sed 's/old/new/gI' fcase-insensitive (GNU)
sed -i 's/old/new/g' fedit the file in place (GNU)
sed -i.bak 's/old/new/g' fin place, keeping f.bak (GNU and macOS)
sed -i '' 's/old/new/g' fin place on macOS and BSD
sed 's#/usr/local#/opt#g' fany delimiter; handy for paths
sed -E 's/([0-9]+)px/\1rem/g' fextended regex; \1 is the first group
sed 's/.*/"&"/' f& is the whole match
sed -n '10,20p' fprint only lines 10 to 20
sed -n '/BEGIN/,/END/p' fprint the block between two patterns
sed '/^#/d' f / sed '/^$/d' fdelete comment / blank lines
sed '1d' f / sed '$d' fdelete the first / last line
sed '3i text' f / sed '/re/a text' finsert before line 3 / append after matching lines (GNU)
sed 's/^[ \t]*//' ftrim leading whitespace (GNU)

awk

awk splits each line into fields: $1, $2 and so on, $NF is the last, $0 the whole line, and NR the line number. A program is pattern { action } pairs; END { … } runs after the last line.

CommandDoes
awk '{print $1}' ffirst whitespace-separated field
awk '{print $1, $NF}' ffirst and last field
awk -F: '{print $1, $7}' /etc/passwdcustom separator
awk 'NR > 1' fskip the header line
awk 'NR == 5' fline 5 only
awk '$3 > 100' frows where field 3 is over 100
awk '/error/ {print $2}' ffield 2 of matching lines
awk '{s += $2} END {print s}' fsum a column
awk '{s += $2} END {print s / NR}' faverage
awk '!seen[$0]++' fdrop duplicate lines, keep order, no sort needed
awk 'length > 120' flines longer than 120 characters
awk '{n[$1]++} END {for (k in n) print n[k], k}' fcount by key
awk '{printf "%-20s %s\n", $1, $2}' faligned columns
awk -F, -v OFS='\t' '{$1 = $1; print}' fsimple CSV to TSV (no quoted commas)

xargs

xargs turns lines of input into arguments for another command, for tools that don't read stdin (rm, mv, chmod).

CommandDoes
… | xargs rmrun rm with every input word as an argument
… | xargs -n 1 cmdone argument per run
… | xargs -P 4 -n 1 cmdfour runs in parallel
… | xargs -I {} cp {} /backup/put each input line where {} is
find … -print0 | xargs -0 cmdNUL-separated input: safe with spaces, quotes and newlines
rg -l -0 x | xargs -0 cmdsame with ripgrep (grep -lZ for grep)
xargs -rdon't run at all when the input is empty (GNU)
xargs -t / xargs -pprint each command / ask before each

Without -0, xargs splits on spaces and treats quotes specially, so my file.txt becomes two arguments.

tee, paste, column, diff

CommandDoes
cmd | tee out.logshow output and save it
cmd | tee -a out.logappend instead
echo x | sudo tee /etc/f >/dev/nullwrite a root-owned file; sudo echo x > /etc/f fails because the redirect runs as you
paste a.txt b.txtjoin files side by side, tab-separated
paste -sd, filejoin all lines with commas
column -talign whitespace-separated columns: mount | column -t
column -t -s,align a CSV
diff a bshow differences
diff -u a bunified format, as used by patches and git
diff -r dir1 dir2 / diff -rq dir1 dir2compare trees / only name the files that differ
diff <(sort a) <(sort b)compare the output of two commands
comm -12 <(sort a) <(sort b)lines in both files (-23: only in a)
cmp a bare two binary files identical?
sha256sum fchecksum; sha256sum -c SHA256SUMS verifies a list

jq

jq filters and reshapes JSON; install it with your package manager.

CommandGives
jq . f.jsonpretty-printed JSON
jq -r '.name'one field, raw (no quotes)
jq '.items[]'each element of an array
jq '.items[0].id'one element's field
jq '.items | length'array length
jq '.items[] | select(.status == "ok")'filter elements
jq '.items | map(.id)'an array of one field
jq '.items[] | {id, name}'keep only some fields
jq -r '.items[] | "\(.id)\t\(.name)"'format lines with string interpolation
jq -r '.items[] | [.id, .name] | @csv'CSV rows (@tsv for TSV)
jq '.a.b // "none"'default when missing or null
jq 'keys'object keys
jq -c '.[]'one compact value per line
jq -s '.' lines.jsonslurp JSON lines into one array
jq --arg u alice '.[] | select(.user == $u)'pass a shell value safely
jq -e '.ok' >/dev/nullexit status 1 when the result is false or null

Pipes and redirection

Every process has three streams: stdin (0, input), stdout (1, output) and stderr (2, errors). By default all three are your terminal.

SyntaxDoes
a | bstdout of a becomes stdin of b
> fstdout to a file, overwriting it
>> fappend
2> err.logstderr to a file
> out.log 2>&1stdout and stderr into one file (order matters)
2>&1 | lessboth streams into a pipe
&> f / &>> fboth streams, bash and zsh shorthand
a |& bpipe both streams (bash, zsh)
< ffile as stdin
2>/dev/nulldiscard errors; >/dev/null 2>&1 discards everything
<<'EOF'here-doc: the following lines up to EOF as stdin, literally
<< EOFhere-doc with $VAR and $(cmd) expanded
<<-EOFhere-doc that strips leading tabs
<<< "text"here-string: one string as stdin
<(cmd)process substitution: cmd's output as a file name
>(cmd)a file name that feeds cmd
: > ftruncate a file to zero bytes
set -o noclobbermake > refuse to overwrite (>| forces)
SyntaxDoes
a; brun both, in order
a && b / a || brun b only if a succeeded / failed
( cd dir && make )subshell: the cd doesn't affect your shell
$(cmd)command substitution: the output as text
echo $?exit status of the last command
set -o pipefaila pipeline fails if any part fails (use in scripts)

Redirections apply left to right: 2>&1 points stderr at wherever stdout points at that moment.

make make > build.log 2>&1 make > build.log 2>&1 make 2>&1 > build.log make 2>&1 > build.log terminal keyboard make 0 stdin stdout 1 stderr 2 build.log terminal screen fd 0, 1 and 2 all start as the terminal 1st, > build.log: fd 1 now writes to the file 2nd, 2>&1: fd 2 copies fd 1, so both reach the file 1st, 2>&1: fd 2 copies fd 1, still the terminal 2nd, > build.log: only fd 1 moves; errors stay on screen

1/5

Every process starts with three open file descriptors: 0 (stdin), 1 (stdout) and 2 (stderr). In a terminal all three are the terminal.

make > build.log 2>&1      # both streams into the file
make 2>&1 > build.log      # errors still on screen
# here-doc into a file
cat > app.ini <<'EOF'
[server]
port = 8080
EOF
diff <(ls dir1) <(ls dir2)   # compare two listings
echo 'vm.swappiness=10' \
  | sudo tee /etc/sysctl.d/99-swap.conf

Globbing and quoting

The shell expands globs (wildcards) into matching file names before the command runs; the command never sees the *.

PatternMatches
*any characters, but not a leading .
?exactly one character
[abc], [a-z], [!0-9]one of / a range / not these
**/*.tsany depth (zsh; bash after shopt -s globstar)
.*dotfiles (bash shopt -s dotglob makes * include them)
{a,b,c}brace expansion: makes the words whether or not files exist
{1..10}, {01..10}, {a..e}sequences, optionally zero-padded
~, ~bobyour home, Bob's home
ls *.log                 # every .log file here
cp config.yml{,.bak}     # cp config.yml config.yml.bak
mkdir -p app/{src,test,docs}
mv report.{txt,md}       # change the extension
echo file{1..3}.txt      # file1.txt file2.txt file3.txt

When nothing matches, bash passes the pattern through unchanged and zsh stops with no matches found. Quote patterns meant for the command, not the shell: find . -name '*.log', curl 'https://x.dev/?q=1'.

QuotingBehavior
'single'fully literal: no variables, globs or escapes
"double"expands $VAR, $(cmd) and backslash escapes; no globbing, no word splitting
\escapes one character: \ for a space, \$ for a dollar sign
$'a\tb'ANSI-C quoting: \t, \n, \x41 (bash, zsh)
unquotedexpanded, then split on whitespace, then globbed
bash, with f='my notes' in a directory holding 1.log and 2.log you type 1 braces 2 ~ $f $(…) 3 split 4 globs 5 unquote echo x{1,2} ~ $f "$f" *.log '*.log' echo x1 x2 ~ $f "$f" *.log '*.log' echo x1 x2 /home/alice my notes "my notes" *.log '*.log' echo x1 x2 /home/alice my notes "my notes" *.log '*.log' echo x1 x2 /home/alice my notes "my notes" 1.log 2.log '*.log' echo x1 x2 /home/alice my notes my notes 1.log 2.log *.log unquoted $f: split in two quoted: kept whole glob matched files echo receives the last row: 10 arguments, with no $, ~ or * left zsh leaves an unquoted $f whole unless you setopt sh_word_split
Expansion order in bash: braces, then ~ and $ in one pass, then word splitting, globbing and quote removal. Only quotes stop splitting and globbing.

Quote every variable: rm "$f", not rm $f, or a name with a space becomes two arguments. Pass a script's arguments on with "$@". In interactive bash, ! inside double quotes triggers history expansion (echo "hi!there" fails); use single quotes.

f='my report.txt'
rm $f            # tries to delete "my" and "report.txt"
rm "$f"          # deletes "my report.txt"
echo '$HOME'     # prints $HOME
echo "$HOME"     # prints /home/alice
echo "today is $(date +%F)"

Variables and environment

SyntaxDoes
name=valueset a shell variable; no spaces around =
"$name", "${name}_old"use it; braces when text follows
export name=valuealso pass it to programs you start (an environment variable)
NAME=value cmdset it for one command only
unset nameremove it
env / printenvlist environment variables
printenv PATHone variable
"${name:-default}"the value, or default if unset or empty
"${name:?message}"abort with message if unset or empty
"${#name}"length
"${file%.txt}"strip a suffix: a.txt → a
"${path##*/}"strip everything up to the last / (like basename)
"${s/old/new}"replace the first match (bash, zsh)
$((3 * 4))arithmetic
VariableHolds
$HOME, $USER, $PWDhome, user name, current directory
$PATHdirectories searched for commands, :-separated, left to right
$SHELLyour login shell
$EDITOReditor used by git, crontab -e, sudoedit
$LANG, $LC_ALLlocale, e.g. C.UTF-8, en_US.UTF-8
$?exit status of the last command
$$, $!PID of this shell / of the last background job
$1, "$@", $#a script's first argument / all of them / how many
echo "$PATH" | tr ':' '\n'          # one dir per line
export PATH="$HOME/.local/bin:$PATH"  # prepend a dir
hash -r      # bash: forget cached command locations
ShellReads at startupPut there
bash, login shell (SSH, console)/etc/profile, then the first of ~/.bash_profile, ~/.bash_login, ~/.profileenvironment variables; Ubuntu's ~/.profile and Fedora's ~/.bash_profile also load ~/.bashrc
bash, other interactive shells (a new terminal tab on Linux)~/.bashrcaliases, functions, prompt, PATH
zsh, every shell~/.zshenvrarely needed
zsh, login shell (every macOS Terminal window)~/.zprofilePATH setup such as Homebrew's brew shellenv
zsh, interactive~/.zshrcaliases, prompt, completion, PATH
~/.bashrc or ~/.zshrc
export EDITOR=vim
export PATH="$HOME/.local/bin:$PATH"
alias ll='ls -lah'
alias gs='git status'
mkcd() { mkdir -p "$1" && cd "$1"; }

Reload with source ~/.bashrc (or . ~/.zshrc), or open a new terminal. macOS has used zsh as the default shell since 10.15 Catalina. Change your login shell with chsh -s "$(command -v zsh)".

Permissions and ownership

Every file has an owner, a group and three sets of permissions.

-rwxr-x---  1 alice devs 4096 Nov 11 09:30 deploy.sh
│└┬┘└┬┘└┬┘    └─┬─┘ └┬─┘
│ │  │  │       │    └ group owner
│ │  │  │       └ user owner
│ │  │  └ others: everyone else
│ │  └ group: members of devs
│ └ user: alice
└ type: - file, d directory, l symlink
OctalBitsMeans
0---nothing
1--xexecute
2-w-write
3-wxwrite, execute
4r--read
5r-xread, execute
6rw-read, write
7rwxread, write, execute

Three digits give user, group, others: 750 is rwxr-x---. On a directory, r lists names, w creates and deletes entries, and x lets you enter it.

ModeSymbolicTypical use
644rw-r--r--ordinary files
755rwxr-xr-xdirectories, scripts, programs
600rw-------secrets, SSH private keys
700rwx------private directories such as ~/.ssh
664 / 775rw-rw-r-- / rwxrwxr-xfiles / directories shared with a group
CommandDoes
chmod +x run.shadd execute for everyone
chmod u+x run.shfor the owner (u) only; g group, o others, a all
chmod go-w fileremove write from group and others
chmod u=rw,go=r fileset exactly (same as 644)
chmod 600 key.pemoctal form
chmod -R u=rwX,go=rX dir/recursive; capital X adds execute to directories only (and files already executable)
sudo chown alice filechange the owner
sudo chown alice:devs fileowner and group
sudo chown :devs filegroup only (same as chgrp devs file)
sudo chown -R www-data:www-data /srv/siterecursive
umaskmask removed from new files' modes: 0022 gives files 644, dirs 755
umask 077new files private (600, dirs 700) in this shell
CommandDoes
sudo cmdrun one command as root; asks for your password
sudo !!rerun the last command with sudo
sudo -u postgres psqlrun as another user
sudo -iroot login shell; exit to leave
sudo -llist what you may run
sudo -kforget the cached password
sudoedit /etc/hostsedit a root-owned file in your own editor
su - alicebecome alice (needs her password)

Setuid, setgid and sticky bits, ACLs and sudo rules are in Linux sysadmin.

Processes and jobs

A process is a running program, numbered by a PID. A job is a process started from your current shell.

CommandShows / does
ps auxevery process: user, PID, %CPU, %MEM, command
ps aux | grep '[n]ginx'find one (the brackets keep grep itself out)
ps -ef --forestparent/child tree
ps aux --sort=-%mem | headbiggest memory users
ps -o pid,ppid,etime,cmd -p 1234chosen columns for one PID
pgrep -a nodePIDs and command lines by name
pgrep -f 'server.js'match anywhere in the full command line
pgrep -u aliceby user
toplive view: P sort by CPU, M memory, 1 per core, k kill, q quit
htop, btopfriendlier live views (install them); in htop F3 search, F5 tree, F9 kill
watch -n 2 'ps aux --sort=-%cpu | head'rerun a command every 2 seconds
time cmdhow long a command takes
timeout 30s cmdstop it after 30 seconds

ps STAT column: R running, S sleeping, D waiting on I/O (unkillable until it returns), T stopped, Z zombie (exited, not yet collected by its parent).

CommandSignalEffect
kill 1234TERM (15)ask the process to exit; the default
kill -INT 1234INT (2)same as Ctrl-C
kill -HUP 1234HUP (1)many daemons reload their config
kill -9 1234KILL (9)force; no cleanup, only after TERM failed
kill -STOP 1234 / -CONTSTOP / CONTpause / resume
pkill -f 'bun run'TERMby command-line match
pkill -u bobTERMevery process of a user
killall nginxTERMby exact name (psmisc package)
kill -0 1234noneexit status says whether it exists and you may signal it
kill -llist signal names
S sleeping waits for input signal handler R running or ready to run D disk wait unkillable for now T stopped paused waits woken disk I/O I/O done Ctrl-Z, STOP CONT: fg, bg kill PID TERM by default TERM 15, INT 2 it may clean up, exit, or ignore it Z zombie exited exit KILL 9 no handler gone parent calls wait()
TERM asks the program, which can clean up first; KILL ends it in the kernel. A zombie has already exited and only waits for its parent.

The full signal table is in Linux sysadmin.

CommandDoes
cmd &start in the background; prints [1] 12345 (job number, PID)
Ctrl-Zsuspend the foreground job
bg / bg %1continue it in the background
fg / fg %1bring it to the foreground
jobs -llist jobs with PIDs
kill %1signal a job by number
waitwait for all background jobs to finish
disown %1detach a job so closing the terminal doesn't kill it
nohup cmd > out.log 2>&1 &immune to hang-ups from the start, output to a file
setsid cmdrun in a new session, detached from the terminal

Closing a terminal or an SSH session sends SIGHUP to its jobs, which ends most programs. For long work over SSH, use tmux (or screen): the session keeps running after you disconnect.

tmuxDoes
tmux new -s workstart a session named work
Ctrl-B ddetach; it keeps running
tmux lslist sessions
tmux attach -t workreattach
Ctrl-B c / n / pnew window / next / previous
Ctrl-B % / Ctrl-B "split into left and right / top and bottom panes

Anything that must survive reboots belongs in systemd (Services and logs).

System info

CommandShows
uname -akernel name, release, architecture
uname -r / uname -mkernel release / CPU architecture (x86_64, aarch64)
cat /etc/os-releasedistro name and version
hostnamectlhostname, OS, kernel, virtualization
uptimetime since boot and load averages (1, 5, 15 minutes)
nproc / lscpuCPU count / model, cores, threads, caches
free -hmemory; read the available column, not free
df -hused and free space per mounted filesystem
df -hT / df -iwith filesystem types / inode usage
du -sh dirtotal size of a directory
du -sh * | sort -hsize of each entry here, biggest last
sudo du -xh -d1 / | sort -htop-level breakdown of one filesystem
ncduinteractive disk-usage browser (install it)
lsblk / lsblk -fdisks and partitions / with filesystems and mount points
lsof -i :3000what has port 3000 open
lsof -p 1234files and sockets a process holds
lsof /var/log/app.logwho holds a file open
sudo lsof +L1deleted files still held open (their space is not freed yet)
lspci, lsusbPCI and USB devices
sudo dmesg -T | tailrecent kernel messages with timestamps
date, timedatectltime, time zone, NTP sync
who, w, lastwho is logged in, what they run, recent logins

The first minute on a slow machine (vmstat, iostat, sar) is in Linux sysadmin.

Networking

CommandShows / does
ip a / ip -br ainterfaces and their addresses (brief: one line each)
ip rroutes; default via … is the gateway
ip route get 1.1.1.1which interface and gateway reach an address
ip link / ip -s linkinterfaces and state / with traffic counters
ip neighneighbor (ARP) cache
hostname -Ithis machine's addresses
ss -tulpnlistening sockets: TCP, UDP, listening, process, numeric
ss -tnpestablished TCP connections and their processes
ss -ssocket summary
ping -c 4 example.comreachability and round-trip time (Linux pings forever without -c)
traceroute example.comeach hop on the path; tracepath needs no root
mtr -rw example.comtraceroute plus loss per hop, as a report
dig example.comDNS answer with details
dig +short example.comjust the answer
dig example.com MXother record types: AAAA, TXT, NS, CNAME
dig @1.1.1.1 example.comask a specific resolver
dig -x 203.0.113.7reverse lookup
nslookup example.comsimpler, older DNS lookup
getent hosts example.comresolve the way programs do, including /etc/hosts
nc -zv host 5432is a TCP port open?
nc -l 9000listen on a port (OpenBSD nc; traditional netcat wants nc -l -p 9000)

ss -p needs sudo to show processes owned by other users. dig comes from dnsutils (Debian/Ubuntu) or bind-utils (Fedora/RHEL).

Old (net-tools)Current (iproute2)
ifconfigip a
route -nip r
netstat -tulpnss -tulpn
arp -aip neigh

curl and wget

curlDoes
curl URLGET; body to stdout
-o file / -Osave to file / under the remote file name
-Lfollow redirects
-ffail (exit 22) on HTTP 4xx/5xx instead of saving the error page
-s / -Ssilent / still show errors; -fsSL is the usual script combination
-I / -iheaders only (HEAD request) / headers plus body
-vverbose: connection, TLS, request and response headers
-X POSTHTTP method
-H 'Authorization: Bearer …'add a header
-d 'a=1&b=2'form data (makes it a POST)
--json '{"name":"x"}'JSON body with JSON headers (curl 7.82+)
-F 'file=@photo.jpg'multipart file upload
-u user:passbasic auth
-C -resume a partial download
--retry 3retry transient failures
--max-time 10 / --connect-timeout 5overall / connection time limit in seconds
-w '%{http_code}\n'print the status code (and other -w variables)
-kskip TLS certificate checks: debugging only
wgetDoes
wget URLdownload into the current directory
wget -O out.tgz URLchoose the file name (-O - writes to stdout)
wget -c URLresume
wget -q URLquiet
wget -r -np -k URLrecursive, never ascend to the parent, rewrite links for offline use
wget --spider URLcheck that it exists without downloading

Remote shells and copying

CommandDoes
ssh user@hostlog in; ssh -p 2222 user@host for another port
ssh host 'df -h'run one command remotely
scp file user@host:/tmp/copy up
scp user@host:/var/log/app.log .copy down
scp -r dir host:a directory, into the remote home (-P 2222: capital P for the port)
rsync -avP src/ host:/srv/app/incremental copy with progress, resumable
rsync -avn --delete src/ host:/srv/app/dry run of an exact mirror

With rsync, src/ copies the contents of src and src copies the directory itself. Keys, ~/.ssh/config and tunnels are in SSH, keys & certificates.

Archives and compression

CommandDoes
tar czf site.tgz site/create (c) a gzip-compressed (z) archive file (f)
tar cJf site.tar.xz site/xz: smaller, slower
tar --zstd -cf site.tar.zst site/zstd: fast and small
tar czf b.tgz --exclude=node_modules app/skip paths
tar tf site.tgzlist the contents (t)
tar xf site.tgzextract (x); GNU tar detects the compression
tar xf site.tgz -C /optextract into another directory
tar xf site.tgz site/index.htmlextract one member
tar xf site.tgz --strip-components=1drop the top-level folder
tar xvf …v: list files while working
zip -r a.zip dir/zip a directory (for Windows and macOS users)
unzip a.zip -d out/extract into out/
unzip -l a.ziplist
gzip fcompress to f.gz and delete f; -k keeps it
gunzip f.gzdecompress (same as gzip -d)
zcat f.gz, zless f.gz, zgrep x f.gzread without unpacking to disk
zstd fcompress to f.zst and keep f; --rm deletes it
zstd -d f.zstdecompress (same as unzstd)
zstd -19 -T0 fhigh compression on all cores
xz -T0 f / xz -d f.xzxz for single files
ExtensionCompressortar flag
.tarnone
.tar.gz, .tgzgzipz
.tar.bz2bzip2j
.tar.xzxzJ
.tar.zstzstd--zstd
.zipzipuse unzip

Packages

Taskapt (Debian, Ubuntu)dnf (Fedora, RHEL)pacman (Arch)brew (macOS)
Refresh the indexsudo apt updateautomaticpart of -Syubrew update
Upgrade everythingsudo apt update && sudo apt upgradesudo dnf upgradesudo pacman -Syubrew upgrade
Installsudo apt install jqsudo dnf install jqsudo pacman -S jqbrew install jq
Removesudo apt remove jqsudo dnf remove jqsudo pacman -Rns jqbrew uninstall jq
Searchapt search jqdnf search jqpacman -Ss jqbrew search jq
Detailsapt show jqdnf info jqpacman -Si jqbrew info jq
List installedapt list --installeddnf list --installedpacman -Qbrew list
List upgradableapt list --upgradablednf check-upgradepacman -Qubrew outdated
Which package owns a filedpkg -S /usr/bin/jqrpm -qf /usr/bin/jqpacman -Qo /usr/bin/jq—
Files in a packagedpkg -L jqrpm -ql jqpacman -Ql jqbrew list jq
Remove unused dependenciessudo apt autoremovesudo dnf autoremovesudo pacman -Rns $(pacman -Qdtq)brew autoremove
Clear the download cachesudo apt cleansudo dnf clean allsudo pacman -Scbrew cleanup
Install a downloaded filesudo apt install ./x.debsudo dnf install ./x.rpmsudo pacman -U x.pkg.tar.zst—
Hold a versionsudo apt-mark hold jqsudo dnf versionlock add jqIgnorePkg in /etc/pacman.confbrew pin jq
  • RHEL's dnf4 spells the upgradable check dnf check-update, and versionlock needs its plugin.
  • apt is meant for people; scripts should call apt-get, whose output is stable.
  • On Arch, never refresh with pacman -Sy alone and then install: always upgrade with -Syu.
  • Homebrew runs without sudo; desktop apps install with brew install --cask.

Repositories, pinning and reboot checks are in Linux sysadmin.

Services and logs

On systemd distros (all the common ones), long-running programs are units managed by systemctl, and their output goes to the journal, read with journalctl.

CommandDoes
systemctl status nginxrunning or not, PID, memory, last log lines
sudo systemctl start nginx / stop / restartcontrol it now
sudo systemctl reload nginxre-read config without a restart (if the service supports it)
sudo systemctl enable --now nginxstart at boot, and start now
sudo systemctl disable --now nginxthe reverse
systemctl is-active nginxactive or inactive, for scripts
systemctl list-units --type=serviceloaded services
systemctl list-units --failedwhat's broken
sudo systemctl daemon-reloadafter editing unit files
journalctl -u nginx -ffollow one service's log
journalctl -u nginx -n 100 --no-pagerits last 100 lines
journalctl -u nginx --since '1 hour ago'a time window; also today, '2026-11-11 09:00', --until
journalctl -p err -berrors and worse since this boot
journalctl -kkernel messages
journalctl -xethe latest entries, with explanations
journalctl --disk-usagehow much space the journal takes

Writing unit files, timers and cron jobs is covered in Linux sysadmin and its journalctl and scheduling sections.

Users

CommandDoes
whoamiyour user name
idUID, GID and all groups
groupsgroup names only
who / wwho is logged in / and what they're running
passwdchange your own password
sudo adduser alicecreate a user interactively (Debian, Ubuntu)
sudo useradd -m -s /bin/bash alicecreate a user with a home and shell (every distro)
sudo passwd aliceset another user's password
sudo usermod -aG docker aliceadd to a group; without -a her other groups are removed
sudo gpasswd -d alice dockerremove from a group
sudo userdel -r alicedelete the user and her home
su - alice / sudo -iu alicebecome alice with her password / with yours
getent passwd aliceaccount details: UID, home, shell
chsh -s /bin/zshchange your login shell
last -n 10recent logins

Group changes apply at the next login (or newgrp docker in the current shell). The admin group is sudo on Debian and Ubuntu and wheel on Fedora and RHEL. More in Linux sysadmin.

References