← DevOps← DevOps
DevOpsDevOps18 Th7, 2026Jul 18, 202620 phút đọc17 min read

Terminal & ScriptingTerminal & Scripting

Thuộc bộ kiến thức DevOps Roadmap.

Tổng quan

Terminal là giao diện làm việc chính của DevOps. GUI và web console vẫn tồn tại, nhưng server được quản trị qua SSH, job CI chạy các bước shell, container không có desktop, và mọi công cụ hạ tầng nghiêm túc đều phát hành CLI trước tiên. Sự thành thạo với shell — di chuyển, ghép pipeline, sửa file bằng vim, đọc trạng thái hệ thống bằng các lệnh monitoring — là khác biệt giữa xử lý sự cố trong năm phút và trong một giờ.

Shell scripting là tầng automation có trước và nằm dưới mọi thứ khác: bootstrap script, các bước trong CI pipeline, Docker entrypoint, cron job và installer script (curl … | bash) gần như đều là Bash. Ngay cả khi automation “chính thức” của bạn viết bằng Python hay Ansible, các one-liner shell vẫn là cách bạn khám phá, kiểm chứng và debug. Triết lý Unix — công cụ nhỏ làm tốt một việc, ghép nối bằng pipe — là bài học thiết kế áp dụng cho toàn bộ DevOps, không chỉ dòng lệnh.

Chủ đề này gồm ba cụm kỹ năng: viết Bash/Zsh script vững chắc, làm chủ bộ công cụ xử lý text và monitoring kinh điển (grep, sed, awk, ps, top, vmstat, sar), và dùng các lệnh mạng (curl, dig, ss, tcpdump) để debug kết nối từ nguyên lý cơ bản. Sợi chỉ xuyên suốt là tính ghép nối (composability): khi đã “ngấm” rằng mọi lệnh đều đọc stdin và ghi stdout, bạn có thể dựng vô số công cụ tạm thời chỉ bằng cách nối vài primitive lại với nhau.

Kiến thức nền tảng

Shell cơ bản: Bash và Zsh

Bash là shell scripting mặc định trên đa số hệ Linux; Zsh (mặc định trên macOS từ Catalina) gần như tương thích Bash khi dùng tương tác, nhưng script nên nhắm vào Bash (hoặc POSIX sh) để dễ mang đi. Lưu ý /bin/sh trên Debian/Ubuntu là dash, không phải Bash — nên script #!/bin/sh phải tránh “bashism” như array và [[ ]]. Các cơ chế cốt lõi cần “ngấm”:

Khung xương một script vững chắc:

#!/usr/bin/env bash
set -Eeuo pipefail            # -e thoát khi lỗi, -u lỗi khi biến chưa gán,
                             # -o pipefail bắt lỗi ở bất kỳ đâu trong pipe,
                             # -E để ERR trap kích hoạt cả bên trong hàm
IFS=$'\n\t'                  # word-splitting an toàn hơn (không tách theo space trần)
trap 'echo "error on line $LINENO (exit $?)" >&2' ERR
trap 'rm -rf "$tmp"' EXIT     # luôn dọn dẹp, kể cả khi lỗi

tmp="$(mktemp -d)"
readonly LOG_DIR="${LOG_DIR:-/var/log/myapp}"

