DevOpsIndex

Linux Commands Cheat Sheet

Organized by category. Know these cold.


File Viewing

cat file.txt              # print whole file
less file.txt             # paginated (q to quit, / to search)
head -20 file.txt         # first 20 lines
tail -20 file.txt         # last 20 lines
tail -f /var/log/app.log  # follow live
tail -F /var/log/app.log  # follow even if file rotates

# Specific line(s)
sed -n "5001p" file.txt           # fastest for large files
sed -n "5001{p;q}" file.txt       # print and exit immediately (even faster)
sed -n "100,200p" file.txt        # range of lines
awk "NR==5001" file.txt           # awk equivalent
head -5001 file.txt | tail -1     # head+tail combo

wc -l file.txt   # count lines (NOT print -- common mistake)
wc -w file.txt   # count words
wc -c file.txt   # count bytes

Principle: sed -n suppresses all output then prints only line N. sed with p;q exits after match -- fastest for large files. wc -l counts lines, it does NOT print them.


grep

grep "error" app.log
grep -i "error" app.log                 # case-insensitive
grep -r "TODO" ./src/                   # recursive
grep -l "error" *.log                   # filenames only
grep -n "error" app.log                 # line numbers
grep -v "DEBUG" app.log                 # invert match
grep -c "error" app.log                 # count matches

grep -A 3 "ERROR" app.log              # 3 lines after
grep -B 3 "ERROR" app.log              # 3 lines before
grep -C 3 "ERROR" app.log              # 3 lines before AND after

grep -E "error|warn|fatal" app.log
grep -E "^[0-9]{4}-" app.log

grep "ERROR" app.log | wc -l
grep "5[0-9][0-9]" access.log          # 5xx HTTP errors

sed

sed "s/old/new/" file.txt              # replace first per line
sed "s/old/new/g" file.txt             # replace all
sed -i "s/old/new/g" file.txt          # in-place edit
sed -i.bak "s/old/new/g" file.txt      # in-place with backup

sed "/pattern/d" file.txt             # delete matching lines
sed "5d" file.txt
sed "5,10d" file.txt

sed -n "5001p" file.txt
sed -n "5001,5010p" file.txt
sed -n "/start/,/end/p" file.txt

sed "s/127\.0\.0\.1/10\.0\.0\.1/g" config
sed "/^#/d" config.txt | sed "/^$/d"              # remove comments and blank lines

awk

awk "{print \}" file.txt
awk "{print \, \}" file.txt
awk -F: "{print \}" /etc/passwd
awk -F, "{print \}" data.csv

awk "\ > 100 {print \, \}" file
awk "NR > 1 {print}" file             # skip header
awk "NR==5001" file
awk "/ERROR/ {print NR": "ash}" log

awk "{sum += \} END {print sum}" f
awk "END {print NR}" file
awk "{total += \; count++} END {print total/count}" f

ps aux | awk "NR>1 {sum += \} END {print "Total CPU%:", sum}"
awk -F: "\ >= 1000 {print \}" /etc/passwd
ps aux | awk "\ ~ /D/"                          # D-state processes

Scenario: Top N Slow Requests from a Log File (awk vs grep)

Task: log lines contain timestamp, response code, and response time columns. Extract the top 100 requests with response time greater than 100ms.

Why grep alone cannot do this: grep is a line-matching tool — it tests whether a line matches a text pattern (regex/fixed string). It has no concept of:

  • numeric comparison (> 100) — regex can match digit patterns, but can't express "greater than," so it can't reliably distinguish 99 from 100 from 1000 without fragile digit-count hacks
  • columns/fields — grep sees the whole line as one string, not fields
  • sorting — grep has no sort capability at all

So grep can filter lines containing certain digit patterns, but it cannot do "numeric field > threshold, sorted, top N" — that requires field-aware, numeric-aware tooling. awk (or perl/python/mlr) is the right tool because it parses fields and does real numeric comparisons.

