Bash & Shell Scripting · Part 10 — Real-World Automation & Best Practices
Capstone: getopts, usage(), script structure, logging, env config, and a production-grade backup script with traps and validation — plus best practices and when NOT to use bash. Bilingual exercises.
This is Part 10 of a 10-part series — the capstone that ties everything together into scripts you would actually ship {Đây là Phần 10 của series 10 bài — bài kết gom mọi thứ thành script bạn thực sự đưa vào production}. Parts 1–9 gave you the language: variables, conditionals, loops, functions, I/O, text tools, and error handling {Phần 1–9 đã cho bạn ngôn ngữ: biến, điều kiện, vòng lặp, function, I/O, công cụ text, và xử lý lỗi}. Now we assemble those pieces into a real automation script with argument parsing, logging, configuration, validation, and cleanup {Giờ ta lắp các mảnh thành script automation thật có parse tham số, logging, cấu hình, kiểm tra, và dọn dẹp}.
By the end of this part you will have a mental template — and a full working example — for any script you write at work {Hết phần này bạn sẽ có một template trong đầu — và một ví dụ chạy được — cho mọi script bạn viết ở công việc}.
The production script pipeline {Pipeline script production}
A maintainable automation script follows the same four beats every time {Một script automation dễ bảo trì luôn theo bốn nhịp giống nhau}:
- Parse args — understand what the user asked for {Parse tham số — hiểu người dùng muốn gì}.
- Validate — check inputs, dependencies, and permissions before touching data {Validate — kiểm tra input, dependency, và quyền trước khi đụng dữ liệu}.
- Do work — the actual task (backup, deploy, sync, etc.) {Làm việc — tác vụ thật (backup, deploy, sync, v.v.)}.
- Log + exit — report what happened and return a meaningful status code {Log + exit — báo cáo chuyện gì xảy ra và trả status code có ý nghĩa}.
Everything else — functions, constants, env defaults — exists to make those four steps readable and safe {Mọi thứ khác — function, hằng số, default từ env — tồn tại để bốn bước đó dễ đọc và an toàn}.
Parsing options with getopts {Parse tùy chọn với getopts}
getopts is bash’s built-in parser for short flags like -v, -h, -o file.txt {getopts là parser tích hợp của bash cho flag ngắn như -v, -h, -o file.txt}. It is POSIX-portable within bash and far less error-prone than hand-rolling a while/case loop {Nó portable trong bash và ít lỗi hơn nhiều so với tự viết vòng while/case}.
The option string {Chuỗi tùy chọn}
The second argument to getopts is an option string {Tham số thứ hai của getopts là chuỗi tùy chọn}:
- A letter alone (
v) means the flag takes no argument:-v{Chữ cái đơn (v) nghĩa là flag không nhận tham số:-v}. - A letter followed by
:(o:) means the flag requires an argument:-o file.txt{Chữ cái theo sau bởi:(o:) nghĩa là flag bắt buộc có tham số:-o file.txt}. - A leading
:silencesgetopts’ own error messages so you can print your ownusage{:ở đầu tắt thông báo lỗi mặc định củagetoptsđể bạn inusageriêng}.
#!/usr/bin/env bash
set -euo pipefail
VERBOSE=0
OUTPUT=""
usage() {
cat >&2 <<'EOF'
Usage: mytool [-h] [-v] [-o FILE] [ARGS...]
-h show this help
-v verbose output
-o write output to FILE (required for some modes)
EOF
}
while getopts ":hvo:" opt; do
case "$opt" in
h) usage; exit 0 ;;
v) VERBOSE=1 ;;
o) OUTPUT="$OPTARG" ;;
\?) echo "Unknown option: -$OPTARG" >&2; usage; exit 2 ;;
:) echo "Option -$OPTARG requires an argument." >&2; usage; exit 2 ;;
esac
done
shift $((OPTIND - 1)) # remaining positional args: "$@"
echo "verbose=$VERBOSE output=$OUTPUT remaining=$*"
Key variables getopts sets for you {Biến getopts đặt sẵn cho bạn}:
| Variable {Biến} | Meaning {Ý nghĩa} |
|---|---|
$opt | The option letter just parsed {Chữ cái vừa parse} |
$OPTARG | The argument for flags that take one (-o) {Tham số cho flag có đối số (-o)} |
$OPTIND | Index of the next positional arg; use shift $((OPTIND - 1)) after the loop {Chỉ số arg vị trí tiếp theo; dùng shift $((OPTIND - 1)) sau vòng lặp} |
Limitation {Giới hạn}:
getoptshandles short flags only (-abcis three flags;-o fileworks). For long options (--verbose,--output=file) usegetoptfrom GNU coreutils or a hand-written parser — or switch to a language with a real CLI library {getoptschỉ xử lý flag ngắn (-abclà ba flag;-o filehoạt động). Với option dài (--verbose,--output=file) dùnggetoptcủa GNU coreutils, parser tự viết — hoặc chuyển sang ngôn ngữ có thư viện CLI thật}.
Building usage() / help {Xây usage() / help}
Every script that accepts flags should ship a usage function printed to stderr {Mọi script nhận flag nên có function usage in ra stderr}. That way normal output on stdout stays clean for piping {Như vậy output thường trên stdout vẫn sạch để pipe}.
Patterns that work well {Pattern hiệu quả}:
- Use a heredoc (
<<'EOF') so you do not accidentally expand$variablesin the help text {Dùng heredoc (<<'EOF') để không vô tình expand$variablestrong help}. - Call
usagefrom-h, from unknown-flag handlers, and from validation failures {Gọiusagetừ-h, từ handler flag lạ, và từ lỗi validate}. - Exit
0for deliberate help (-h), exit2(or your project’s convention) for misuse {Thoát0khi help cố ý (-h), thoát2(hoặc quy ước dự án) khi dùng sai}.
usage() {
cat >&2 <<EOF
$(basename "$0") — backup a directory to a tarball
Usage: $(basename "$0") [-hvn] [-o DIR] SOURCE
-h show help
-v verbose logging
-n dry-run (show commands, do not execute)
-o output directory (default: \$BACKUP_DIR or ./backups)
Environment:
BACKUP_DIR default output directory
LOG_LEVEL info | warn | error (default: info)
Exit codes:
0 success
1 runtime failure (copy, tar, etc.)
2 usage / validation error
EOF
}
Note \$BACKUP_DIR in the heredoc — escaped so the help shows the literal name, not the variable’s value {Chú ý \$BACKUP_DIR trong heredoc — escape để help hiện tên literal, không phải giá trị biến}.
Structuring a real script {Cấu trúc script thật}
Scattered top-level commands become unmaintainable after ~50 lines {Lệnh rải rác ở top-level sẽ khó bảo trì sau ~50 dòng}. Production scripts follow a skeleton {Script production theo một khung xương}:
#!/usr/bin/env bash
# strict mode — Part 9 covered each flag
set -euo pipefail
# ── constants ──────────────────────────────────────
readonly SCRIPT_NAME="$(basename "$0")"
readonly DEFAULT_BACKUP_DIR="${BACKUP_DIR:-./backups}"
# ── globals set by getopts / env ───────────────────
VERBOSE=0
DRY_RUN=0
# ── logging ────────────────────────────────────────
log() { ... }
info() { ... }
warn() { ... }
error() { ... }
# ── helpers ────────────────────────────────────────
usage() { ... }
require_cmd() { ... }
cleanup() { ... }
# ── core logic ─────────────────────────────────────
validate_inputs() { ... }
run_backup() { ... }
# ── entry point ────────────────────────────────────
main() {
parse_args "$@"
validate_inputs
run_backup
}
main "$@"
Why main "$@" at the bottom? {Vì sao main "$@" ở cuối?}
- The entire script body runs inside
main, so a prematurereturndoes not accidentally skip cleanup setup {Toàn bộ script chạy trongmain, nênreturnsớm không vô tình bỏ qua setup cleanup}. - You can source the file in tests and call individual functions without executing
main{Bạn có thể source file trong test và gọi từng function mà không chạymain}. "$@"preserves argument quoting — never use$*for forwarding user input {"$@"giữ quoting tham số — không bao giờ dùng$*để chuyển input người dùng}.
Logging helpers — stderr with timestamps {Helper logging — stderr kèm timestamp}
Scripts should log diagnostics to stderr and reserve stdout for data the user might pipe {Script nên log chẩn đoán ra stderr và để stdout cho dữ liệu người dùng có thể pipe}. A tiny logging layer keeps messages consistent {Một lớp logging nhỏ giữ thông điệp nhất quán}:
_log_level="${LOG_LEVEL:-info}" # info | warn | error
log() {
# usage: log LEVEL "message"
local level="$1"
shift
printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$level" "$*" >&2
}
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
# Optional: suppress info when quiet
info() {
[[ "$VERBOSE" -eq 1 || "$_log_level" == "info" ]] && log INFO "$@" || true
}
Redirecting to >&2 is non-negotiable for tools that emit machine-readable stdout {Redirect >&2 là bắt buộc với tool xuất stdout dạng máy đọc được}. Timestamps matter when you grep logs hours later in CI {Timestamp quan trọng khi bạn grep log vài giờ sau trong CI}.
Configuration via environment variables {Cấu hình qua biến môi trường}
Hard-coding paths and secrets in scripts is a maintenance and security trap {Hard-code path và secret trong script là bẫy bảo trì và bảo mật}. Use environment variables with defaults {Dùng biến môi trường kèm default}:
# ${VAR:-default} — use default if unset or empty
# ${VAR:-} — empty default (explicit "no value")
readonly BACKUP_DIR="${BACKUP_DIR:-./backups}"
readonly RETENTION_DAYS="${RETENTION_DAYS:-7}"
readonly COMPRESS="${COMPRESS:-gzip}" # gzip | none
Document every env var in usage() and in a comment block at the top of the file {Ghi mọi biến env trong usage() và trong block comment đầu file}. For secrets (API tokens, passwords), never put them in the script — read from env or a secrets manager and fail fast if missing {Với secret (token API, mật khẩu), không bao giờ để trong script — đọc từ env hoặc secrets manager và fail nhanh nếu thiếu}.
if [[ -z "${DEPLOY_TOKEN:-}" ]]; then
error "DEPLOY_TOKEN is not set"
exit 2
fi
Complete example: backup-dir.sh {Ví dụ đầy đủ: backup-dir.sh}
Here is a production-grade backup script that uses every technique from this series {Đây là script backup mức production dùng mọi kỹ thuật trong series}. Read it top to bottom before copying pieces into your own projects {Đọc từ trên xuống trước khi copy từng phần vào dự án của bạn}.
#!/usr/bin/env bash
# backup-dir.sh — tarball a source directory with validation, logging, and cleanup
#
# Environment:
# BACKUP_DIR output directory (default: ./backups)
# RETENTION_DAYS delete archives older than N days (default: 7, 0 = skip)
# LOG_LEVEL info | warn | error
#
# Exit codes:
# 0 success
# 1 runtime error (tar, disk, etc.)
# 2 usage / validation error
set -euo pipefail
# ── constants ──────────────────────────────────────
readonly SCRIPT_NAME="$(basename "$0")"
readonly DEFAULT_BACKUP_DIR="${BACKUP_DIR:-./backups}"
readonly RETENTION_DAYS="${RETENTION_DAYS:-7}"
# ── runtime state ──────────────────────────────────
VERBOSE=0
DRY_RUN=0
OUTPUT_DIR="$DEFAULT_BACKUP_DIR"
SOURCE=""
TEMP_LIST=""
# ── logging ────────────────────────────────────────
log() {
local level="$1"
shift
printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S')" "$level" "$*" >&2
}
info() { [[ "$VERBOSE" -eq 1 ]] && log INFO "$@" || log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
# ── usage ──────────────────────────────────────────
usage() {
cat >&2 <<EOF
$SCRIPT_NAME — create a timestamped tarball backup of a directory
Usage: $SCRIPT_NAME [-hvn] [-o DIR] SOURCE
-h show this help
-v verbose (log every step)
-n dry-run (print commands, do not execute)
-o output directory (default: \$BACKUP_DIR or ./backups)
Environment:
BACKUP_DIR default -o value
RETENTION_DAYS prune archives older than N days (0 = disable)
Examples:
$SCRIPT_NAME -v ./myapp
BACKUP_DIR=/mnt/backups $SCRIPT_NAME -o /mnt/backups ./myapp
EOF
}
# ── helpers ────────────────────────────────────────
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
error "required command not found: $cmd"
exit 2
fi
}
run() {
# run [cmd...] — respects DRY_RUN
if [[ "$DRY_RUN" -eq 1 ]]; then
info "[dry-run] $*"
else
info "exec: $*"
"$@"
fi
}
cleanup() {
local code=$?
if [[ -n "$TEMP_LIST" && -f "$TEMP_LIST" ]]; then
rm -f "$TEMP_LIST"
fi
if [[ "$code" -ne 0 ]]; then
error "exiting with status $code"
fi
exit "$code"
}
# ── argument parsing ───────────────────────────────
parse_args() {
local opt
while getopts ":hvno:" opt; do
case "$opt" in
h) usage; exit 0 ;;
v) VERBOSE=1 ;;
n) DRY_RUN=1 ;;
o) OUTPUT_DIR="$OPTARG" ;;
\?) error "unknown option: -$OPTARG"; usage; exit 2 ;;
:) error "option -$OPTARG requires an argument"; usage; exit 2 ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then
error "missing SOURCE directory argument"
usage
exit 2
fi
if [[ $# -gt 1 ]]; then
error "unexpected extra arguments: $*"
usage
exit 2
fi
SOURCE="$1"
}
# ── validation ─────────────────────────────────────
validate_inputs() {
require_cmd tar
require_cmd find
require_cmd date
if [[ ! -d "$SOURCE" ]]; then
error "SOURCE is not a directory: $SOURCE"
exit 2
fi
if [[ ! -d "$OUTPUT_DIR" ]]; then
info "creating output directory: $OUTPUT_DIR"
run mkdir -p "$OUTPUT_DIR"
fi
if [[ ! -w "$OUTPUT_DIR" ]]; then
error "output directory is not writable: $OUTPUT_DIR"
exit 2
fi
}
# ── core work ──────────────────────────────────────
prune_old_backups() {
[[ "$RETENTION_DAYS" -eq 0 ]] && return 0
info "pruning backups older than $RETENTION_DAYS days in $OUTPUT_DIR"
run find "$OUTPUT_DIR" -maxdepth 1 -name '*.tar.gz' -mtime "+$RETENTION_DAYS" -print -delete
}
run_backup() {
local base dest ts
base="$(basename "$SOURCE")"
ts="$(date '+%Y%m%d-%H%M%S')"
dest="$OUTPUT_DIR/${base}-${ts}.tar.gz"
TEMP_LIST="$(mktemp)"
trap cleanup EXIT
info "backing up $SOURCE → $dest"
run tar -czf "$dest" -C "$(dirname "$SOURCE")" "$base"
if [[ "$DRY_RUN" -eq 0 ]]; then
local size
size="$(du -h "$dest" | cut -f1)"
info "backup complete: $dest ($size)"
fi
prune_old_backups
info "done"
}
# ── entry point ────────────────────────────────────
main() {
parse_args "$@"
validate_inputs
run_backup
}
main "$@"
Walk through what each layer does {Đi qua từng lớp làm gì}:
set -euo pipefail— any failure stops the script; unset variables error; piped failures propagate {mọi lỗi dừng script; biến chưa set báo lỗi; lỗi trong pipe lan truyền} (Part 9).getopts+usage— predictable CLI; unknown flags exit2{CLI dự đoán được; flag lạ thoát2}.require_cmd— fail before work iftar/findmissing {fail trước khi làm việc nếu thiếutar/find}.mktemp+trap cleanup EXIT— temp file removed on success, error, or Ctrl-C {file tạm xóa khi thành công, lỗi, hoặc Ctrl-C}.run()+-n— dry-run mode for CI previews and operator confidence {chế độ dry-run cho preview CI và tự tin vận hành}.- Meaningful exit codes —
2for misuse,1for runtime (implicit viaset -e) {exit code có nghĩa —2dùng sai,1runtime (ngầm quaset -e)}.
Best practices checklist {Checklist best practice}
Before you merge or deploy a shell script, run through this list {Trước khi merge hoặc deploy shell script, chạy qua checklist này}:
| Check {Kiểm tra} | Why {Vì sao} |
|---|---|
#!/usr/bin/env bash shebang | Portable bash path {Đường dẫn bash portable} |
set -euo pipefail at top (after any needed -e exceptions) | Fail fast, no silent unset vars {Fail nhanh, không biến unset im lặng} |
usage() + -h + exit 2 on bad args | Operators know how to run it {Vận hành biết cách chạy} |
| Logs to stderr, data to stdout | Safe piping {Pipe an toàn} |
command -v for external deps | Clear error instead of “command not found” mid-run {Lỗi rõ thay vì “command not found” giữa chừng} |
trap cleanup EXIT for temp files / locks | No leftover junk on failure {Không rác tạm khi lỗi} |
Quote variables: "$var", "$@" | Word-splitting and glob surprises (Part 2) {Word-splitting và glob bất ngờ (Phần 2)} |
readonly for constants | Accidental overwrite caught early {Ghi đè nhầm bị bắt sớm} |
Env vars documented + defaults via ${VAR:-default} | Configurable without editing source {Cấu hình không cần sửa source} |
Run ShellCheck (shellcheck script.sh) | Catches common bugs before runtime {Bắt bug phổ biến trước runtime} |
LF line endings, chmod +x | No \r surprises (Part 1) {Không bất ngờ \r (Phần 1)} |
When NOT to use bash {Khi KHÔNG nên dùng bash}
Bash excels at gluing Unix tools and automating tasks that are mostly spawning processes and moving files {Bash giỏi ghép công cụ Unix và tự động hóa tác vụ chủ yếu spawn process và di chuyển file}. Reach for Python, Go, or another language when {Chuyển sang Python, Go, hoặc ngôn ngữ khác khi}:
- Logic gets complex — nested conditionals, state machines, or business rules that need unit tests {Logic phức tạp — điều kiện lồng nhau, state machine, hoặc rule nghiệp vụ cần unit test}.
- You need real data structures — dicts, JSON parsing, CSV with typed columns, ORM/database layers {Cần cấu trúc dữ liệu thật — dict, parse JSON, CSV có cột kiểu, lớp ORM/database}.
- Cross-platform is required — Windows without WSL, or macOS + Linux with identical behavior beyond
#!/usr/bin/env bash{Cần cross-platform — Windows không WSL, hoặc macOS + Linux hành vi giống hệt ngoài#!/usr/bin/env bash}. - Heavy string or JSON work —
jqhelps, but maintaining 200 lines ofjq+ bash is often harder than 50 lines of Python {Xử lý string/JSON nặng —jqgiúp, nhưng bảo trì 200 dòngjq+ bash thường khó hơn 50 dòng Python}. - Performance at scale — processing gigabytes in-process, tight loops, or concurrency beyond background jobs {Hiệu năng quy mô lớn — xử lý gigabyte trong process, vòng lặp chặt, hoặc concurrency vượt background job}.
- Team maintainability — if only one person reads bash and everyone else reads TypeScript, a small Node/Python CLI may win {Bảo trì theo team — nếu chỉ một người đọc bash còn cả team đọc TypeScript, CLI Node/Python nhỏ có thể thắng}.
A common pattern: bash script as a thin wrapper that validates env, then exec python -m mytool "$@" {Pattern phổ biến: bash là lớp bọc mỏng validate env, rồi exec python -m mytool "$@"}.
Mistakes even experienced devs make {Lỗi cả dev có kinh nghiệm vẫn mắc}
- ❌ Parsing
"$@"withfor arg in $*— breaks on spaces in filenames {Parse"$@"bằngfor arg in $*— vỡ khi tên file có khoảng trắng}. Use"$@"orgetopts{Dùng"$@"hoặcgetopts}. - ❌ Forgetting
shift $((OPTIND - 1))aftergetopts— positional args still include flags {Quênshift $((OPTIND - 1))saugetopts— arg vị trí vẫn còn flag}. - ❌ Logging to stdout in a script meant to be piped {Log ra stdout trong script để pipe}. Operators get corrupted data streams {Vận hành nhận luồng dữ liệu hỏng}.
- ❌
trapset after the code that creates temp files — early exit skips cleanup {trapđặt sau code tạo file tạm — thoát sớm bỏ qua cleanup}. Set trap as soon asmktempsucceeds {Đặt trap ngay khimktempthành công}. - ❌
set -einside a function without thinking — a failed command in a conditional can still surprise you; test withset -eexplicitly in mind (Part 9) {set -etrong function không suy nghĩ — lệnh fail trong điều kiện vẫn có thể gây bất ngờ; test vớiset -etrong đầu (Phần 9)}. - ❌ Growing a 400-line bash script with JSON APIs instead of admitting it is now an application {Để script bash 400 dòng gọi JSON API thay vì thừa nhận đó giờ là ứng dụng}.
Exercises {Bài tập}
Try each before opening the solution {Thử từng bài trước khi mở lời giải}.
- Extend
backup-dir.shwith a-k Nflag that overridesRETENTION_DAYSfor a single run (without exporting the env var) {Mở rộngbackup-dir.shvới flag-k Nghi đèRETENTION_DAYScho một lần chạy (không export biến env)}. - Write a minimal
deploy.shthat accepts-h,-v, and-t TARGET(required), checksgitandsshexist, and in dry-run mode (-n) only printsrsync -av ./dist/ "$TARGET"{Viếtdeploy.shtối thiểu nhận-h,-v, và-t TARGET(bắt buộc), kiểm tragitvàsshtồn tại, và ở dry-run (-n) chỉ inrsync -av ./dist/ "$TARGET"}. - Run
shellcheckon your script from exercise 2 and fix every warning it reports {Chạyshellchecktrên script bài 2 và sửa mọi cảnh báo}.
Solution {Lời giải}
# Exercise 1 — add to parse_args() in backup-dir.sh:
# In the option string, add k::
while getopts ":hvnk:o:" opt; do
case "$opt" in
# ... existing cases ...
k) RETENTION_DAYS="$OPTARG" ;;
esac
done
# And in usage(), document:
# -k N keep backups for N days (overrides RETENTION_DAYS for this run)#!/usr/bin/env bash
# deploy.sh — exercise 2 + 3 (ShellCheck-clean)
set -euo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
VERBOSE=0
DRY_RUN=0
TARGET=""
usage() {
cat >&2 <<EOF
Usage: $SCRIPT_NAME [-hvn] -t TARGET
-h help
-v verbose
-n dry-run
-t rsync target (required), e.g. user@host:/var/www/app
EOF
}
log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*" >&2; }
require_cmd() {
command -v "$1" >/dev/null 2>&1 || { log "missing: $1"; exit 2; }
}
main() {
local opt
while getopts ":hvnt:" opt; do
case "$opt" in
h) usage; exit 0 ;;
v) VERBOSE=1 ;;
n) DRY_RUN=1 ;;
t) TARGET="$OPTARG" ;;
\?) log "unknown option: -$OPTARG"; usage; exit 2 ;;
:) log "-$OPTARG needs an argument"; usage; exit 2 ;;
esac
done
if [[ -z "$TARGET" ]]; then
log "missing -t TARGET"
usage
exit 2
fi
require_cmd git
require_cmd ssh
[[ "$VERBOSE" -eq 1 ]] && log "target=$TARGET dry_run=$DRY_RUN"
if [[ "$DRY_RUN" -eq 1 ]]; then
log "[dry-run] rsync -av ./dist/ $TARGET"
else
rsync -av ./dist/ "$TARGET"
fi
}
main "$@"shellcheck deploy.sh # exercise 3 — should report no issuesSeries recap {Tóm tắt series}
You made it through all ten parts {Bạn đã hoàn thành cả mười phần}. Here is the full arc {Đây là toàn bộ lộ trình}:
- Part 1 — The Shell, the Shebang & Your First Script
- Part 2 — Variables, Quoting & Expansion
- Part 3 — Conditionals,
test& Exit Codes - Part 4 — Loops
- Part 5 — Functions, Arguments & Sourcing
- Part 6 — Arrays & String Manipulation
- Part 7 — I/O Redirection & Pipes
- Part 8 — Text Processing: grep, sed & awk
- Part 9 — Robust Scripting & Error Handling
- Part 10 — Real-World Automation & Best Practices (you are here) {Phần 10 — Automation thực tế & Best practice (bạn đang ở đây)}
Takeaway {Điều cốt lõi}
A production bash script is not a bag of commands — it is a small program with a strict header, parsed arguments, validated inputs, structured functions, stderr logging, env-based config, trapped cleanup, and meaningful exit codes {Script bash production không phải túi lệnh — mà là chương trình nhỏ có header strict, tham số đã parse, input đã validate, function có cấu trúc, log stderr, config từ env, cleanup có trap, và exit code có nghĩa}. Use bash where it shines (glue, deploy hooks, backups); reach for a richer language when the script starts looking like an app {Dùng bash nơi nó mạnh (glue, deploy hook, backup); chuyển sang ngôn ngữ mạnh hơn khi script bắt đầu giống ứng dụng}. You now have the full toolkit — go automate something real {Giờ bạn có đủ công cụ — hãy tự động hóa thứ gì đó thật}.