usage() { echo "usage: $0 <env>" >&2; exit 2; }
[[ $# -eq 1 ]] || usage

log() { printf '%s %s\n' "$(date -Is)" "$*" >&2; }

main() {
  local env="$1"
  for f in "$LOG_DIR"/*.log; do
    [[ -e "$f" ]] || continue    # phòng khi glob không khớp gì
    gzip -k -- "$f"              # -- dừng phân tích option (tên file có dấu gạch)
  done
  log "done for $env"
}
main "$@"

Mỗi dòng ở đây đều xứng đáng có mặt: set -Eeuo pipefail biến lỗi âm thầm thành crash ồn ào, trap EXIT đảm bảo dọn dẹp, mktemp -d tránh đường dẫn temp đoán trước được, [[ -e "$f" ]] || continue xử lý trường hợp “glob không khớp gì”, và -- ngăn tên file bị hiểu thành option.

Text editor: sống sót với vim và hơn thế

Chắc chắn sẽ có ngày bạn SSH vào một server mà vim (hoặc vi) là editor duy nhất. Bộ kỹ năng sinh tồn tối thiểu:

PhímHành động
i / a / EscChèn trước / sau con trỏ / quay về normal mode
:w :q :wq :q!Lưu / thoát / lưu-thoát / thoát không lưu
dd yy p PXóa (cắt) dòng / copy dòng / dán sau / dán trước
x r<c>Xóa ký tự / thay ký tự bằng <c>
u / Ctrl-rUndo / redo
/text rồi n / NTìm xuôi, kết quả kế / trước
:nohXóa highlight tìm kiếm
gg / G / :42Đầu file / cuối file / tới dòng 42
0 / ^ / $Đầu dòng / ký tự đầu không trắng / cuối dòng
w / b / %Từ kế / từ trước / dấu ngoặc khớp
:%s/old/new/gThay thế trong cả file (gc để xác nhận từng chỗ)
v / V / Ctrl-vChọn theo ký tự / dòng / khối

Vượt qua mức sinh tồn, hãy học operator + motion/text object, vì chúng cộng hưởng theo cấp nhân chứ không phải cấp cộng: d (xóa), c (đổi), y (yank) kết hợp với iw (từ hiện tại), i" (trong nháy), ip (đoạn văn), it (trong tag). Nên ciw đổi một từ, di" xóa nội dung trong nháy, ci( đổi nội dung trong ngoặc, dap xóa một đoạn. Học . (lặp lại thay đổi gần nhất) và count (3dd). vimtutor (30 phút trên terminal) là điểm khởi đầu bài bản nhanh nhất; nano là phương án dự phòng tốt nếu nó có sẵn và bạn không sửa qua một đường truyền chập chờn.

Tư duy pipeline kiểu Unix

# Top 10 IP client theo số request trong access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10

Mỗi công cụ làm một việc; | ghép chúng thành một dây chuyền xử lý dữ liệu: awk trích field, sort gom các dòng giống nhau lại để uniq -c đếm được, sort -rn thứ hai sắp theo số lượng giảm dần, head lấy top 10. Học thật kỹ ~15 công cụ cốt lõi tốt hơn thuộc lòng hàng trăm flag — và cùng một hình dạng năm bước (trích → sắp → đếm → sắp → giới hạn) giải quyết một tỷ lệ lớn các câu hỏi phân tích log thực tế.

Khái niệm chính

Bộ công cụ xử lý text

Công cụCông dụngVí dụ
grepLọc dòng theo patterngrep -rEn 'ERROR|FATAL' /var/log/app/
sedSửa theo stream (thay thế, xóa, in khoảng)sed -i.bak 's/8080/9090/g' config.yaml
awkXử lý theo field, cả một ngôn ngữ miniawk -F: '$3 >= 1000 {print $1}' /etc/passwd
cutCắt cột theo delimiter hoặc bytecut -d, -f1,3 users.csv
sortSắp xếp dòng (số, theo key, unique)sort -t, -k2 -n data.csv
uniqKhử/đếm dòng trùng liền kềsort f | uniq -c
trChuyển đổi / xóa / nén ký tựtr 'a-z' 'A-Z' < f, tr -d '\r'
wcĐếm dòng / từ / bytewc -l access.log
head/tailN dòng đầu / cuối; tail -f để theo dõitail -f -n100 app.log
jqTruy vấn và biến đổi JSONkubectl get pods -o json | jq -r '.items[].metadata.name'
yqÝ tưởng tương tự cho YAMLyq '.spec.replicas' deploy.yaml
xargsDựng command line từ stdingit ls-files '*.sh' | xargs shellcheck
findTìm file theo thuộc tính, thao tác lên chúngfind /tmp -type f -mtime +7 -delete

Chọn giữa grep/sed/awk là nguồn bối rối thường gặp. Quy tắc: grep để tìm/lọc dòng, sed cho thay thế/xóa theo dòng đơn giản, awk khi cần field, số học, điều kiện, hoặc tích lũy. Nếu lệnh sed của bạn đã có hơn một mệnh đề ngăn bởi ; và một hold buffer, có lẽ nó nên là awk (hoặc một script thật).

awk đáng đầu tư thực sự — nó là một ngôn ngữ hoàn chỉnh cho dữ liệu dạng bảng:

# Cộng cột 5 (byte) gom theo cột 1 (IP), in tổng đã sắp xếp
awk '{bytes[$1] += $5} END {for (ip in bytes) print bytes[ip], ip}' access.log | sort -rn

jq đáng chú ý ngang bằng — gần như mọi API và CLI hiện đại đều trả về JSON, và parse nó bằng grep/sed rất mong manh:

curl -s https://api.github.com/repos/kubernetes/kubernetes |
  jq '{name: .name, stars: .stargazers_count, license: .license.spdx_id}'

# select + map: tên các pod không ở trạng thái Running
kubectl get pods -o json |
  jq -r '.items[] | select(.status.phase != "Running") | .metadata.name'

Regular expression: nền tảng dùng chung

grep, sed, awk và phần lớn công cụ dùng chung regular expression, nhưng ở các “phương ngữ” khác nhau: BRE (basic — grep, sed mặc định, nơi + ? { } ( ) phải escape bằng backslash), ERE (extended — grep -E, awk, sed -E, nơi các metacharacter đó dùng được mà không cần escape), và PCRE (grep -P, có lookaround và \d). Khi một pattern “không chạy”, phương ngữ thường là thủ phạm. Ưu tiên -E cho dễ đọc. Anchor (^ $), lớp ký tự ([[:digit:]], \w), quantifier (* + ? {n,m}), và group/backreference là vốn từ đáng thuộc lòng.

Giám sát process và hiệu năng

Công cụCho thấy gìKhi nào dùng
ps aux, ps -ef --forestDanh sách/cây process tại một thời điểmCó process nào, ai sở hữu, trạng thái ra sao
top / htopCPU/memory theo process, thời gian thựcCái nhìn đầu tiên khi có sự cố (htop thân thiện hơn)
vmstat 1CPU, memory, swap, run queue toàn hệ thốngMáy đang nghẽn CPU, memory hay I/O?
iostat -xz 1Throughput, latency (await), mức sử dụng theo đĩaNghi ngờ nghẽn ở disk
mpstat -P ALL 1Mức sử dụng chi tiết theo từng CPUTải trải đều hay dồn vào một core?
pidstat 1CPU/IO/memory theo từng process qua thời gianProcess nào gây ra triệu chứng toàn hệ thống?
sarSố liệu lịch sử (sysstat)“3 giờ sáng đã xảy ra chuyện gì?” (sau khi việc đã xong)
free -hTóm tắt memory / swapKiểm tra headroom bộ nhớ
uptimeLoad average (1 / 5 / 15 phút)Tín hiệu sức khỏe nhanh và chiều xu hướng

Cách đọc output vmstat 1 (các cột quan trọng):

Sắc thái của load average: trên Linux nó đếm cả task đang chạy lẫn task ở uninterruptible-sleep (D-state, thường là disk), nên load cao mà CPU thấp thường nghĩa là I/O chứ không phải tính toán. Chia load cho số core để có con số trên mỗi core.

Một quy trình triage sự cố 60 giây thực dụng, dựa theo checklist của Brendan Gregg:

uptime                     # 1. xu hướng load — đang tăng, giảm, hay ổn định?
dmesg -T | tail            # 2. OOM kill, lỗi đĩa, cảnh báo kernel
vmstat 1 5                 # 3. CPU vs memory vs swap vs iowait trong một cái nhìn
free -h                    # 4. headroom bộ nhớ (chú ý "available")
iostat -xz 1 3             # 5. có đĩa nào bão hòa không? (%util gần 100, await cao)
ss -s                      # 6. tóm tắt trạng thái socket/kết nối
top                        # 7. giờ soi vào process "tội đồ"

Các lệnh mạng

Công cụCông dụngVí dụ
pingReachability + latency khứ hồi (ICMP)ping -c4 10.0.0.5
curlGửi HTTP(S) request, test API, đo thời giancurl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://api.example.com/healthz
wgetTải file, mirrorwget -qO- https://example.com/script.sh
digTra cứu DNS (chi tiết authoritative)dig +short api.example.com, dig @8.8.8.8 example.com MX
host/nslookupTra cứu DNS nhanhhost api.example.com
ssTrạng thái socket (netstat hiện đại)ss -tlnp (đang listen), ss -tan state established
netstatLiệt kê socket/route kiểu cũnetstat -rn (routing) — nên ưu tiên ss/ip route
ipInterface, địa chỉ, route (thay ifconfig)ip addr, ip route get 8.8.8.8
traceroute / mtrĐường đi và loss/latency theo hopmtr --report example.com
tcpdumpBắt và phân tích gói tintcpdump -i eth0 -nn port 443 -w cap.pcap
nc (netcat)Thăm dò TCP/UDP thô, test portnc -zv db.internal 5432
openssl s_clientKiểm tra cert TLS và handshakeopenssl s_client -connect host:443 -servername host

Kỹ năng curl giá trị nhất là bảng phân rã thời gian bằng -w, cho bạn biết latency nằm ở đâu (DNS, connect, TLS, byte đầu tiên):

curl -sS -o /dev/null \
  -w 'dns:%{time_namelookup} conn:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
  https://api.example.com/

Một quy trình debug kết nối theo tầng — luôn đi lên từ dưới, vì mỗi tầng phụ thuộc tầng dưới nó:

dig +short svc.example.com          # 1. DNS có resolve không, và ra gì?
ping -c3 <ip>                        # 2. Host có tới được không? (ICMP có thể bị chặn — không kết luận được)
nc -zv <ip> 5432                     # 3. Port TCP có mở / đang listen không?
curl -v https://svc.example.com/     # 4. Tầng app + TLS có trả lời đúng không?
sudo tcpdump -nn host <ip> and port 5432   # 5. Thực sự có gì trên đường truyền? (phương án cuối, sự thật gốc)

Diễn giải kết quả: DNS lỗi → resolver/record; ping được nhưng nc bị refused → service chết hoặc firewall chặn port đó; nc được nhưng curl treo → vấn đề TLS hoặc tầng ứng dụng; mọi thứ “chạy” nhưng app sai → dùng tcpdump để nhìn byte thật.

Các mẫu điều khiển Bash đáng thuộc lòng

# Retry với exponential backoff — pattern bạn sẽ dùng lại mãi mãi
for i in {1..5}; do
  curl -fsS https://example.com/healthz && break
  echo "attempt $i failed; retrying in $((2 ** i))s" >&2
  sleep $((2 ** i))
done

# Duyệt kết quả find an toàn (chịu được khoảng trắng/xuống dòng trong tên)
find /data -name '*.csv' -print0 | while IFS= read -r -d '' f; do
  process "$f"
done

# Đọc file từng dòng mà không mất dòng cuối không có ký tự xuống dòng
while IFS= read -r line || [[ -n "$line" ]]; do
  echo "got: $line"
done < input.txt

# Rẽ nhánh bằng case (hình dạng của hầu hết script điều khiển service)
case "${1:-}" in
  start)   systemctl start myapp ;;
  stop)    systemctl stop myapp ;;
  restart) systemctl restart myapp ;;
  *)       echo "usage: $0 {start|stop|restart}" >&2; exit 2 ;;
esac

# Array và associative array (Bash 4+)
declare -A limits=([cpu]=2 [mem]=512Mi)
for k in "${!limits[@]}"; do echo "$k -> ${limits[$k]}"; done

Dùng shell tương tác hiệu quả

Shell tương tác thưởng công cho một nhóm thói quen nhỏ: Ctrl-r để tìm ngược trong lịch sử, !! (lệnh gần nhất), !$ (tham số cuối), Ctrl-a/Ctrl-e (đầu/cuối dòng), Ctrl-w/Ctrl-u (xóa từ/dòng), cd - (thư mục trước), và Alt-. (chèn tham số cuối). Session bền quan trọng khi làm qua SSH: tmux (hoặc screen) giữ công việc sống qua các lần mất kết nối và cho bạn chia pane để monitor trong lúc làm việc. Hãy học cấu hình history (HISTSIZE, HISTCONTROL=ignoredups:erasedups), alias, và một prompt hiển thị thư mục hiện tại, git branch, và Kubernetes context để bạn không bao giờ chạy đúng lệnh ở sai nơi.

Best Practices

  1. Mọi Bash script bắt đầu bằng set -Eeuo pipefail. Lỗi âm thầm giữa script là nguyên nhân của những lần deploy dở dang; hãy làm cho lỗi ồn ào và chết ngay. Thêm IFS=$'\n\t' để chế ngự word-splitting.
  2. Đặt mọi biến trong nháy kép ("$var", "${arr[@]}"). Biến không quote bị word-splitting và globbing, vỡ khi gặp space, tab và * — lớp bug shell kinh điển và phổ biến nhất.
  3. Chạy ShellCheck cho mọi script (trong editor lẫn trong CI). Nó bắt lỗi quoting, portability và logic ngay lập tức và dạy bạn luật khi làm.
  4. Ưu tiên $(...) hơn backtick và [[ ... ]] hơn [ ... ] trong Bash — command substitution lồng nhau gọn gàng, và [[ ]] tránh cạm bẫy word-splitting cùng hỗ trợ khớp pattern/regex.
  5. Giữ script có tính idempotent. Dùng mkdir -p, ln -sf, rm -f, và kiểm tra trạng thái tường minh trước khi thay đổi; chạy lại sau một thất bại nửa chừng phải an toàn và cho cùng kết quả cuối.
  6. Rào các lệnh phá hủy. Hỗ trợ --dry-run, echo lệnh ra trước khi chạy, và đừng bao giờ để rm -rf "$dir/" chạy với biến có thể rỗng — dùng ${dir:?dir is required} để giá trị chưa gán làm dừng thay vì xóa /.
  7. Chuyển sang Python khi vượt ~100–200 dòng hoặc khi cần cấu trúc dữ liệu, xử lý lỗi thật, số học với số thực, hay test. Shell là keo dán và điều phối, không phải ngôn ngữ viết ứng dụng.
  8. Dùng mktemp cho file tạm và trap ... EXIT để dọn dẹp. Đường dẫn temp đoán trước được (/tmp/myapp.tmp) vừa là bug race-condition vừa là lỗ hổng bảo mật (tấn công symlink); dọn dẹp được đảm bảo ngăn rò rỉ đĩa.
  9. Học jq/yq cho tử tế. Parse JSON/YAML bằng grep/sed/cut rất mong manh và vỡ khi định dạng thay đổi; một biểu thức jq thay thế cả chuỗi pipeline dễ vỡ và sống sót qua việc reformat.
  10. Làm chủ “năm bước pipeline” (extract | sort | uniq -c | sort -rn | head). Nó trả lời phần lớn câu hỏi “top N … là gì?” trên log mà không cần script gì cả.
  11. Hiểu các cột của vmstat/iostat/top, không chỉ bản thân lệnh. Biết wa cao nghĩa là iowait và st cao nghĩa là hàng xóm ồn ào là thứ biến con số thô thành một chẩn đoán.
  12. Debug kết nối theo tầng, từ dưới lên (DNS → host → port → app → gói tin). Nhảy thẳng vào tcpdump là phí thời gian khi dig đã có thể chỉ ra vấn đề trong một dòng.
  13. Thu thập bằng chứng trước khi kết luận. Khi có sự cố, chuyển output vào file (vmstat 1 60 > vmstat.txt, tcpdump -w cap.pcap, dmesg -T > dmesg.txt) để trạng thái thoáng qua còn lại để phân tích sau khi đã giảm nhẹ.
  14. Ưu tiên đo bằng curl -w hơn là đoán. Khi thứ gì đó “chậm”, bảng phân rã DNS/connect/TLS/TTFB cho biết cần sửa tầng nào thay vì suy đoán.
  15. Dùng session bền (tmux) cho mọi thứ chạy lâu qua SSH. Một lần rớt kết nối không bao giờ được phép giết một migration hay một tail -f bạn đang theo dõi.
  16. Comment cái vì sao, không phải cái làm gì, và luôn có hàm usage(). Sáu tháng sau, ý đồ đằng sau một pipeline thông minh và ý nghĩa của từng flag chỉ là tài liệu nếu bạn đã viết chúng ra.

Tài liệu tham khảo

Part of the DevOps Roadmap knowledge base.

Overview

The terminal is the primary interface of DevOps work. GUIs and web consoles exist, but servers are administered over SSH, CI jobs run shell steps, containers have no desktop, and every serious infrastructure tool ships as a CLI first. Fluency in the shell — navigating, composing pipelines, editing files with vim, and reading system state with monitoring commands — is the difference between resolving an incident in five minutes and in an hour.

Shell scripting is the automation layer that predates and underlies everything else: bootstrap scripts, CI pipeline steps, Docker entrypoints, cron jobs, and installer scripts (curl … | bash) are almost all Bash. Even when your “real” automation lives in Python or Ansible, shell one-liners are how you explore, verify, and debug. The Unix philosophy — small tools that do one thing well, composed with pipes — is a design lesson that applies to all of DevOps, not just the command line.

This topic covers three skill clusters: writing robust Bash/Zsh scripts, mastering the classic text-processing and monitoring toolset (grep, sed, awk, ps, top, vmstat, sar), and using networking commands (curl, dig, ss, tcpdump) to debug connectivity from first principles. The through-line is composability: once you internalize that every command reads stdin and writes stdout, you can build an unbounded number of ad-hoc tools by connecting a handful of primitives.

Fundamentals

Shell basics: Bash and Zsh

Bash is the default scripting shell on most Linux systems; Zsh (default on macOS since Catalina) is largely Bash-compatible interactively but scripts should target Bash (or POSIX sh) for portability. Note that /bin/sh on Debian/Ubuntu is dash, not Bash — so #!/bin/sh scripts must avoid “bashisms” like arrays and [[ ]]. Core mechanics to internalize:

A robust script skeleton:

#!/usr/bin/env bash
set -Eeuo pipefail            # -e exit on error, -u error on unset var,
                             # -o pipefail catch failures anywhere in a pipe,
                             # -E make ERR trap fire inside functions
IFS=$'\n\t'                  # safer word-splitting (no bare spaces)
trap 'echo "error on line $LINENO (exit $?)" >&2' ERR
trap 'rm -rf "$tmp"' EXIT     # always clean up, even on error

tmp="$(mktemp -d)"
readonly LOG_DIR="${LOG_DIR:-/var/log/myapp}"

usage() { echo "usage: $0 <env>" >&2; exit 2; }
[[ $# -eq 1 ]] || usage

log() { printf '%s %s\n' "$(date -Is)" "$*" >&2; }

main() {
  local env="$1"
  for f in "$LOG_DIR"/*.log; do
    [[ -e "$f" ]] || continue    # guard against no-match glob
    gzip -k -- "$f"              # -- stops option parsing (filenames with dashes)
  done
  log "done for $env"
}
main "$@"

Every line here earns its place: set -Eeuo pipefail turns silent failure into a loud crash, the EXIT trap guarantees cleanup, mktemp -d avoids predictable temp paths, [[ -e "$f" ]] || continue handles the “glob matched nothing” case, and -- prevents filenames from being parsed as options.

Text editors: vim survival and beyond

You will land on a server where vim (or vi) is the only editor. Minimum survival kit:

KeysAction
i / a / EscInsert before / after cursor / back to normal mode
:w :q :wq :q!Save / quit / save-quit / quit without saving
dd yy p PDelete (cut) line / yank (copy) line / paste after / before
x r<c>Delete char / replace char with <c>
u / Ctrl-rUndo / redo
/text then n / NSearch forward, next / previous match
:nohClear search highlight
gg / G / :42Top / bottom / go to line 42
0 / ^ / $Start of line / first non-blank / end of line
w / b / %Next word / previous word / matching bracket
:%s/old/new/gReplace in whole file (gc to confirm each)
v / V / Ctrl-vVisual char / line / block selection

Beyond survival, learn operators + motions/text objects, because they compose multiplicatively rather than additively: d (delete), c (change), y (yank) combine with iw (inner word), i" (inside quotes), ip (inner paragraph), it (inside tag). So ciw changes a word, di" deletes inside quotes, ci( changes inside parentheses, dap deletes a paragraph. Learn . (repeat last change) and counts (3dd). vimtutor (30 minutes at the terminal) is the fastest structured start; nano is a fine fallback if it is installed and you are not editing over a flaky link.

The Unix pipeline mindset

# Top 10 client IPs by request count in an access log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10

Each tool does one job; | composes them into a data-processing assembly line: awk extracts the field, sort groups identical lines together so uniq -c can count them, the second sort -rn orders by count descending, head takes the top 10. Learning ~15 core tools deeply beats memorizing hundreds of flags — and the same five-stage shape (extract → sort → count → sort → limit) solves a huge fraction of real log-analysis questions.

Key Concepts

Text manipulation toolbox

ToolPurposeExample
grepFilter lines by patterngrep -rEn 'ERROR|FATAL' /var/log/app/
sedStream editing (substitute, delete, print ranges)sed -i.bak 's/8080/9090/g' config.yaml
awkField-based processing, a whole mini-languageawk -F: '$3 >= 1000 {print $1}' /etc/passwd
cutExtract columns by delimiter or bytecut -d, -f1,3 users.csv
sortOrder lines (numeric, by key, unique)sort -t, -k2 -n data.csv
uniqDeduplicate / count adjacent duplicatessort f | uniq -c
trTranslate / delete / squeeze characterstr 'a-z' 'A-Z' < f, tr -d '\r'
wcCount lines / words / byteswc -l access.log
head/tailFirst / last N lines; tail -f to followtail -f -n100 app.log
jqQuery and transform JSONkubectl get pods -o json | jq -r '.items[].metadata.name'
yqSame idea for YAMLyq '.spec.replicas' deploy.yaml
xargsBuild command lines from stdingit ls-files '*.sh' | xargs shellcheck
findLocate files by attribute, act on themfind /tmp -type f -mtime +7 -delete

Choosing among grep/sed/awk is a common source of confusion. A rule of thumb: grep to find/filter lines, sed for simple line-oriented substitution/deletion, awk when you need fields, arithmetic, conditionals, or accumulation. If your sed command has grown more than one ;-separated clause and a hold buffer, it should probably be awk (or a real script).

awk deserves real investment — it is a complete language for tabular data:

# Sum column 5 (bytes) grouped by column 1 (IP), print totals sorted
awk '{bytes[$1] += $5} END {for (ip in bytes) print bytes[ip], ip}' access.log | sort -rn

jq deserves equal attention — nearly every modern API and CLI emits JSON, and parsing it with grep/sed is fragile:

curl -s https://api.github.com/repos/kubernetes/kubernetes |
  jq '{name: .name, stars: .stargazers_count, license: .license.spdx_id}'

# select + map: names of pods that are not Running
kubectl get pods -o json |
  jq -r '.items[] | select(.status.phase != "Running") | .metadata.name'

Regular expressions: the shared substrate

grep, sed, awk, and most tools share regular expressions, but in dialects: BRE (basic — grep, sed by default, where + ? { } ( ) must be backslash-escaped), ERE (extended — grep -E, awk, sed -E, where those metacharacters work unescaped), and PCRE (grep -P, with lookaround and \d). When a pattern “doesn’t work,” the dialect is the usual culprit. Prefer -E for readability. Anchors (^ $), classes ([[:digit:]], \w), quantifiers (* + ? {n,m}), and groups/backreferences are the vocabulary worth memorizing.

Process and performance monitoring

ToolWhat it showsWhen to reach for it
ps aux, ps -ef --forestPoint-in-time process list / treeWhich processes exist, who owns them, their state
top / htopLive CPU/memory per processFirst look during an incident (htop is friendlier)
vmstat 1System-wide CPU, memory, swap, run queueIs the box CPU-, memory-, or I/O-bound?
iostat -xz 1Per-disk throughput, latency (await), utilizationSuspected disk bottleneck
mpstat -P ALL 1Per-CPU utilization breakdownIs load spread evenly or pinned to one core?
pidstat 1Per-process CPU/IO/memory over timeWhich process is causing the system-wide symptom?
sarHistorical metrics (sysstat)“What happened at 3 AM?” (after the fact)
free -hMemory / swap summaryMemory headroom check
uptimeLoad averages (1 / 5 / 15 min)Quick health signal and trend direction

Reading vmstat 1 output (the columns that matter):

Load average nuance: it counts both running and uninterruptible-sleep (D-state, usually disk) tasks on Linux, so a high load with low CPU usually means I/O, not compute. Divide load by core count for a per-core figure.

A practical 60-second incident triage, based on Brendan Gregg’s checklist:

uptime                     # 1. load trend — rising, falling, steady?
dmesg -T | tail            # 2. OOM kills, disk errors, kernel warnings
vmstat 1 5                 # 3. CPU vs memory vs swap vs iowait at a glance
free -h                    # 4. memory headroom (watch "available")
iostat -xz 1 3             # 5. is a disk saturated? (%util near 100, high await)
ss -s                      # 6. socket/connection state summary
top                        # 7. now zoom into the guilty process

Networking commands

ToolPurposeExample
pingReachability + round-trip latency (ICMP)ping -c4 10.0.0.5
curlHTTP(S) requests, API testing, timingcurl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://api.example.com/healthz
wgetDownload files, mirrorwget -qO- https://example.com/script.sh
digDNS lookups (authoritative detail)dig +short api.example.com, dig @8.8.8.8 example.com MX
host/nslookupQuick DNS lookupshost api.example.com
ssSocket state (modern netstat)ss -tlnp (listeners), ss -tan state established
netstatLegacy socket/route listingnetstat -rn (routes) — prefer ss/ip route
ipInterfaces, addresses, routes (replaces ifconfig)ip addr, ip route get 8.8.8.8
traceroute / mtrPath and per-hop loss/latencymtr --report example.com
tcpdumpPacket capture and inspectiontcpdump -i eth0 -nn port 443 -w cap.pcap
nc (netcat)Raw TCP/UDP probe, port testnc -zv db.internal 5432
openssl s_clientInspect TLS certs and handshakesopenssl s_client -connect host:443 -servername host

The most valuable curl skill is the -w timing breakdown, which tells you where latency lives (DNS, connect, TLS, first byte):

curl -sS -o /dev/null \
  -w 'dns:%{time_namelookup} conn:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
  https://api.example.com/

A layered connectivity debug flow — always work up the stack, since each layer depends on the one below:

dig +short svc.example.com          # 1. Does DNS resolve, and to what?
ping -c3 <ip>                        # 2. Is the host reachable? (ICMP may be blocked — not conclusive)
nc -zv <ip> 5432                     # 3. Is the TCP port open / listening?
curl -v https://svc.example.com/     # 4. Does the app + TLS layer answer correctly?
sudo tcpdump -nn host <ip> and port 5432   # 5. What is actually on the wire? (last resort, ground truth)

Interpreting the outcome: DNS fails → resolver/records; ping works but nc refused → service down or firewall on that port; nc works but curl hangs → TLS or application-layer problem; everything “works” but the app misbehaves → tcpdump to see the real bytes.

Bash control flow worth memorizing

# Retry with exponential backoff — a pattern you will reuse forever
for i in {1..5}; do
  curl -fsS https://example.com/healthz && break
  echo "attempt $i failed; retrying in $((2 ** i))s" >&2
  sleep $((2 ** i))
done

# Safe iteration over find results (handles spaces/newlines in names)
find /data -name '*.csv' -print0 | while IFS= read -r -d '' f; do
  process "$f"
done

# Read a file line by line without losing the last unterminated line
while IFS= read -r line || [[ -n "$line" ]]; do
  echo "got: $line"
done < input.txt

# Case dispatch (the shape of most service-control scripts)
case "${1:-}" in
  start)   systemctl start myapp ;;
  stop)    systemctl stop myapp ;;
  restart) systemctl restart myapp ;;
  *)       echo "usage: $0 {start|stop|restart}" >&2; exit 2 ;;
esac

# Arrays and associative arrays (Bash 4+)
declare -A limits=([cpu]=2 [mem]=512Mi)
for k in "${!limits[@]}"; do echo "$k -> ${limits[$k]}"; done

Interactive shell efficiency

The interactive shell rewards a small set of habits: Ctrl-r for reverse history search, !! (last command), !$ (last argument), Ctrl-a/Ctrl-e (line start/end), Ctrl-w/Ctrl-u (delete word/line), cd - (previous directory), and Alt-. (insert last argument). Persistent sessions matter over SSH: tmux (or screen) keeps your work alive across disconnects and lets you split panes for monitoring while you work. Learn to configure history (HISTSIZE, HISTCONTROL=ignoredups:erasedups), aliases, and a prompt that shows the current directory, git branch, and Kubernetes context so you never run the right command in the wrong place.

Best Practices

  1. Start every Bash script with set -Eeuo pipefail. Silent failure mid-script is how half-finished deployments happen; make errors loud and fatal. Add IFS=$'\n\t' to tame word-splitting.
  2. Quote every variable expansion ("$var", "${arr[@]}"). Unquoted expansions undergo word-splitting and globbing and break on spaces, tabs, and * — the classic and most frequent shell bug class.
  3. Run ShellCheck on all scripts (in your editor and in CI). It statically catches quoting, portability, and logic errors instantly and teaches you the rules as it goes.
  4. Prefer $(...) over backticks and [[ ... ]] over [ ... ] in Bash — command substitution nests cleanly, and [[ ]] avoids word-splitting pitfalls and supports pattern/regex matching.
  5. Keep scripts idempotent. Use mkdir -p, ln -sf, rm -f, and explicit state checks before mutations; re-running after a partial failure must be safe and produce the same end state.
  6. Guard destructive commands. Support --dry-run, echo the command before running it, and never let rm -rf "$dir/" run with a possibly-empty variable — use ${dir:?dir is required} so an unset value aborts instead of deleting /.
  7. Graduate to Python past ~100–200 lines or when you need data structures, real error handling, arithmetic on floats, or tests. Shell is glue and orchestration, not an application language.
  8. Use mktemp for temp files and trap ... EXIT for cleanup. Predictable temp paths (/tmp/myapp.tmp) are both a race-condition bug and a security issue (symlink attacks); guaranteed cleanup prevents disk leaks.
  9. Learn jq/yq properly. Parsing JSON/YAML with grep/sed/cut is fragile and breaks on formatting changes; one jq expression replaces a brittle pipeline and survives reformatting.
  10. Master the pipeline five-step (extract | sort | uniq -c | sort -rn | head). It answers a large fraction of “what’s the top N …?” questions over logs without any scripting at all.
  11. Understand vmstat/iostat/top columns, not just the commands. Knowing that high wa means iowait and high st means a noisy neighbor is what turns raw numbers into a diagnosis.
  12. Debug connectivity in layers, bottom-up (DNS → host → port → app → packets). Jumping straight to tcpdump wastes time when dig would have shown the problem in one line.
  13. Capture evidence before you conclude. During incidents, redirect outputs to files (vmstat 1 60 > vmstat.txt, tcpdump -w cap.pcap, dmesg -T > dmesg.txt) so the transient state survives for analysis after mitigation.
  14. Prefer curl -w timing over guessing. When something is “slow,” the DNS/connect/TLS/TTFB breakdown tells you which layer to fix instead of speculating.
  15. Use persistent sessions (tmux) for anything long-running over SSH. A dropped connection should never kill a migration or a tail -f you are watching.
  16. Comment the why, not the what, and include a usage() function. Six months later, the intent behind a clever pipeline and the meaning of each flag are documented only if you wrote them down.

References