# Log line example (space-separated):
# 2026-07-14T00:10:23 GET /api/users 200 142
#   field: 1=timestamp 2=method 3=path 4=status 5=response_time_ms

awk '$5 > 100' access.log | sort -k5 -nr | head -100
  • awk '$5 > 100' — filters lines where field 5 (response time) is numerically greater than 100. This is real numeric comparison, not text matching.
  • sort -k5 -nr — sort by field 5, numeric (-n), descending (-r).
  • head -100 — top 100 results.

Combining awk + grep is still useful — grep is great for a first-pass narrowing before awk does the numeric work, e.g. filtering to a specific endpoint or status code range first:

grep "/api/orders" access.log | awk '$5 > 100' | sort -k5 -nr | head -100
grep -E " 5[0-9]{2} " access.log | awk '$5 > 100' | sort -k5 -nr | head -100   # narrow to 5xx first

If the log is JSON ({"time":"...", "response_time_ms":142, "status":200}), use jq instead of awk/grep:

jq -c 'select(.response_time_ms > 100)' access.log | jq -s 'sort_by(-.response_time_ms) | .[:100]'

If the log is CSV with a header, prefer mlr (Miller) over raw awk -F, — it's column-name-aware instead of column-number-aware, avoiding off-by-one errors if columns shift:

mlr --csv filter '$response_time > 100' then sort -nr response_time then head -n 100 access.log

Principle: grep = text pattern matching on whole lines, no numeric/field awareness, no sort. awk/perl/mlr/jq = field-aware, numeric-aware, composable with sort. Use grep to narrow candidate lines by pattern first (cheap, fast), then hand off to awk for numeric filtering and sort/head for ranking.


cut / sort / uniq / tr

cut -d: -f1 /etc/passwd
cut -d, -f2,4 data.csv
cut -c1-10 file.txt

sort file.txt
sort -n file.txt
sort -r file.txt
sort -k2 file.txt
sort -t: -k3 -n /etc/passwd

sort file.txt | uniq
sort file.txt | uniq -c
sort file.txt | uniq -d
# for findings the unique top 10 entries in the file based on sorting numerics
sort access.log | uniq -c | sort -rn | head -10

echo "hello" | tr "a-z" "A-Z"
echo "a:b:c" | tr ":" "<br>"
cat file | tr -d "\r"

find

find /var/log -name "*.log"
find . -name "*.tf" -type f
find / -name "passwd" 2>/dev/null

find /var/log -mtime +7
find /tmp -mtime +1 -delete
find . -newer reference.txt

find / -type f -size +100M
find . -empty

find / -perm 777 -type f
find / -user root -perm -4000

find /tmp -name "*.tmp" -exec rm {} \;
find . -name "*.log" -exec grep "ERROR" {} +
find /app -name "*.py" | xargs grep "import"

Networking

ss -tulpn
ss -s
netstat -tulpn
lsof -i :8080
lsof -i TCP -s TCP:ESTABLISHED

dig google.com
dig google.com A
dig @8.8.8.8 google.com
nslookup google.com
host google.com

ping -c 4 8.8.8.8
traceroute 8.8.8.8
curl -I https://api.example.com
curl -v https://api.example.com
curl -o /dev/null -s -w "%{http_code}" URL
wget -q -O- URL | head

tcpdump -i eth0 port 443
tcpdump -i any host 10.0.0.1
tcpdump -w capture.pcap

Processes

ps aux
ps -ef
pgrep nginx
pgrep -f "python script.py"

kill -15 <PID>                       # SIGTERM: graceful, always try first
kill -9 <PID>                        # SIGKILL: force kill, no cleanup
pkill nginx
kill -l

command &
nohup command &
jobs
fg %1
bg %1
disown %1

nice -n 10 command
renice -n 5 -p <PID>

strace -p <PID>
strace -e trace=open,read,write -p <PID>
lsof -p <PID>
cat /proc/<PID>/status
cat /proc/<PID>/cmdline | tr "�" " "

Disk and Files

