Command Box bài học 12: Xử lý Flags, Options và Arguments sao cho tường minh, ít lỗi vặt
Khi thiết kế một câu lệnh trong Command Box, ranh giới giữa một công cụ "dễ gõ, dễ nhớ" và một công cụ "dễ gây tai nạn" nằm ở cách bạn định nghĩa đầu vào. Rất nhiều lập trình viên thiết kế CLI theo cảm tính: lúc thì bắt truyền tham số qua vị trí (cbox deploy staging order-srv), lúc lại bắt truyền qua cờ (cbox deploy --env=staging --service=order-srv), hoặc đặt tên cờ mỗi nơi một kiểu (--id, --order_id, --orderId).
Sự thiếu nhất quán này khiến người dùng liên tục phải gõ --help để tra cứu, hoặc nguy hiểm hơn là truyền nhầm tham số dẫn đến chạy sai logic trên môi trường thực tế. Bài học số 12 sẽ giúp bạn thiết lập bộ quy tắc chuẩn POSIX/GNU để phân định rạch ròi giữa Arguments, Options và Flags, kết hợp cùng các kỹ thuật kiểm soát ràng buộc nâng cao trong Golang (Cobra/pflag).

(Hình minh họa: Một câu lệnh CLI chuẩn mực luôn tách biệt rõ ràng giữa lệnh thực thi, các tùy chọn mở rộng và đối tượng đích).
1. Phân biệt đúng bản chất: Arguments vs. Options vs. Flags
Trong giao tiếp hàng ngày, anh em kỹ sư thường gọi chung mọi tham số truyền vào CLI là "flag" hoặc "arg". Nhưng khi bắt tay vào thiết kế kiến trúc parser cho Command Box, bạn phải tách bạch 3 khái niệm này:
| Thành phần | Cú pháp nhận diện | Bản chất & Vai trò | Ví dụ thực tế |
|---|---|---|---|
| Arguments (Positional Args) | Đứng độc lập, không có dấu gạch ngang (-) phía trước. |
Đối tượng đích (Direct Object) mà hành động trực tiếp nhắm tới. Thứ tự truyền vào quyết định ý nghĩa. | cbox logs tail order-service (tên service là Argument) |
| Options (Key-Value Flags) | Có tiền tố --key=value hoặc -k value. |
Bộ bổ nghĩa (Modifiers) giúp tinh chỉnh cách thức câu lệnh vận hành. Không phụ thuộc thứ tự. | cbox db dump --env=staging --limit=500 |
| Flags (Boolean Switches) | Có tiền tố --flag hoặc -f, không nhận giá trị đi kèm. |
Công tắc Bật/Tắt (Switches) làm thay đổi trạng thái hoặc hành vi mặc định (mặc định luôn là false). |
cbox order sync --force --dry-run -v |
2. Quy tắc vàng: Khi nào dùng Positional Argument, khi nào dùng Flag/Option?
Đây là câu hỏi gây tranh cãi nhiều nhất khi thiết kế CLI: "Nên viết cbox order cancel ORD123 hay cbox order cancel --id=ORD123?". Hãy áp dụng 4 nguyên tắc phân xử sau đây:
① Quy tắc "Số lượng tối đa là 1" (The Single-Target Rule)
Chỉ dùng Positional Argument khi đối tượng đó là bắt buộc, hiển nhiên theo ngữ nghĩa của động từ đứng trước, và chỉ có đúng 1 loại đối tượng.
-
Tốt (Tự nhiên, nhanh gọn):
cbox service restart payment-apihoặccbox file upload ./backup.sql. -
Tệ (Bẫy thứ tự):
cbox db copy staging_db prod_dbCái nào là nguồn (source), cái nào là đích (destination)? Nếu người dùng lỡ tay đảo ngược thứ tự, dữ liệu Staging sẽ ghi đè bay màu dữ liệu Production! -
Sửa lại bằng Options tường minh:
cbox db copy --from=staging_db --to=prod_db.
② Mọi tham số mang tính "Hủy diệt" hoặc "Nhạy cảm" đều phải dùng Option có tên
Với các lệnh có khả năng xóa dữ liệu, đổi trạng thái đơn hàng hoặc tác động vào môi trường thực tế, hãy ép người dùng phải gõ tên cờ tường minh (--env=prod, --target-id=100) thay vì dùng vị trí. Việc bắt ngón tay phải gõ chữ --env= giúp bộ não có thêm 1 giây phản xạ kiểm tra an toàn.
③ Không bao giờ tạo Boolean Flag mang giá trị mặc định là true
Khi người dùng nhìn thấy một cái cờ kiểu --validate, họ mặc định hiểu rằng nếu không truyền cờ thì sẽ không validate. Nếu bạn đặt mặc định là true, để tắt nó đi họ phải gõ --validate=false (rất ngược tay trên terminal).
- Giải pháp: Nếu hành vi mặc định đã là
true, hãy đặt tên cờ theo chiều phủ định:--no-validatehoặc--skip-verify(mặc định làfalse, khi truyền vào sẽ bật cờ bỏ qua bước kiểm tra).
④ Chuẩn hóa tên gọi trên toàn bộ Command Box (Naming Consistency)
Không được phép ở lệnh db thì dùng --environment, sang lệnh kafka lại dùng --env, còn sang lệnh order lại viết --stage. Hãy lập một bảng từ điển Flag chuẩn ngay từ đầu:
-
-e, --env: Môi trường (local,staging,prod) -
-o, --output: Định dạng xuất dữ liệu (table,json,yaml) -
-f, --file: Đường dẫn file đầu vào/đầu ra -
-n, --dry-run: Chạy thử không ghi dữ liệu -
-y, --yes: Tự động đồng ý các câu hỏi xác nhận -
-t, --timeout: Thời gian chờ tối đa (kiểutime.Durationnhư30s,5m)
3. Vũ khí bí mật của Cobra: Kiểm soát Ràng buộc Chéo (Flag Groups)
Một trong những nguyên nhân gây ra "lỗi vặt" nhiều nhất trong code CLI là lập trình viên phải tự viết hàng chục câu lệnh if/else để kiểm tra sự xung đột giữa các cờ. Từ phiên bản Cobra hiện đại, bạn có sẵn 3 hàm khóa ràng buộc tự động cực kỳ mạnh mẽ:
-
MarkFlagsMutuallyExclusive("flagA", "flagB")(Loại trừ lẫn nhau): Cấm người dùng truyền cả 2 cờ cùng lúc. Ví dụ: trong lệnh đồng bộ đơn hàng, không thể vừa truyền--all(đồng bộ tất cả) lại vừa truyền--id=123(đồng bộ 1 đơn). -
MarkFlagsRequiredTogether("flagA", "flagB")(Đi kèm bắt buộc): Nếu đã truyền cờ A thì bắt buộc phải có cờ B. Ví dụ: nếu truyền--db-userthì bắt buộc phải truyền kèm--db-pass. -
MarkFlagsOneRequired("flagA", "flagB")(Ít nhất một cái): Bắt buộc người dùng phải chọn ít nhất 1 trong các cờ được liệt kê. Ví dụ: phải truyền--idhoặc--from-filethì lệnh mới chạy.
4. Thực hành viết code: Xử lý Flags, Options & Arguments chặt chẽ trong Golang
Hãy xem cách kết hợp Custom Type (pflag.Value) để tự động kiểm tra giá trị hợp lệ của --env, kết hợp với Positional Args Validator và Flag Groups trong một lệnh thực tế cbox kafka replay:
Go
package kafka
import (
"fmt"
"strings"
"time"
"github.com/spf13/cobra"
)
// 1. Tạo kiểu dữ liệu Enum tự kiểm tra hợp lệ (Custom Flag Type) cho Environment
type EnvType string
const (
EnvLocal EnvType = "local"
EnvStaging EnvType = "staging"
EnvProd EnvType = "prod"
)
func (e *EnvType) String() string { return string(*e) }
func (e *EnvType) Type() string { return "env" }
func (e *EnvType) Set(v string) error {
switch strings.ToLower(v) {
case "local", "staging", "prod":
*e = EnvType(strings.ToLower(v))
return nil
default:
return fmt.Errorf("chỉ chấp nhận một trong các giá trị: [local, staging, prod]")
}
}
// 2. Gom toàn bộ Options/Flags vào một Struct riêng biệt
type replayOptions struct {
env EnvType
messageID string
fromFile string
limit int
timeout time.Duration
dryRun bool
}
func NewReplayCmd() *cobra.Command {
opts := &replayOptions{
env: EnvLocal, // Giá trị mặc định an toàn nhất
}
cmd := &cobra.Command{
Use: "replay <topic-name>",
Short: "Phát lại (replay) các bản tin lỗi vào Kafka topic đích",
Example: ` # Replay 1 bản tin cụ thể vào topic 'orders.created' trên môi trường staging
cbox kafka replay orders.created --id=MSG_9981 --env=staging
# Replay hàng loạt từ file danh sách kèm chế độ chạy thử (dry-run)
cbox kafka replay orders.created --file=./dlq_ids.txt --limit=100 --dry-run`,
// 3. Kiểm soát chặt chẽ Positional Argument: Bắt buộc truyền đúng 1 tên Topic
Args: cobra.ExactArgs(1),
// Hỗ trợ gợi ý tên Topic khi người dùng bấm phím Tab
ValidArgs: []string{"orders.created", "payments.completed", "inventory.updated"},
RunE: func(cmd *cobra.Command, args []string) error {
topic := args[0] // An toàn tuyệt đối vì đã có cobra.ExactArgs(1) bảo vệ
fmt.Printf("🚀 Đang chuẩn bị replay vào topic [%s] (Môi trường: %s)\n", topic, opts.env)
if opts.dryRun {
fmt.Println("💡 [DRY-RUN MODE] Chỉ mô phỏng, không bắn bản tin thật vào Kafka.")
}
// Gọi xuống tầng Core xử lý...
return nil
},
}
// 4. Đăng ký các Flags với kiểu dữ liệu chuẩn xác
cmd.Flags().VarP(&opts.env, "env", "e", "Môi trường đích (local|staging|prod)")
cmd.Flags().StringVarP(&opts.messageID, "id", "i", "", "Mã bản tin đơn lẻ cần replay")
cmd.Flags().StringVarP(&opts.fromFile, "file", "f", "", "Đường dẫn file chứa danh sách mã bản tin")
cmd.Flags().IntVarP(&opts.limit, "limit", "l", 50, "Số lượng bản tin tối đa xử lý trong 1 lần chạy")
cmd.Flags().DurationVarP(&opts.timeout, "timeout", "t", 30*time.Second, "Thời gian chờ tối đa (ví dụ: 10s, 2m)")
cmd.Flags().BoolVar(&opts.dryRun, "dry-run", false, "Chạy mô phỏng kiểm tra trước khi thực thi thật")
// 5. Áp dụng khóa ràng buộc chéo (Flag Groups) - Không cần viết if/else thủ công!
// Bắt buộc phải truyền --id HOẶC --file
cmd.MarkFlagsOneRequired("id", "file")
// Nhưng cấm truyền cả --id VÀ --file cùng một lúc
cmd.MarkFlagsMutuallyExclusive("id", "file")
// Cờ --limit chỉ có ý nghĩa khi đi kèm với --file
cmd.MarkFlagsMutuallyExclusive("id", "limit")
return cmd
}
Điều gì xảy ra khi người dùng gõ sai trên Terminal?
Nhờ thiết kế trên, bạn không cần viết một dòng if/else nào trong hàm RunE mà vẫn bắt được mọi lỗi ngớ ngẩn với thông báo chuẩn chỉnh từ Cobra:
-
Gõ thiếu tên topic (
cbox kafka replay --id=123):Error: accepts 1 arg(s), received 0 -
Truyền sai tên môi trường (
cbox kafka replay orders.created --id=123 --env=dev):Error: invalid argument "dev" for "-e, --env" flag: chỉ chấp nhận một trong các giá trị: [local, staging, prod] -
Truyền cả
--idlẫn--filecùng lúc:Error: if any flags in the group [id file] are set none of the others can be; [file id] were all set -
Truyền thời gian dễ đọc (
--timeout=1m30s): Thư việnpflagtự động parse chuỗi"1m30s"thành đúng90 * time.Secondmà không sợ nhầm lẫn đơn vị giây hay mili-giây như khi dùng kiểu số nguyênint!
5. Mẹo xử lý dấu gạch ngang kép -- (End of Options Delimiter)
Một lỗi kinh điển khác khi viết Command Box đóng vai trò "vỏ bọc" (wrapper) gọi xuống công cụ khác (như docker exec, ssh, hoặc go test) là xung đột cờ. Ví dụ: bạn viết lệnh cbox container exec <container-name> <command>. Khi người dùng gõ:
Bash
cbox container exec my-redis redis-cli -p 6380
Parser của Command Box sẽ nhìn thấy cờ -p 6380 và tưởng đó là cờ của cbox, dẫn đến báo lỗi unknown shorthand flag: 'p'!
Cách giải quyết chuẩn POSIX: Hướng dẫn người dùng (và thiết kế hàm nhận args) sử dụng ký hiệu hai dấu gạch ngang -- để báo cho Cobra biết: "Mọi thứ đứng sau dấu -- đều là Arguments nguyên bản truyền thẳng xuống lệnh con, ngừng parse flag tại đây!":
Bash
cbox container exec my-redis -- redis-cli -p 6380 -a mysecret
Trong Cobra, toàn bộ phần phía sau -- sẽ nằm gọn gàng trong mảng args để bạn truyền thẳng vào exec.CommandContext mà không bao giờ bị lỗi xung đột cờ.
💡 Bài tập thực hành cuối bài (Dành cho độc giả):
Hãy mở code Command Box của bạn và tìm tất cả những chỗ đang khai báo cờ thời gian bằng kiểu
IntVar(ví dụ--timeout=30không rõ là giây hay phút) để đổi ngay sangDurationVar(time.Duration). Đồng thời, hãy áp dụng interfacepflag.Value(như ví dụEnvTypeở trên) cho cờ--envhoặc--formatđể loại bỏ hoàn toàn các đoạn codeif env != ...lặp đi lặp lại trong dự án!
All rights reserved