Best Practice trong Bash: Tổng hợp kỹ thuật làm chủ script
Tổng hợp best practice trong Bash DevOps: shebang, set -euo pipefail, modular functions, logging, config management, exit codes, shellcheck, và test với bats.
Sau 19 bài trong series, chúng ta đã đi qua từ cơ bản đến nâng cao — từ variables, loops, functions, đến security, cloud CLI. Bài viết này sẽ tổng hợp tất cả best practice để bạn viết Bash scripts chuyên nghiệp, dễ bảo trì, và an toàn trong DevOps.
Tại sao cần best practice? Script Bash chạy tự động trên server, trong pipeline, và đôi khi do nhiều người maintain. Nếu không có quy tắc, scripts sẽ trở thành “spaghetti code” — khó debug, dễ lỗi, và tiềm ẩn lỗ hổng bảo mật.
#!/usr/bin/env bash
# ==============================================================================# Script : <tên script># Mục đích: <mô tả ngắn gọn># Usage: <script.sh> [options] <args># ==============================================================================set -euo pipefail
# ===== CONSTANTS =====readonlySCRIPT_NAME="$(basename "$0")"readonlySCRIPT_DIR="$(cd"$(dirname "$0")"&&pwd)"readonlyTIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"# ===== CONFIGURATION =====readonlyLOG_FILE="${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}"readonlySLACK_WEBHOOK="${SLACK_WEBHOOK:-}"# ===== FUNCTIONS =====log(){locallevel="$1"localmessage="$2"echo"[$TIMESTAMP] [$level] $message"| tee -a "$LOG_FILE"}usage(){ cat << EOF
Usage: $SCRIPT_NAME <option> <argument>
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
Examples:
$SCRIPT_NAME --verbose server01
EOFexit"${1:-0}"}cleanup(){localexit_code=$?if[[$exit_code -ne 0]];then log "ERROR""Script failed with exit code $exit_code"fi# Cleanup code herereturn$exit_code}# ===== MAIN =====main(){# Parse argumentswhile[[$# -gt 0]];docase"$1" in
-h|--help) usage 0;; -v|--verbose)VERBOSE=true;shift;; *)break;;esacdone# Validate required argumentsif[[$# -lt 1]];then log "ERROR""Missing required argument" usage 1fi log "INFO""Script started"# Main logic here log "INFO""Script completed"}# ===== ENTRY POINT =====trap cleanup EXIT
main "$@"
Tại sao template này?
Component
Lý do
#!/usr/bin/env bash
Portable, tìm bash trong PATH
set -euo pipefail
Exit on error, undefined vars, pipe failures
readonly
Tránh thay đổi constants
function log()
Logging chuẩn, dễ debug
usage()
Document script usage
cleanup()
Trap EXIT để cleanup
main()
Entry point rõ ràng
Shebang đúng cách
1
2
3
4
5
6
7
8
9
10
11
12
# KHÔNG dùng#!/bin/bash # Có thể không đúng path# NÊN dùng#!/usr/bin/env bash # Portable, hoạt động trên mọi hệ thống# Nếu script cần root#!/usr/bin/env bashif[[$EUID -ne 0]];thenecho"This script must be run as root" >&2exit1fi
Error handling
set -euo pipefail
1
2
3
4
5
6
# Luôn đặt ở đầu scriptset -euo pipefail
# -e: Thoát ngay khi có lỗi# -u: Báo lỗi khi dùng biến chưa định nghĩa# -o pipefail: Thoát nếu bất kỳ command trong pipe fail
Trap errors
1
2
3
4
5
6
7
8
# Trap ERRtrap'echo "Error at line $LINENO" >&2' ERR
# Trap EXIT (luôn chạy, dù thành công hay thất bại)trap cleanup EXIT
# Trap specific signalstrap'echo "Interrupted"; exit 130' INT TERM
Exit codes có ý nghĩa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Trả về exit code có nghĩaexit0# Successexit1# General errorexit2# Misuse of shell commandexit126# Permission problemexit127# Command not foundexit 128+n # Fatal error signal "n"# Trong scriptvalidate_input(){if[[ -z "$1"]];thenecho"ERROR: Missing argument" >&2return1fireturn0}
log(){locallevel="$1"localmessage="$2"locallog_file="${LOG_FILE:-/tmp/${SCRIPT_NAME}.log}"echo"[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message"| tee -a "$log_file"}# Usagelog "INFO""Deployment started"log "ERROR""Failed to connect to database"
Configuration management
Tách config khỏi code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# BAD — hardcode trong scriptDATABASE_URL="postgres://user:pass@localhost:5432/db"API_KEY="sk-1234567890"# GOOD — load từ config filereadonlyCONFIG_FILE="${CONFIG_FILE:-${SCRIPT_DIR}/config.env}"if[[ -f "$CONFIG_FILE"]];thenset -a
source"$CONFIG_FILE"set +a
elseecho"ERROR: Config file not found: $CONFIG_FILE" >&2exit1fi# Validate required config: "${DATABASE_URL:?DATABASE_URL is required}": "${API_KEY:?API_KEY is required}"
# Function với local variablesdeploy_app(){localenvironment="$1"localversion="$2"# Validate inputsif[[ -z "$environment"|| -z "$version"]];thenecho"Usage: deploy_app <environment> <version>" >&2return1fi# Local scope — không conflict với global varslocaldeploy_dir="/opt/apps/${environment}"# Logicecho"Deploying $version to $environment..."# Deploy logic herereturn0}# Function với return value qua stdoutget_latest_version(){localapp_name="$1"# Trả value qua stdout, KHÔNG dùng return curl -s "https://api.example.com/apps/${app_name}/version"}# UsageVERSION=$(get_latest_version "myapp")deploy_app "production""$VERSION"
# BAD — word splitting, globbingrm $FILEcp $SOURCE$DESTecho$VARIABLE# GOOD — always quoterm "$FILE"cp "$SOURCE""$DEST"echo"$VARIABLE"# Exception: arrays và [[ ]]for file in "${FILES[@]}";doecho"$file"doneif[[$var=="pattern"]];thenecho"matched"fi
Command substitution
1
2
3
4
5
6
7
# BADcd$(dirname $0)files=$(ls)# GOODcd"$(dirname "$0")"files=$(ls)# ls output thường safe, nhưng vẫn nên cẩn thận
Testing với bats
Cài đặt
1
2
3
4
5
6
7
8
# Clone và installgit clone https://github.com/bats-core/bats-core.git
cd bats-core
./install.sh /usr/local
# Hoặc dùng package managerbrew install bats # macOSapt-get install bats # Debian/Ubuntu