df -h
df -i                                # inode usage (can be full with free disk space!)
du -sh /var/log/*
du -sh /* | sort -rh | head -10

ls -lh
ls -lt
ls -la

lsblk
fdisk -l
mount | column -t

chmod 755 file                       # rwxr-xr-x
chmod 644 file                       # rw-r--r--
chmod +x script.sh
chown user:group file
chown -R appuser:appuser /app
umask 022                            # new files 644, dirs 755

tar -czf archive.tar.gz /path/
tar -xzf archive.tar.gz
tar -xzf archive.tar.gz -C /target/
tar -tzf archive.tar.gz
tar -czf - /dir | ssh user@host "tar -xzf - -C /target"

scp file.txt user@host:/path/
rsync -avz /src/ user@host:/dst/
rsync -avz --delete /src/ /dst/

System Monitoring

uname -r
uname -a
cat /etc/os-release
uptime
w

lscpu
free -h
cat /proc/meminfo
vmstat 1 5

# top / htop columns:
#   us% = user CPU   sy% = kernel/syscall   wa% = IO wait   id% = idle
#   RES = actual RAM used   VIRT = virtual address space (includes unmapped pages)
#   high wa%  -> disk bottleneck
#   high sy%  -> syscall heavy, context switches
#   high us%  -> CPU bound

iostat -x 1 5
iotop

Follow-up: %wa (iowait) vs %st (steal) — what's the difference?

Column Meaning Fix
%wa iowait CPU idle while waiting for I/O (disk/network) Faster disk, async I/O, read replicas
%st steal Hypervisor gave your CPU cycles to another VM — noisy neighbour Move to dedicated instance or different host
%sy syscall Time in kernel mode (context switches, system calls) Reduce goroutines, profile with perf trace

iowait = your problem. steal = hypervisor's fault. Both appear as "CPU not working" but have completely different fixes. Check %st first on cloud VMs.

Follow-up: What does load average 4.0 on a 2-core machine mean?

Load average = running average of processes in R (runnable) + D (uninterruptible I/O wait) state.

4.0 on 2 cores  → 2 processes competing for CPU + 2 queued waiting
2.0 on 2 cores  → fully saturated, no queue
1.0 on 2 cores  → 50% utilization

Rule: load ≤ CPU count = healthy. Load > 2× CPU count = investigate.

High load + high %id (CPU idle) = I/O wait problem (D-state processes), not CPU bound. Check iostat -x and iotop.

nproc            # logical CPU count
uptime           # load avg 1/5/15 min
cat /proc/loadavg  # load + running/total process count

journalctl -u nginx -f
journalctl -u nginx --since "1 hour ago"
journalctl -p err -b
dmesg | tail -50
dmesg | grep -i "oom"

systemctl status nginx
systemctl start nginx
systemctl stop nginx
systemctl restart nginx
systemctl enable nginx
systemctl disable nginx
systemctl list-units --failed

Cron

# minute  hour  day-of-month  month  day-of-week  command
# * = any   */N = every N   N-M = range   N,M = list

crontab -l
crontab -e
crontab -r                           # REMOVE ALL -- be careful

0 2 * * *        /opt/backup.sh
*/5 * * * *      /opt/healthcheck.sh
0 9-17 * * 1-5   /opt/report.sh
@reboot          /opt/startup.sh
0 0 1 * *        /opt/monthly.sh

grep CRON /var/log/syslog            # Debian/Ubuntu
grep CRON /var/log/cron              # RHEL/CentOS

Redirection and Pipes

command > file.txt        # stdout overwrite
command >> file.txt       # stdout append
command 2> errors.txt     # stderr only
command 2>&1              # stderr to stdout
command &> file.txt       # both stdout and stderr
command < input.txt       # file as stdin

command > /dev/null 2>&1  # suppress everything
command 2>/dev/null
command | tee output.log
command | tee output.log | grep ERROR

ps aux | grep nginx | grep -v grep | awk "{print \}"
history | awk "{print \}" | sort | uniq -c | sort -rn | head -10
cat /etc/passwd | cut -d: -f1 | sort

