0

Command Box bài học 11: Thiết kế Sub-command đa cấp và cấu trúc phân cấp lệnh chuyên nghiệp

Khi Command Box mới có 3-5 tính năng, bạn có thể đặt tất cả các lệnh nằm phẳng lì ngay dưới lệnh gốc: cbox migrate, cbox dump, cbox sync-order, cbox check-kafka.

Nhưng khi công cụ phát triển lên 40-50 lệnh phục vụ từ Database, Message Queue, Docker cho tới quản lý vận hành hệ thống, cấu trúc phẳng (Flat Structure) sẽ lập tức biến màn hình --help thành một "bãi rác" dài cả trăm dòng. Người dùng không thể nhớ nổi lệnh xóa cache viết là cbox clear-redis, cbox redis-flush hay cbox flush-cache.

Bài học số 11 — bài học mở màn cho Giai đoạn 2 (Thiết kế CLI, Parser & Trải nghiệm người dùng) — sẽ hướng dẫn bạn cách quy hoạch Cây lệnh đa cấp (Multi-level Command Tree) theo chuẩn công nghiệp của kubectl, docker và gh, giúp người dùng có thể "đoán đúng tên lệnh" ngay cả khi chưa từng đọc tài liệu.

image.png

(Hình minh họa: Cách tổ chức câu lệnh theo cấu trúc cây phân cấp rõ ràng giữa Root Command, nhánh Subcommand và các đối tượng/cờ đi kèm).

1. Cuộc chiến kinh điển: <Noun> <Verb> hay <Verb> <Noun>?

Trước khi viết dòng code đầu tiên cho cây lệnh, bạn phải chọn một quy ước ngữ pháp thống nhất cho toàn bộ Command Box. Thế giới CLI hiện đại chia làm 2 trường phái lớn:

Trường phái Cấu trúc cú pháp Đại diện tiêu biểu Ưu điểm & Khi nào nên chọn?
1. Noun →\rightarrow Verb
(Danh từ trước, Động từ sau)
cbox <resource> <action>
(Ví dụ: cbox db dump, cbox kafka lag)
docker container ls, gh pr create, gcloud compute instances list Khuyên dùng cho Command Box đa năng: Gom nhóm tự nhiên theo từng miền hạ tầng/nghiệp vụ (db, kafka, order, redis). Dễ phân tách package trong code và cực kỳ lợi hại khi bấm phím Tab khám phá tính năng.
2. Verb →\rightarrow Noun
(Động từ trước, Danh từ sau)
cbox <action> <resource>
(Ví dụ: cbox get pod, cbox delete svc)
kubectl get pods, git checkout branch Chỉ phù hợp khi mọi tài nguyên trong hệ thống đều có chung một tập hành động CRUD giống hệt nhau (get, describe, delete, apply).

Chốt quy tắc vàng cho Command Box: Hãy chọn mô hình cbox <noun> <verb> (Domain-Driven Hierarchy). Một kỹ sư Backend luôn tư duy theo ngữ cảnh công việc hiện tại: "Mình đang cần xử lý Kafka" →\rightarrow gõ cbox kafka →\rightarrow bấm <Tab> để xem có những hành động gì (lag, consume, replay, reset-offset).

2. Quy tắc "Chiều sâu tối đa là 3" (The Depth-3 Limit)

Phân cấp giúp tổ chức gọn gàng, nhưng phân cấp quá sâu lại là kẻ thù của tốc độ gõ phím. Hãy nhìn vào một câu lệnh bị thiết kế quá đà (Over-engineered):

Bash

# ❌ Quá sâu (5 cấp): Gõ mỏi tay và gây ức chế
cbox infrastructure database mysql backup create --db=orders

Trong thiết kế CLI chuyên nghiệp, chiều sâu lý tưởng của một cây lệnh chỉ nên dừng lại ở 2 đến 3 cấp:

  • Cấp 0 (Root): cbox (Điểm chạm duy nhất)

  • Cấp 1 (Domain / Group Noun): db, kafka, order, docker, env (Nhóm tài nguyên chính)

  • Cấp 2 (Action Verb hoặc Sub-resource):

    • Nếu nhóm đơn giản (2 cấp): cbox db dump, cbox order sync.

    • Nếu nhóm lớn cần chia nhỏ (tối đa 3 cấp): cbox kafka topic create, cbox kafka group reset-offset.

3. 4 Kỹ thuật nâng tầm Cây lệnh Đa cấp trong Cobra

