Command Line cho Developer · Part 10 - Bash Automation Playbook
Capstone command line: khi nào viết script, template bash an toàn, strict mode, trap cleanup, require_cmd, dry-run, parse args đơn giản và project doctor script.
Đây là Phần 10, bài kết của series Command Line cho Developer. Chín phần trước cho bạn các viên gạch: file, log, search, pipe, Git, curl, process, package manager, Docker, SSH. Bài này gom chúng thành script automation.
Script tốt không phải vì nó dài. Script tốt vì nó biến một workflow dễ sai thành một lệnh rõ ràng, lặp lại được, và báo lỗi dễ hiểu.
Khi nào nên viết script?
Viết script khi:
- bạn chạy cùng chuỗi lệnh nhiều lần;
- workflow có bước dễ quên;
- cần validate input trước khi làm việc;
- cần log lại kết quả;
- cần chạy giống nhau trên máy dev và CI;
- một lỗi nhỏ có thể làm mất dữ liệu hoặc deploy sai môi trường.
Không cần script khi:
- command chỉ chạy một lần;
- logic quá domain-specific và nên nằm trong app code;
- cần xử lý dữ liệu phức tạp hơn khả năng shell;
- team không thể đọc/bảo trì Bash.
Nếu script bắt đầu parse JSON phức tạp, gọi API nhiều bước, retry/backoff, hoặc có state lớn, cân nhắc Node/Python/Go.
Template bash an toàn
#!/usr/bin/env bash
set -euo pipefail
main() {
echo "Hello"
}
main "$@"
Ý nghĩa:
| Dòng | Tác dụng |
|---|---|
#!/usr/bin/env bash | chạy bằng bash từ PATH |
set -e | dừng khi command fail |
set -u | lỗi khi dùng biến chưa set |
set -o pipefail | pipeline fail nếu một bước fail |
main "$@" | truyền argument an toàn |
Đây là baseline. Series Bash & Shell Scripting đào sâu từng phần nếu bạn muốn học kỹ.
Logging và lỗi
log() {
printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2
}
die() {
log "ERROR: $*"
exit 1
}
Dùng stderr cho log để stdout vẫn có thể pipe:
log "building project"
npm run build
Khi script dùng trong CI, timestamp nhỏ giúp đọc timeline dễ hơn.
Kiểm tra command bắt buộc
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}
require_cmd git
require_cmd node
require_cmd npm
Fail sớm tốt hơn fail ở giữa script với lỗi khó hiểu.
Với nhiều command:
for cmd in git node npm jq; do
require_cmd "$cmd"
done
Cleanup bằng trap
tmp_dir=$(mktemp -d)
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT
trap cleanup EXIT đảm bảo cleanup chạy khi script kết thúc, kể cả fail.
Không dùng path tạm tự đoán như /tmp/my-script. Dùng mktemp để tránh đụng file người khác hoặc race condition.
Dry-run mode
Một pattern rất hữu ích:
DRY_RUN=0
run() {
if [[ "$DRY_RUN" == "1" ]]; then
printf '[dry-run] %q ' "$@" >&2
printf '\n' >&2
else
"$@"
fi
}
Dùng:
run rm -rf dist
run npm run build
Dry-run giúp script deploy, sync, cleanup bớt đáng sợ. User thấy script định làm gì trước khi nó làm thật.
Parse args đơn giản
DRY_RUN=0
PORT=3000
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=1
shift
;;
--port)
PORT="${2:-}"
[[ -n "$PORT" ]] || die "--port requires a value"
shift 2
;;
-h|--help)
echo "Usage: project-doctor [--dry-run] [--port PORT]"
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
Với script lớn hơn, dùng getopts cho flag ngắn hoặc chuyển sang ngôn ngữ có parser CLI tốt. Với script nội bộ nhỏ, while/case đủ rõ.
Capstone: project-doctor.sh
Script này kiểm tra những thứ hay làm dev server fail: Git state, Node/npm, port, package scripts.
#!/usr/bin/env bash
set -euo pipefail
PORT=3000
log() {
printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2
}
die() {
log "ERROR: $*"
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}
usage() {
cat >&2 <<'USAGE'
Usage: project-doctor.sh [--port PORT]
Checks:
- required commands
- git working tree
- package.json scripts
- whether a local port is already in use
USAGE
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--port)
PORT="${2:-}"
[[ -n "$PORT" ]] || die "--port requires a value"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
}
check_tools() {
for cmd in git node npm; do
require_cmd "$cmd"
done
}
check_git() {
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
log "git status:"
git status --short
else
log "not inside a git repository"
fi
}
check_package_scripts() {
[[ -f package.json ]] || {
log "package.json not found"
return
}
log "package scripts:"
node -e "const p=require('./package.json'); console.log(Object.keys(p.scripts||{}).join('\n'))"
}
check_port() {
log "checking port $PORT"
if command -v lsof >/dev/null 2>&1; then
if lsof -i ":$PORT" >/dev/null 2>&1; then
log "port $PORT is in use:"
lsof -i ":$PORT"
else
log "port $PORT is free"
fi
else
log "lsof not found; skipping port check"
fi
}
main() {
parse_args "$@"
check_tools
check_git
check_package_scripts
check_port
}
main "$@"
Chạy:
chmod +x project-doctor.sh
./project-doctor.sh --port 4321
Checklist script trước khi commit
bash -n script.sh
shellcheck script.sh
./script.sh --help
Nếu script có thao tác nguy hiểm:
- có
--dry-run; - log rõ path/target;
- validate input;
- hỏi confirmation ở command dùng tay;
- không hard-code secret;
- không dùng
rm -rf "$var"khivarcó thể rỗng.
Bài tập
- Tạo
scripts/project-doctor.shtừ template trên. - Thêm check cho
pnpmnếu repo dùngpnpm-lock.yaml. - Thêm
--jsonđể in package scripts bằng JSON nếu cójq. - Chạy
shellcheckvà sửa cảnh báo nếu có.
Gợi ý mở rộng
if [[ -f pnpm-lock.yaml ]]; then
require_cmd pnpm
fiIn JSON nếu có jq:
if command -v jq >/dev/null 2>&1; then
jq '.scripts' package.json
else
node -e "const p=require('./package.json'); console.log(Object.keys(p.scripts||{}).join('\n'))"
fiĐiều cốt lõi
Command line là sức mạnh khi bạn biết ghép lệnh. Bash script là bước tiếp theo khi workflow cần lặp lại an toàn. Viết script nhỏ, fail sớm, log rõ, cleanup bằng trap, có dry-run cho thao tác nguy hiểm, và để logic phức tạp sang ngôn ngữ phù hợp.