One-Liners

# Largest files on system
find / -type f -exec du -h {} + 2>/dev/null | sort -rh | head -20

# What process has a file open
lsof /var/log/app.log

# Watch command every 2 seconds
watch -n 2 "ss -tulpn | grep :8080"

# Check if port is open (no nmap)
(echo >/dev/tcp/host/443) 2>/dev/null && echo "OPEN" || echo "CLOSED"
nc -zv host 443

# Count 5xx errors per minute from nginx log
awk "\ ~ /^5/ {print \}" access.log | cut -d: -f2 | sort | uniq -c

# Lines in file2 not in file1
comm -23 <(sort file1.txt) <(sort file2.txt)

# Replace string in all yaml files recursively
find . -type f -name "*.yaml" -exec sed -i "s/v1/v2/g" {} +

# Monitor memory of a process over time
while true; do ps -p <PID> -o pid,rss,vsz,pcpu; sleep 5; done

# TCP connection count per state
ss -s | grep TCP
netstat -an | awk "/^tcp/ {print \}" | sort | uniq -c | sort -rn

# D-state (stuck on IO) processes
ps aux | awk "\ ~ /D/"

# High load investigation
uptime
top
iostat -x 1 5
iotop
ps aux | awk "\ ~ /D/"
mount | grep nfs
df -h

Finding Large Files

ls cannot filter by size — it lists and formats only. find is the correct tool for size filtering.

# Standard: files above 100MB
find / -type f -size +100M 2>/dev/null

# Best combo: find filters, ls -lhS formats and sorts by size, head limits output
find . -type f -size +100M | xargs ls -lhS | head -20

# du approach — top 20 largest files/dirs under current dir (fastest for "what's eating disk?")
du -ah . | sort -rh | head -20

# find with built-in ls output (permissions, size, path)
find / -type f -size +100M -ls 2>/dev/null | sort -k7 -rn | head -20

# ncdu — interactive disk usage explorer (install separately)
ncdu /

Can you use ls -lhtr | grep?

Technically yes, but it's fragile:

ls -lhtr | grep -E "[0-9]G"            # catches GB files
ls -lhtr | grep -E "[1-9][0-9]{2}M"    # tries to catch 100M+ — misses 99.9M, catches 100M correctly

Problem: ls -lh outputs human-readable sizes (99.9M, 2.3G) — no reliable numeric threshold via grep. 99.9M < 100M but could match. 2.3G > 100M but needs a separate grep for G. Use find for correctness.

find size units: c=bytes, k=KB, M=MB, G=GB. +100M = strictly greater than 100MB.

Follow-up: find -size +100M vs find -size +104857600c — what's the difference?

find treats M as mebibytes (MiB = 1,048,576 bytes), not megabytes (MB = 1,000,000 bytes). So -size +100M means strictly greater than 104,857,600 bytes — files of exactly 100.0 MiB are excluded.

Use c (bytes) when you need exact byte-level precision:

# Files strictly between 99MB and 100MB (using decimal MB)
find / -type f -size +99000000c -size -100000000c 2>/dev/null

# Files >= 100 MiB (equivalent to +100M but explicit)
find / -type f -size +104857600c 2>/dev/null

For most "find large files" purposes -size +100M is correct and readable. Use c only when the distinction between MiB and MB matters (e.g., verifying file upload size limits defined in megabytes).


Disk Management on EC2

Expanding root filesystem (EBS volume resize)

Root FS is fixed at launch size (e.g., 20GB). To expand without replacing the instance:

Step 1: Resize the EBS volume in AWS

# Via AWS CLI (no downtime — EBS resize is online)
aws ec2 modify-volume \
  --volume-id vol-0abc123 \
  --size 50              # new size in GB

# Wait until state = "optimizing" or "completed"
aws ec2 describe-volumes-modifications --volume-id vol-0abc123

Step 2: Extend the partition (inside the EC2 instance)

