jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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òngTác dụng
#!/usr/bin/env bashchạy bằng bash từ PATH
set -edừng khi command fail
set -ulỗi khi dùng biến chưa set
set -o pipefailpipeline 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:

  • --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" khi var có thể rỗng.

Bài tập

  1. Tạo scripts/project-doctor.sh từ template trên.
  2. Thêm check cho pnpm nếu repo dùng pnpm-lock.yaml.
  3. Thêm --json để in package scripts bằng JSON nếu có jq.
  4. Chạy shellcheck và sửa cảnh báo nếu có.
Gợi ý mở rộng
if [[ -f pnpm-lock.yaml ]]; then
  require_cmd pnpm
fi

In 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.