Command Box bài học 2: Tư duy đóng gói: Chuyển hóa những câu lệnh Bash rải rác thành một CLI thống nhất
Sau khi nhận diện được những "nỗi đau" vận hành ở Bài học 1, phản ứng tự nhiên của hầu hết anh em Backend là gom các lệnh hay dùng vào vài file .sh rồi ném vào một thư mục scripts/ ở góc project.
Cách làm này giải quyết được phần ngọn (không phải gõ lại lệnh dài), nhưng lại tạo ra một cơn ác mộng mới: Sự hỗn loạn của các tập lệnh rời rạc. Bài viết thứ 2 này sẽ giúp bạn thay đổi hoàn toàn tư duy — từ người "viết script chữa cháy" sang một người "kiến trúc công cụ" thông qua việc chuẩn hóa và đóng gói mọi thứ về một mối (Single Entrypoint).
1. Tại sao thư mục scripts/ luôn kết thúc trong hỗn loạn?
Hãy nhìn vào một thư mục scripts/ điển hình sau 6 tháng phát triển dự án:
Plaintext
scripts/
├── deploy_staging.sh
├── deploy_prod_v2_final.sh
├── fix_kafka_lag.sh
├── dump-db-local.sh
├── test_webhook.py
└── sync_es_new.sh
Có 4 vấn đề chí mạng khiến cấu trúc này sớm muộn cũng sụp đổ:
-
Không có quy chuẩn đặt tên và khám phá (Zero Discoverability): File thì dùng
snake_case, file thìkebab-case, cái viết bằng Bash, cái lại lôi Python ra chạy. Một người mới nhìn vào không biết file nào còn dùng được, file nào đã lỗi thời từ năm ngoái. -
Lệ thuộc vào Đường dẫn hiện tại (Working Directory Dependency): Rất nhiều script viết kiểu
source ./config.envhoặcdocker compose -f ../docker-compose.yml up. Nếu bạn đứng ở thư mục gốc gọi./scripts/dump-db-local.shthì chạy ngon, nhưng nếucdvào trongscripts/rồi mới gõ./dump-db-local.shlà báo lỗi không tìm thấy file ngay lập tức. -
Truyền tham số theo "Hệ tâm linh" (Positional Arguments): Trong Bash, việc dùng
$1,$2,$3là một cái bẫy. Lệnh./sync_es_new.sh orders true 500nghĩa là gì? Số500là batch size hay timeout?truelà xóa index cũ hay chạy ngầm? Chỉ cần truyền ngược thứ tự$2và$3là thảm họa xảy ra. -
Thiếu cơ chế chia sẻ logic chung (Code Duplication): Hàm kiểm tra kết nối DB, hàm in màu sắc ra màn hình, hay hàm bắn thông báo về Telegram bị copy-paste lặp đi lặp lại ở cả 10 file
.sh. Khi đổi token hoặc đổi format log, bạn phải mở cả 10 file ra sửa.
2. Tư duy Đóng gói (Encapsulation): Chuyển từ "Files" sang "Capabilities"
Đóng gói thành một CLI thống nhất (Command Box) không phải là dùng một ngôn ngữ lập trình khác để gọi lại lệnh exec("bash script.sh"). Đó là việc thay đổi cách tổ chức tư duy theo 3 trụ cột:
① Điểm chạm duy nhất (Single Entrypoint)
Thay vì bắt người dùng nhớ tên hàng chục file script và đường dẫn của chúng, họ chỉ cần nhớ duy nhất một tên lệnh gốc (ví dụ: cbox hoặc tên viết tắt dự án của bạn). Mọi tính năng đều xuất phát từ điểm chạm này và có thể gọi từ bất kỳ thư mục nào trên máy tính nhờ được gắn vào biến môi trường $PATH.
② Phân cấp theo Ngữ cảnh (Noun-Verb / Domain-Driven Hierarchy)
Một CLI chuyên nghiệp giống như một hệ thống RESTful API được thiết kế tốt. Thay vì đặt tên hàm dài ngoằng, hãy gom nhóm theo Đối tượng (Noun) và Hành động (Verb) hoặc ngược lại:
| Cách gọi bằng Script rời rạc (Cũ) | Cách gọi qua Command Box (Mới) | Ý nghĩa kiến trúc |
|---|---|---|
./scripts/dump-db-local.sh |
cbox db dump --env=local |
Gom vào nhóm quản lý Database (db). |
./scripts/fix_kafka_lag.sh |
cbox kafka reset-offset --group=order |
Gom vào nhóm xử lý hàng đợi (kafka). |
./scripts/sync_es_new.sh |
cbox search reindex --target=orders |
Gom vào nhóm tìm kiếm (search). |
./scripts/deploy_prod_v2_final.sh |
cbox deploy --env=prod --version=v2 |
Chuẩn hóa tham số bằng Flags tường minh. |
③ Biến Tham số ẩn thành Hợp đồng rõ ràng (Explicit Contracts)
Thay thế hoàn toàn $1, $2 vô nghĩa bằng hệ thống Flags (--flag) có tên, có kiểu dữ liệu (string, int, boolean), có giá trị mặc định (default value) và bắt buộc kiểm tra hợp lệ (validation) trước khi chạy.
3. Thực hành chuyển đổi: Từ một đoạn Bash "Mì ăn liền" sang Command Box
Hãy xem một ví dụ thực tế khi bạn cần viết công cụ: Kiểm tra và đồng bộ lại dữ liệu đơn hàng bị lệch giữa MySQL và Elasticsearch.
❌ Cách cũ: File resync_order.sh đầy rủi ro
Bash
#!/bin/bash
# Cách chạy: ./resync_order.sh 100500 prod true
ORDER_ID=$1
ENV=$2
FORCE=$3
if [ -z "$ORDER_ID" ]; then
echo "Thiếu order id rồi!"
exit 1
fi
echo "Đang đồng bộ đơn hàng $ORDER_ID trên môi trường $ENV..."
# Gọi curl hoặc chạy câu lệnh php/go rời rạc bên dưới...
curl -X POST "http://internal-api.$ENV.local/v1/orders/$ORDER_ID/sync?force=$FORCE"
Điểm yếu: Nếu người dùng gõ ./resync_order.sh prod 100500 (nhầm thứ tự), script vẫn chạy và gọi vào URL [http://internal-api.100500.local/](http://internal-api.100500.local/)... gây lỗi ngớ ngẩn. Không có --help, không kiểm tra xem $ENV truyền vào có nằm trong danh sách cho phép (local, staging, prod) hay không.
✅ Cách mới: Tư duy đóng gói trong Command Box (Minh họa bằng Golang + Cobra)
Khi đưa vào Command Box, ta tách biệt rõ ràng 3 lớp: Định nghĩa giao diện lệnh (CLI Definition) Kiểm tra ràng buộc (Validation) Thực thi nghiệp vụ (Execution).
Go
// Cấu trúc lệnh: cbox order sync --id=100500 --env=prod --force
var (
orderID int
env string
force bool
)
var orderSyncCmd = &cobra.Command{
Use: "sync",
Short: "Đồng bộ dữ liệu đơn hàng từ MySQL sang Elasticsearch",
Example: ` cbox order sync --id=100500 --env=staging
cbox order sync -i 100500 -e prod --force`,
PreRunE: func(cmd *cobra.Command, args []string) error {
// Lớp Validation: Chặn lỗi ngay từ cửa ngõ
if env != "local" && env != "staging" && env != "prod" {
return fmt.Errorf("môi trường '%s' không hợp lệ (chỉ chấp nhận: local, staging, prod)", env)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
// Lớp Thực thi: Gọi trực tiếp hàm xử lý chuẩn chỉnh
return syncOrderService(orderID, env, force)
},
}
func init() {
orderSyncCmd.Flags().IntVarP(&orderID, "id", "i", 0, "ID của đơn hàng cần đồng bộ (Bắt buộc)")
orderSyncCmd.Flags().StringVarP(&env, "env", "e", "local", "Môi trường thực thi (local|staging|prod)")
orderSyncCmd.Flags().BoolVarP(&force, "force", "f", false, "Ghi đè dữ liệu bất chấp trạng thái hiện tại")
_ = orderSyncCmd.MarkFlagRequired("id")
}
Giá trị nhận lại ngay lập tức:
-
Tự bảo vệ: Truyền
--id=abc(sai kiểu số nguyên) CLI tự động báo lỗi ngay mà chưa cần chạy vào code của bạn. -
Không cần nhớ thứ tự: Bạn gõ
cbox order sync -f -e prod -i 100500hay đảo vị trí các cờ thế nào thì kết quả vẫn chính xác tuyệt đối. -
Tài liệu tự sinh: Gõ
cbox order sync --help, một bảng hướng dẫn đẹp đẽ kèm ví dụ (Example) hiện ra ngay lập tức.
4. Quy trình 4 bước để "Dọn rác" Script cũ vào Command Box
Nếu dự án của bạn đang có sẵn hàng chục file script rải rác, đừng xóa chúng ngay. Hãy áp dụng chiến lược chuyển đổi từng phần (Strangler Fig Pattern):
-
Bước 1 - Kiểm kê và Phân loại (Inventory): Liệt kê toàn bộ các câu lệnh/script đang có. Gom chúng thành các nhóm miền nghiệp vụ (ví dụ:
db,docker,kafka,deploy,log). -
Bước 2 - Tạo vỏ bọc trước (Wrapper Phase): Khởi tạo khung Command Box. Ở giai đoạn đầu, nếu chưa kịp viết lại logic bằng code mới, bạn có thể cho Command Box đứng làm "mặt tiền" nhận flags chuẩn chỉnh, sau đó gọi ngầm vào script cũ bên dưới. Người dùng bắt đầu quen với việc gõ lệnh qua Command Box.
-
Bước 3 - Tách lớp Cấu hình dùng chung (Shared Context): Đưa toàn bộ các biến hardcode (URL, cổng kết nối, đường dẫn thư mục gốc dự án) vào một bộ quản lý cấu hình tập trung bên trong Command Box.
-
Bước 4 - Thay thế lõi (Native Rewrite): Dần dần xóa bỏ từng file
.shbên dưới và viết lại bằng thư viện gốc của ngôn ngữ lập trình để tận dụng tốc độ, xử lý đa luồng và bắt lỗi chi tiết.
💡 Bài tập thực hành cuối bài (Dành cho độc giả):
Hãy lấy 1 file
.shphức tạp nhất mà bạn đang dùng hiện tại. Hãy thử thiết kế lại "Mặt tiền" cho nó trên giấy theo cấu trúc:
Tên lệnh phân cấp:
<tên-box> <nhóm-lệnh> <hành-động>Danh sách các Flags: Cờ nào là bắt buộc? Cờ nào có giá trị mặc định? Kiểu dữ liệu của từng cờ là gì?
Chỉ riêng việc viết tường minh các thông số này ra, bạn đã loại bỏ được 80% nguy cơ gõ nhầm lệnh trong lúc đang xử lý sự cố (incident) lúc nửa đêm!
All rights reserved