# Check current layout
lsblk
# NAME    MAJ:MIN  SIZE MOUNTPOINT
# xvda    202:0     50G            ← volume is now 50G
# └─xvda1 202:1     20G /          ← partition still 20G

# Grow the partition to fill the volume
sudo growpart /dev/xvda 1          # device + partition number

lsblk   # xvda1 should now show 50G

Step 3: Resize the filesystem

# ext4
sudo resize2fs /dev/xvda1

# xfs (Amazon Linux 2 default)
sudo xfs_growfs /

# Verify
df -h /
# /dev/xvda1   50G  18G  32G  36% /

No reboot needed. All three steps can be done on a live, running instance.

Follow-up: After growpart /dev/xvda 1, why do you need resize2fs separately?

growpart and resize2fs operate on different layers:

Physical disk (EBS volume)    ← aws ec2 modify-volume expands this
  └── Partition table          ← growpart extends the partition boundary
        └── Filesystem (ext4)  ← resize2fs expands the fs data structures
              └── Files

growpart /dev/xvda 1 rewrites the partition table entry so partition 1 now claims the additional sectors. But the ext4 filesystem inside that partition still has its superblock, block groups, and inode tables sized for the old boundary — it has no idea the partition got larger.

resize2fs /dev/xvda1 reads the new partition size, adds block groups to cover the extra space, and updates the superblock. Only after this step can you actually write files into the new space.

For XFS filesystems (Amazon Linux 2 default): use xfs_growfs / instead of resize2fs. XFS can only grow while mounted (unlike ext4 which can grow unmounted too).

# Check filesystem type before choosing the right tool
df -T /          # shows Type column: ext4 or xfs

# ext4
sudo resize2fs /dev/xvda1

# xfs (must be mounted)
sudo xfs_growfs /

Adding a new EBS volume (extra disk)

When root FS is fixed and you don't want to resize it — attach a second volume instead.

Step 1: Attach volume in AWS

aws ec2 attach-volume \
  --volume-id vol-0xyz789 \
  --instance-id i-0abc123 \
  --device /dev/sdf

Step 2: Partition and format (first time only)

# Confirm device appeared
lsblk                          # look for nvme1n1 or xvdf

# Create partition table (skip if using whole disk)
sudo fdisk /dev/nvme1n1
# press: n (new) → p (primary) → 1 → Enter → Enter → w (write)

# Format
sudo mkfs.ext4 /dev/nvme1n1p1  # or mkfs.xfs

Step 3: Mount

sudo mkdir -p /data

# Temporary mount
sudo mount /dev/nvme1n1p1 /data

# Permanent mount (survives reboot) — add to /etc/fstab
echo "UUID=$(blkid -s UUID -o value /dev/nvme1n1p1)  /data  ext4  defaults,nofail  0  2" \
  | sudo tee -a /etc/fstab

sudo mount -a    # test fstab without rebooting
df -h /data

Use nofail in fstab so the instance still boots if the volume is detached.


Detaching an EBS volume safely

Step 1: Unmount first (always)

# Check what's using the mount
lsof /data           # list open files on mount
fuser -m /data       # list PIDs using it

# Stop processes, then unmount
sudo umount /data

# If "device busy":
sudo fuser -km /data   # force-kill processes (careful in prod)
sudo umount /data

Step 2: Remove from fstab (or it'll fail to boot next time if volume is gone)

sudo vim /etc/fstab   # remove the /data line

Step 3: Detach via AWS

aws ec2 detach-volume --volume-id vol-0xyz789

# Verify
aws ec2 describe-volumes --volume-ids vol-0xyz789
# State: "available" means safely detached

Never detach from AWS console/CLI without unmounting first — can corrupt the filesystem.


Quick reference

Goal Command
Check disk usage df -h
Check what's using space du -ah / | sort -rh | head -20
List block devices lsblk
Find volume UUID blkid
Resize root after EBS expand growpart /dev/xvda 1 then resize2fs /dev/xvda1
Check filesystem type df -T or blkid