Khi xây dựng cây lệnh nhiều tầng bằng thư viện spf13/cobra trong Golang, có 4 kỹ thuật cốt lõi giúp CLI của bạn mượt mà hơn hẳn các công cụ nghiệp dư:

① Lệnh Cha chỉ làm "Biển chỉ dẫn" (Pure Grouping Commands)

Với các lệnh đứng ở nhánh giữa như cbox db hoặc cbox kafka, tuyệt đối không khai báo hàm Run hoặc RunE cho chúng. Khi một cobra.Command không có hàm Run nhưng có các lệnh con (AddCommand), nếu người dùng gõ cụt lủn cbox db rồi bấm Enter, Cobra sẽ tự động hiểu đó là yêu cầu xem danh sách các lệnh con bên trong nhánh db và in ra bảng hướng dẫn mà không báo lỗi!

② Phân chia nhóm hiển thị bằng Command Groups (Từ Cobra v1.6+)

Khi lệnh gốc cbox có tới 15 nhánh con, nếu để Cobra tự sắp xếp theo thứ tự bảng chữ cái (A-Z), lệnh config sẽ nằm cạnh lệnh container, lệnh version nằm cạnh vpc rất lộn xộn. Hãy dùng tính năng AddGroup để chia màn hình --help thành các khu vực trực quan:

  • CORE WORKFLOWS: order, payment, inventory

  • INFRASTRUCTURE & DATA: db, kafka, redis, es

  • SYSTEM & CONFIG: env, doctor, version

③ Kế thừa Cờ xuống toàn bộ nhánh con (PersistentFlags theo Nhánh)

Nhiều người chỉ biết gắn PersistentFlags ở Root Command (cbox). Thực tế, bạn có thể gắn PersistentFlags ngay tại Lệnh cha cấp 1! Ví dụ: Mọi lệnh nằm trong nhánh cbox kafka ... (lag, replay, consume) đều cần cờ --brokers. Thay vì khai báo lặp lại cờ --brokers ở cả 5 lệnh lá, bạn chỉ cần khai báo PersistentFlags().StringVar(&brokers, "brokers", ...) một lần duy nhất ở lệnh cha kafkaCmd!

④ Hệ thống Bí danh thông minh (Aliases) cho người gõ nhanh

Người mới cần sự tường minh (cbox database migrate), nhưng người dùng lâu năm cần tốc độ (cbox db mig). Đừng bắt họ phải chọn một trong hai — hãy dùng thuộc tính Aliases: []string{...} của Cobra để hỗ trợ cả hai cùng lúc!

4. Thực hành viết code: Dựng Cây lệnh Đa cấp có phân nhóm & kế thừa trong Go

Hãy xem cách lắp ghép một nhánh lệnh 3 cấp (cbox kafka group lag) kết hợp AddGroup, PersistentFlags cấp nhánh và Aliases chuẩn kiến trúc:

Go

package cli

import (
	"fmt"
	"github.com/spf13/cobra"
)

// 1. Lệnh Gốc (Cấp 0): Phân chia khu vực hiển thị rõ ràng bằng Command Groups
func NewRootCmd() *cobra.Command {
	rootCmd := &cobra.Command{
		Use:   "cbox",
		Short: "Command Box - Hệ sinh thái công cụ dòng lệnh nội bộ",
	}

	// Định nghĩa các nhóm chức năng lớn trên màn hình --help
	rootCmd.AddGroup(
		&cobra.Group{ID: "domain", Title: "Nghiệp vụ Backend (Domain Workflows):"},
		&cobra.Group{ID: "infra", Title: "Hạ tầng & Dữ liệu (Infrastructure):"},
	)

	// Gắn nhánh lệnh vào đúng Group ID
	kafkaCmd := NewKafkaCmd()
	kafkaCmd.GroupID = "infra"
	rootCmd.AddCommand(kafkaCmd)

	return rootCmd
}

// 2. Lệnh Nhánh Cấp 1 (cbox kafka): Chia sẻ cấu hình chung cho toàn bộ nhánh con
type KafkaBranchOptions struct {
	Brokers []string
}

