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 am
pwd
go home / back to the previous directory
cd / cd -
list everything with sizes
ls -lah
list newest files last
ls -ltr
make nested directories
mkdir -p a/b/c
copy a directory, keeping permissions
cp -a src/ dst/
rename or move
mv old new
delete a directory tree
rm -r dir (look first: ls dir)
make a shortcut (symlink)
ln -s /path/to/target name
read a long file
less file
watch a log grow
tail -F app.log
count lines
wc -l file
find files by name
find . -name '*.conf' or fd conf
find text inside files
grep -rn 'text' . or rg text
replace text in a file
sed -i 's/old/new/g' file
know which program runs
type -a python3
save output and errors to a file
cmd > out.log 2>&1
make a script executable
chmod +x script.sh
change a file's owner
sudo chown -R user:group dir
run the last command as root
sudo !!
see running processes
ps aux or htop
stop a process
kill PID, pkill -f pattern
find what uses port 3000
sudo ss -ltnp 'sport = :3000'
check free disk space
df -h
see what is big here
du -sh * | sort -h
check memory
free -h
see the OS and kernel version
cat /etc/os-release, uname -r
see my IP addresses
ip -br a
test if a host is up / a port is open
ping -c 3 host / nc -zv host 443
look up DNS
dig +short example.com
download a file
curl -fLO URL
call a JSON API
curl -fsS URL | jq .
copy files to a server
rsync -avP dir/ host:/path/
pack / unpack a tarball
tar czf out.tgz dir/ / tar xf file.tgz
install a package
sudo apt install x / sudo dnf install x
restart a service and read its logs
sudo systemctl restart x, journalctl -u x -f
keep a job running after I log out
tmux, or nohup cmd &
rerun a command every 2 seconds
watch -n 2 cmd
get help for a command
man 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
Part
Meaning
alice@web1
user and host name
~/src
current directory; ~ is your home (/home/alice)
$ / #
ordinary user / root; slow down at a # prompt
ls
the command: a program on PATH, a shell builtin, an alias or a function
-la
short options, combined (-l -a)
--color=auto
long option; values follow = or a space
/etc
argument: 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
Command
Shows
man ls
the manual page; /word search, n next, q quit
man 5 crontab
a specific section (5: file formats)
man -k compress
search page names and descriptions (same as apropos)
ls --help
built-in usage summary (most GNU tools)
help cd
bash builtins such as cd, export, jobs (zsh: man zshbuiltins)
tldr tar
short community examples (install a tldr client)
type ls
whether a name is an alias, builtin, function or file
whatis ls
one-line description
Section
Holds
1
user commands (ls, grep)
2, 3
system calls, C library functions
5
file formats (passwd, crontab, fstab)
7
overviews (signal, regex, hier)
8
admin 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.
Keys
Does
Ctrl-A / Ctrl-E
jump to start / end of the line
Alt-B / Alt-F
back / forward one word (macOS Terminal: enable "Use Option as Meta key")
Ctrl-U
cut from the cursor to the start of the line (zsh: the whole line)
Ctrl-K
cut from the cursor to the end of the line
Ctrl-W
cut the previous word
Ctrl-Y
paste what you cut
Ctrl-_
undo
Ctrl-L
clear the screen (like clear)
Ctrl-C
interrupt the running command (SIGINT), or abandon the line
search 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-E
edit the line in $EDITOR (bash)
Ctrl-S / Ctrl-Q
freeze / unfreeze output; if the terminal seems stuck, press Ctrl-Q
reset
(a command) repair a garbled terminal
History
Form
Means
history
numbered 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
!42
command number 42 from history
!ssh
the most recent command starting with ssh
!ssh:p
print it without running
^old^new
rerun 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.
Navigating and files
pwd # print working directorycd /var/log # absolute path: starts with /cd src # relative to where you arecd .. # up one levelcd # home (same as cd ~)cd - # back to the previous directorypushd /etc; popd # visit, then return
Path shortcuts: ~ home, . this directory, .. the parent, / the root of the filesystem.
Command
Shows
ls
names
ls -l
long: mode, links, owner, group, size, modified time
ls -a
include hidden dotfiles (names that start with .)
ls -A
like -a without . and ..
ls -lah
long, all, human-readable sizes (4.0K, 1.2G)
ls -lt / ls -ltr
newest first / newest last
ls -lS
biggest first
ls -d */
directories only
ls -R
recursive
ls -1
one name per line
ls -i
inode numbers
tree -L 2
tree two levels deep (tree package); -a dotfiles, -d dirs only, -I 'node_modules' skip
Create, copy, move, delete
Command
Does
mkdir logs
make a directory
mkdir -p a/b/c
make missing parents too; no error if it exists
touch notes.md
create an empty file, or update an existing file's modified time
cp a.txt b.txt
copy a file; overwrites b.txt without asking
cp -i a b / cp -n a b
ask before overwriting / never overwrite
cp -r src dst
copy a directory recursively
cp -a src dst
archive copy: recursive, keeps modes, owners, timestamps and symlinks
cp -v
print each file copied
mv old new
rename
mv a b dir/
move several files into a directory; -i ask, -n never overwrite
ln -s /opt/app/current app
symlink called app pointing at the target (target first, like cp)
ln -sfn releases/v2 current
repoint an existing symlink to a directory
readlink -f app
resolve a path to its real location
rm file
delete a file (no trash, no undo)
rm -r dir
delete a directory tree; -i asks for each file, -I asks once
rm -rf dir
no questions, no errors for missing files
rmdir dir
delete only if empty
stat file
size, mode, owner, inode, access / modify / change times
stat -c '%a %U %s %n' file
chosen fields (GNU; macOS uses stat -f)
file mystery
what 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 srcinside it
(dst/src).
Viewing files
Command
Use
cat file
print a whole (short) file
cat a b > c
concatenate files
cat -n file
with line numbers
less file
scroll through a file (keys below)
head file / head -n 50 file
first 10 / 50 lines
head -c 200 file
first 200 bytes
tail -n 50 file
last 50 lines
tail -n +2 file
everything from line 2 on (skip a header)
tail -f app.log
keep printing new lines as they arrive; Ctrl-C stops
tail -F app.log
follow by name, so it survives log rotation
wc -l file
count lines; -w words, -c bytes, -m characters
wc -l *.ts
per file, plus a total
tac file
lines in reverse order
xxd file | head
bytes in hex (hexdump -C is similar)
bat file
cat with syntax highlighting (Debian/Ubuntu name: batcat)
Key in less
Does
Space / b
page down / up
j / k, arrows
line down / up
g / G
top / bottom
/text / ?text
search forward / backward; n next, N previous
&text
show only matching lines; & then Enter clears
F
follow like tail -f; Ctrl-C stops following
-S
toggle wrapping of long lines (typed inside less)
v
open the file in $EDITOR
h / q
help / 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.
Command
Finds
find . -name '*.log'
by name, case-sensitive; quote the pattern
find . -iname 'readme*'
case-insensitive
find . -type f / -type d / -type l
files / directories / symlinks
find . -maxdepth 1 -type d
only the top level
find . -mtime -1
modified in the last 24 hours
find . -mtime +30
modified more than 30 days ago (counted in whole days)
skip a directory without descending into it (faster)
find . -name '*.tmp' -delete
delete 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 cmd
pass names safely, even with spaces or newlines
find / -xdev -name core
stay on one filesystem
find / -name x 2>/dev/null
hide "Permission denied" noise
Command
Use
locate nginx.conf
instant search of a name database (plocate package); refresh with sudo updatedb
fd conf
a 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 python3
first match on PATH
type -a python3
every match, plus aliases, functions and builtins
command -v jq
portable check for scripts: prints the path, exit status 1 if missing
whereis ls
binary, source and man page locations
# stop a script early if a tool is missingcommand -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.
Command
Does
grep 'error' app.log
lines containing error
grep -i
ignore case
grep -n
show line numbers
grep -r 'x' dir/
search a directory recursively (-R also follows symlinks)
grep -w
whole words only
grep -v
invert: lines that do not match
grep -c
count matching lines
grep -l / grep -L
names of files that match / don't
grep -o
print 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 3
3 lines after / before / around each match
grep -h / -H
hide / show file names
grep -q
quiet, exit status only: if grep -q x f; then …
--include='*.py'
with -r: only these file names
--exclude-dir=node_modules
with -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/blanksgrep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' access.log \ | sort | uniq -c | sort -rn | head # top IPsps aux | grep '[n]ginx' # [n] keeps grep out of the result
Regex
Matches
.
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|b
either (with -E)
(ab)+
group (with -E)
\b, \w, \s
word 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.
Command
Does
rg 'pattern'
search from here down
rg -i / rg -S
ignore case / smart case (case-sensitive only if the pattern has capitals)
rg -w, -F, -v, -c, -l, -o, -n, -C 3
same 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 -uu
also hidden files / also ignored and hidden files
rg --files | rg config
list 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 x
NUL-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
Command
Does
cut -d: -f1 /etc/passwd
field 1, with : as the separator
cut -d, -f2,4 / cut -d, -f3-
fields 2 and 4 / field 3 to the end
cut -c1-80
characters 1 to 80
sort file
alphabetical
sort -n / -h / -V
numeric / human sizes (2K, 1G) / versions (1.10 after 1.9)
sort -r
reverse
sort -t, -k2,2n
by field 2 of comma-separated lines, numerically
sort -u
sorted and deduplicated
sort -o file file
sort a file in place (sort file > file empties it)
LC_ALL=C sort
plain byte order, faster and locale-independent
uniq
collapse adjacent duplicates, so sort first
uniq -c
prefix each line with its count
uniq -d / uniq -u
only repeated / only unique lines
tr a-z A-Z
uppercase; 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 3
3 random lines
# frequency count: the most common lines firstsort access.log | uniq -c | sort -rn | head
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.
Command
Does
sed 's/old/new/' f
replace the first match on each line
sed 's/old/new/g' f
replace every match
sed 's/old/new/gI' f
case-insensitive (GNU)
sed -i 's/old/new/g' f
edit the file in place (GNU)
sed -i.bak 's/old/new/g' f
in place, keeping f.bak (GNU and macOS)
sed -i '' 's/old/new/g' f
in place on macOS and BSD
sed 's#/usr/local#/opt#g' f
any delimiter; handy for paths
sed -E 's/([0-9]+)px/\1rem/g' f
extended regex; \1 is the first group
sed 's/.*/"&"/' f
& is the whole match
sed -n '10,20p' f
print only lines 10 to 20
sed -n '/BEGIN/,/END/p' f
print the block between two patterns
sed '/^#/d' f / sed '/^$/d' f
delete comment / blank lines
sed '1d' f / sed '$d' f
delete the first / last line
sed '3i text' f / sed '/re/a text' f
insert before line 3 / append after matching lines (GNU)
sed 's/^[ \t]*//' f
trim 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.
Command
Does
awk '{print $1}' f
first whitespace-separated field
awk '{print $1, $NF}' f
first and last field
awk -F: '{print $1, $7}' /etc/passwd
custom separator
awk 'NR > 1' f
skip the header line
awk 'NR == 5' f
line 5 only
awk '$3 > 100' f
rows where field 3 is over 100
awk '/error/ {print $2}' f
field 2 of matching lines
awk '{s += $2} END {print s}' f
sum a column
awk '{s += $2} END {print s / NR}' f
average
awk '!seen[$0]++' f
drop duplicate lines, keep order, no sort needed
awk 'length > 120' f
lines longer than 120 characters
awk '{n[$1]++} END {for (k in n) print n[k], k}' f
count by key
awk '{printf "%-20s %s\n", $1, $2}' f
aligned columns
awk -F, -v OFS='\t' '{$1 = $1; print}' f
simple 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).
Command
Does
… | xargs rm
run rm with every input word as an argument
… | xargs -n 1 cmd
one argument per run
… | xargs -P 4 -n 1 cmd
four runs in parallel
… | xargs -I {} cp {} /backup/
put each input line where {} is
find … -print0 | xargs -0 cmd
NUL-separated input: safe with spaces, quotes and newlines
rg -l -0 x | xargs -0 cmd
same with ripgrep (grep -lZ for grep)
xargs -r
don't run at all when the input is empty (GNU)
xargs -t / xargs -p
print 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
Command
Does
cmd | tee out.log
show output and save it
cmd | tee -a out.log
append instead
echo x | sudo tee /etc/f >/dev/null
write a root-owned file; sudo echo x > /etc/f fails because the redirect runs as you
paste a.txt b.txt
join files side by side, tab-separated
paste -sd, file
join all lines with commas
column -t
align whitespace-separated columns: mount | column -t
column -t -s,
align a CSV
diff a b
show differences
diff -u a b
unified format, as used by patches and git
diff -r dir1 dir2 / diff -rq dir1 dir2
compare 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 b
are two binary files identical?
sha256sum f
checksum; sha256sum -c SHA256SUMS verifies a list
jq
jq filters and reshapes JSON; install it with your package manager.
Command
Gives
jq . f.json
pretty-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.json
slurp JSON lines into one array
jq --arg u alice '.[] | select(.user == $u)'
pass a shell value safely
jq -e '.ok' >/dev/null
exit 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.
here-doc: the following lines up to EOF as stdin, literally
<< EOF
here-doc with $VAR and $(cmd) expanded
<<-EOF
here-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
: > f
truncate a file to zero bytes
set -o noclobber
make > refuse to overwrite (>| forces)
Syntax
Does
a; b
run both, in order
a && b / a || b
run 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 pipefail
a 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.
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 filemake 2>&1 > build.log # errors still on screen# here-doc into a filecat > app.ini <<'EOF'[server]port = 8080EOFdiff <(ls dir1) <(ls dir2) # compare two listingsecho '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 *.
Pattern
Matches
*
any characters, but not a leading .
?
exactly one character
[abc], [a-z], [!0-9]
one of / a range / not these
**/*.ts
any 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
~, ~bob
your home, Bob's home
ls *.log # every .log file herecp config.yml{,.bak} # cp config.yml config.yml.bakmkdir -p app/{src,test,docs}mv report.{txt,md} # change the extensionecho 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'.
Quoting
Behavior
'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)
unquoted
expanded, then split on whitespace, then globbed
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 $HOMEecho "$HOME" # prints /home/aliceecho "today is $(date +%F)"
Variables and environment
Syntax
Does
name=value
set a shell variable; no spaces around =
"$name", "${name}_old"
use it; braces when text follows
export name=value
also pass it to programs you start (an environment variable)
NAME=value cmd
set it for one command only
unset name
remove it
env / printenv
list environment variables
printenv PATH
one 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
Variable
Holds
$HOME, $USER, $PWD
home, user name, current directory
$PATH
directories searched for commands, :-separated, left to right
$SHELL
your login shell
$EDITOR
editor used by git, crontab -e, sudoedit
$LANG, $LC_ALL
locale, 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 lineexport PATH="$HOME/.local/bin:$PATH" # prepend a dirhash -r # bash: forget cached command locations
Shell
Reads at startup
Put there
bash, login shell (SSH, console)
/etc/profile, then the first of ~/.bash_profile, ~/.bash_login, ~/.profile
environment variables; Ubuntu's ~/.profile and Fedora's ~/.bash_profile also load ~/.bashrc
bash, other interactive shells (a new terminal tab on Linux)
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
Octal
Bits
Means
0
---
nothing
1
--x
execute
2
-w-
write
3
-wx
write, execute
4
r--
read
5
r-x
read, execute
6
rw-
read, write
7
rwx
read, 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.
Mode
Symbolic
Typical use
644
rw-r--r--
ordinary files
755
rwxr-xr-x
directories, scripts, programs
600
rw-------
secrets, SSH private keys
700
rwx------
private directories such as ~/.ssh
664 / 775
rw-rw-r-- / rwxrwxr-x
files / directories shared with a group
Command
Does
chmod +x run.sh
add execute for everyone
chmod u+x run.sh
for the owner (u) only; g group, o others, a all
chmod go-w file
remove write from group and others
chmod u=rw,go=r file
set exactly (same as 644)
chmod 600 key.pem
octal form
chmod -R u=rwX,go=rX dir/
recursive; capital X adds execute to directories only (and files already executable)
sudo chown alice file
change the owner
sudo chown alice:devs file
owner and group
sudo chown :devs file
group only (same as chgrp devs file)
sudo chown -R www-data:www-data /srv/site
recursive
umask
mask removed from new files' modes: 0022 gives files 644, dirs 755
umask 077
new files private (600, dirs 700) in this shell
Command
Does
sudo cmd
run one command as root; asks for your password
sudo !!
rerun the last command with sudo
sudo -u postgres psql
run as another user
sudo -i
root login shell; exit to leave
sudo -l
list what you may run
sudo -k
forget the cached password
sudoedit /etc/hosts
edit a root-owned file in your own editor
su - alice
become 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.
Command
Shows / does
ps aux
every process: user, PID, %CPU, %MEM, command
ps aux | grep '[n]ginx'
find one (the brackets keep grep itself out)
ps -ef --forest
parent/child tree
ps aux --sort=-%mem | head
biggest memory users
ps -o pid,ppid,etime,cmd -p 1234
chosen columns for one PID
pgrep -a node
PIDs and command lines by name
pgrep -f 'server.js'
match anywhere in the full command line
pgrep -u alice
by user
top
live view: P sort by CPU, M memory, 1 per core, k kill, q quit
htop, btop
friendlier 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 cmd
how long a command takes
timeout 30s cmd
stop it after 30 seconds
psSTAT 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).
Command
Signal
Effect
kill 1234
TERM (15)
ask the process to exit; the default
kill -INT 1234
INT (2)
same as Ctrl-C
kill -HUP 1234
HUP (1)
many daemons reload their config
kill -9 1234
KILL (9)
force; no cleanup, only after TERM failed
kill -STOP 1234 / -CONT
STOP / CONT
pause / resume
pkill -f 'bun run'
TERM
by command-line match
pkill -u bob
TERM
every process of a user
killall nginx
TERM
by exact name (psmisc package)
kill -0 1234
none
exit status says whether it exists and you may signal it
kill -l
list signal names
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.
start in the background; prints [1] 12345 (job number, PID)
Ctrl-Z
suspend the foreground job
bg / bg %1
continue it in the background
fg / fg %1
bring it to the foreground
jobs -l
list jobs with PIDs
kill %1
signal a job by number
wait
wait for all background jobs to finish
disown %1
detach 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 cmd
run 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.
tmux
Does
tmux new -s work
start a session named work
Ctrl-B d
detach; it keeps running
tmux ls
list sessions
tmux attach -t work
reattach
Ctrl-B c / n / p
new 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
Command
Shows
uname -a
kernel name, release, architecture
uname -r / uname -m
kernel release / CPU architecture (x86_64, aarch64)
cat /etc/os-release
distro name and version
hostnamectl
hostname, OS, kernel, virtualization
uptime
time since boot and load averages (1, 5, 15 minutes)
nproc / lscpu
CPU count / model, cores, threads, caches
free -h
memory; read the available column, not free
df -h
used and free space per mounted filesystem
df -hT / df -i
with filesystem types / inode usage
du -sh dir
total size of a directory
du -sh * | sort -h
size of each entry here, biggest last
sudo du -xh -d1 / | sort -h
top-level breakdown of one filesystem
ncdu
interactive disk-usage browser (install it)
lsblk / lsblk -f
disks and partitions / with filesystems and mount points
lsof -i :3000
what has port 3000 open
lsof -p 1234
files and sockets a process holds
lsof /var/log/app.log
who holds a file open
sudo lsof +L1
deleted files still held open (their space is not freed yet)
lspci, lsusb
PCI and USB devices
sudo dmesg -T | tail
recent kernel messages with timestamps
date, timedatectl
time, time zone, NTP sync
who, w, last
who is logged in, what they run, recent logins
The first minute on a slow machine (vmstat, iostat, sar) is in
Linux sysadmin.
Networking
Command
Shows / does
ip a / ip -br a
interfaces and their addresses (brief: one line each)
reachability and round-trip time (Linux pings forever without -c)
traceroute example.com
each hop on the path; tracepath needs no root
mtr -rw example.com
traceroute plus loss per hop, as a report
dig example.com
DNS answer with details
dig +short example.com
just the answer
dig example.com MX
other record types: AAAA, TXT, NS, CNAME
dig @1.1.1.1 example.com
ask a specific resolver
dig -x 203.0.113.7
reverse lookup
nslookup example.com
simpler, older DNS lookup
getent hosts example.com
resolve the way programs do, including /etc/hosts
nc -zv host 5432
is a TCP port open?
nc -l 9000
listen 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)
ifconfig
ip a
route -n
ip r
netstat -tulpn
ss -tulpn
arp -a
ip neigh
curl and wget
curl
Does
curl URL
GET; body to stdout
-o file / -O
save to file / under the remote file name
-L
follow redirects
-f
fail (exit 22) on HTTP 4xx/5xx instead of saving the error page
-s / -S
silent / still show errors; -fsSL is the usual script combination
-I / -i
headers only (HEAD request) / headers plus body
-v
verbose: connection, TLS, request and response headers
-X POST
HTTP 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:pass
basic auth
-C -
resume a partial download
--retry 3
retry transient failures
--max-time 10 / --connect-timeout 5
overall / connection time limit in seconds
-w '%{http_code}\n'
print the status code (and other -w variables)
-k
skip TLS certificate checks: debugging only
wget
Does
wget URL
download into the current directory
wget -O out.tgz URL
choose the file name (-O - writes to stdout)
wget -c URL
resume
wget -q URL
quiet
wget -r -np -k URL
recursive, never ascend to the parent, rewrite links for offline use
wget --spider URL
check that it exists without downloading
Remote shells and copying
Command
Does
ssh user@host
log 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
Command
Does
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.tgz
list the contents (t)
tar xf site.tgz
extract (x); GNU tar detects the compression
tar xf site.tgz -C /opt
extract into another directory
tar xf site.tgz site/index.html
extract one member
tar xf site.tgz --strip-components=1
drop 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.zip
list
gzip f
compress to f.gz and deletef; -k keeps it
gunzip f.gz
decompress (same as gzip -d)
zcat f.gz, zless f.gz, zgrep x f.gz
read without unpacking to disk
zstd f
compress to f.zst and keepf; --rm deletes it
zstd -d f.zst
decompress (same as unzstd)
zstd -19 -T0 f
high compression on all cores
xz -T0 f / xz -d f.xz
xz for single files
Extension
Compressor
tar flag
.tar
none
.tar.gz, .tgz
gzip
z
.tar.bz2
bzip2
j
.tar.xz
xz
J
.tar.zst
zstd
--zstd
.zip
zip
use unzip
Packages
Task
apt (Debian, Ubuntu)
dnf (Fedora, RHEL)
pacman (Arch)
brew (macOS)
Refresh the index
sudo apt update
automatic
part of -Syu
brew update
Upgrade everything
sudo apt update && sudo apt upgrade
sudo dnf upgrade
sudo pacman -Syu
brew upgrade
Install
sudo apt install jq
sudo dnf install jq
sudo pacman -S jq
brew install jq
Remove
sudo apt remove jq
sudo dnf remove jq
sudo pacman -Rns jq
brew uninstall jq
Search
apt search jq
dnf search jq
pacman -Ss jq
brew search jq
Details
apt show jq
dnf info jq
pacman -Si jq
brew info jq
List installed
apt list --installed
dnf list --installed
pacman -Q
brew list
List upgradable
apt list --upgradable
dnf check-upgrade
pacman -Qu
brew outdated
Which package owns a file
dpkg -S /usr/bin/jq
rpm -qf /usr/bin/jq
pacman -Qo /usr/bin/jq
—
Files in a package
dpkg -L jq
rpm -ql jq
pacman -Ql jq
brew list jq
Remove unused dependencies
sudo apt autoremove
sudo dnf autoremove
sudo pacman -Rns $(pacman -Qdtq)
brew autoremove
Clear the download cache
sudo apt clean
sudo dnf clean all
sudo pacman -Sc
brew cleanup
Install a downloaded file
sudo apt install ./x.deb
sudo dnf install ./x.rpm
sudo pacman -U x.pkg.tar.zst
—
Hold a version
sudo apt-mark hold jq
sudo dnf versionlock add jq
IgnorePkg in /etc/pacman.conf
brew 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.
Command
Does
systemctl status nginx
running or not, PID, memory, last log lines
sudo systemctl start nginx / stop / restart
control it now
sudo systemctl reload nginx
re-read config without a restart (if the service supports it)
sudo systemctl enable --now nginx
start at boot, and start now
sudo systemctl disable --now nginx
the reverse
systemctl is-active nginx
active or inactive, for scripts
systemctl list-units --type=service
loaded services
systemctl list-units --failed
what's broken
sudo systemctl daemon-reload
after editing unit files
journalctl -u nginx -f
follow one service's log
journalctl -u nginx -n 100 --no-pager
its last 100 lines
journalctl -u nginx --since '1 hour ago'
a time window; also today, '2026-11-11 09:00', --until
create a user with a home and shell (every distro)
sudo passwd alice
set another user's password
sudo usermod -aG docker alice
add to a group; without -a her other groups are removed
sudo gpasswd -d alice docker
remove from a group
sudo userdel -r alice
delete the user and her home
su - alice / sudo -iu alice
become alice with her password / with yours
getent passwd alice
account details: UID, home, shell
chsh -s /bin/zsh
change your login shell
last -n 10
recent 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.