func NewKafkaCmd() *cobra.Command {
	branchOpts := &KafkaBranchOptions{}

	cmd := &cobra.Command{
		Use:     "kafka",
		Aliases: []string{"kf", "mq"}, // Cho phép gõ tắt: 'cbox kf ...'
		Short:   "Quản lý, kiểm tra độ trễ và xử lý bản tin Apache Kafka",
		// Lưu ý: KHÔNG khai báo hàm Run ở đây để biến nó thành Pure Container Command
	}

	// Cờ Persistent cấp nhánh: Tự động truyền xuống cả 'topic' lẫn 'group' và các lệnh cháu!
	cmd.PersistentFlags().StringSliceVar(
		&branchOpts.Brokers, "brokers", []string{"localhost:9092"}, "Danh sách địa chỉ Kafka Brokers",
	)

	// Gắn các nhánh con Cấp 2 vào 'kafka'
	cmd.AddCommand(NewKafkaGroupCmd(branchOpts))
	// cmd.AddCommand(NewKafkaTopicCmd(branchOpts))

	return cmd
}

// 3. Lệnh Nhánh Cấp 2 (cbox kafka group): Gom các tác vụ liên quan đến Consumer Group
func NewKafkaGroupCmd(branchOpts *KafkaBranchOptions) *cobra.Command {
	cmd := &cobra.Command{
		Use:     "group",
		Aliases: []string{"grp", "cg"}, // Cho phép gõ tắt: 'cbox kf cg lag'
		Short:   "Thao tác với Kafka Consumer Groups (lag, reset-offset, describe)",
	}

	// Gắn các lệnh Lá thực thi (Cấp 3)
	cmd.AddCommand(NewGroupLagCmd(branchOpts))
	return cmd
}

// 4. Lệnh Lá Cấp 3 (cbox kafka group lag): Nơi thực sự chứa hàm RunE
func NewGroupLagCmd(branchOpts *KafkaBranchOptions) *cobra.Command {
	var groupID string

	cmd := &cobra.Command{
		Use:   "lag",
		Short: "Kiểm tra độ trễ (Consumer Lag) theo thời gian thực của một Group",
		Example: `  # Cách gõ đầy đủ tường minh:
  cbox kafka group lag --id=order-processor

  # Cách gõ tắt siêu tốc nhờ Aliases và kế thừa cờ --brokers:
  cbox kf cg lag -i order-processor --brokers=10.0.1.10:9092`,
		RunE: func(cmd *cobra.Command, args []string) error {
			// Sử dụng trực tiếp biến branchOpts.Brokers được kế thừa từ lệnh cha 'kafka'!
			fmt.Printf("🔍 Đang kiểm tra lag của group [%s] tại cụm brokers %v...\n",
				groupID, branchOpts.Brokers)
			return nil
		},
	}

	cmd.Flags().StringVarP(&groupID, "id", "i", "", "Tên Consumer Group cần kiểm tra (Bắt buộc)")
	_ = cmd.MarkFlagRequired("id")

	return cmd
}

Trải nghiệm gõ lệnh linh hoạt từ cấu trúc trên:

  • Dành cho người mới (hoặc viết trong CI/CD script cần dễ đọc):cbox kafka group lag --id=order-worker --brokers=10.0.0.1:9092

  • Dành cho kỹ sư trực hệ thống cần thao tác nhanh trong 1 giây:cbox kf cg lag -i order-worker

5. Bật tính năng "Gợi ý sửa lỗi gõ nhầm" (SuggestionsMinimumDistance)

Khi có nhiều nhánh lệnh con, việc kỹ sư gõ nhanh bị sai chính tả (ví dụ gõ cbox kafka gruop thay vì group, hoặc cbox db mgiarte thay vì migrate) xảy ra như cơm bữa.

Đừng để người dùng nhận một câu báo lỗi lạnh lùng. Ngay tại Root Command, hãy bật thuật toán khoảng cách Levenshtein có sẵn trong Cobra chỉ với 2 dòng cấu hình:

Go

rootCmd.SuggestionsMinimumDistance = 2 // Sai lệch tối đa 2 ký tự sẽ tự động gợi ý lệnh đúng

Khi đồng nghiệp gõ nhầm cbox kafka gruop, Command Box sẽ trả lời cực kỳ thông minh giống hệt như git:

Plaintext

Error: unknown command "gruop" for "cbox kafka"

Did you mean this?
        group

💡 Bài tập thực hành cuối bài (Dành cho độc giả):

Hãy vẽ ra giấy toàn bộ các lệnh dự kiến có trong Command Box của bạn và sắp xếp chúng vào mô hình Cây 3 cấp (cbox -> domain -> action). Hãy thử gom các lệnh con vào tối đa 3 nhóm cobra.Group tại Root Command và thêm ít nhất 1 Aliases viết tắt 2-3 ký tự cho các nhánh lệnh được gõ nhiều nhất mỗi ngày!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí