Xây dựng CLI với Rust và Clap: Tạm biệt những script Bash ‘rối như tơ vò’

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Tại sao mình quyết định ‘chia tay’ Bash để sang Rust?

Hồi đầu năm, team mình quản lý hệ thống microservices bằng một tập hợp script Bash khổng lồ. Chúng dùng để lọc log, check health và hỗ trợ deploy. Ban đầu, Bash rất tiện vì viết nhanh. Tuy nhiên, khi logic phức tạp hơn — như cần gọi API, parse JSON hay xử lý lỗi — mọi thứ dần trở nên mất kiểm soát.

Việc debug một file Bash dài 500 dòng mà không có type-safety chẳng khác nào đánh cược với vận may. Mình đã dành một cuối tuần để viết lại công cụ đó bằng Rust và thư viện Clap. Kết quả thật bất ngờ: productivity của cả team tăng vọt. Các lỗi runtime ngớ ngẩn biến mất hoàn toàn. Tốc độ thực thi cũng nhanh gấp 10 lần so với trước đây.

Nếu bạn muốn xây dựng công cụ dòng lệnh chuyên nghiệp, có tính năng tự động gợi ý (help) và kiểm tra tham số chặt chẽ, Rust chính là chân ái. File binary sau khi build có thể chạy ngay trên mọi server mà không cần cài thêm runtime hay dependency nào khác.

Cài đặt môi trường trong 30 giây

Trước hết, máy bạn cần có Rust. Nếu chưa cài, hãy chạy lệnh thần thánh này:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Tiếp theo, chúng ta khởi tạo dự án với cargo. Mình sẽ đặt tên công cụ này là rtool — một trợ thủ giả định để quản lý cấu hình.

cargo new rtool
cd rtool

Mở file Cargo.toml và thêm clap vào. Ở đây mình dùng tính năng derive. Nó giúp định nghĩa CLI cực kỳ ngắn gọn bằng các annotation, tương tự như Decorator trong TypeScript.

[dependencies]
clap = { version = "4.4", features = ["derive"] }
anyhow = "1.0" # Xử lý lỗi nhanh gọn hơn

Định nghĩa CLI bằng Derive API

Điểm ăn tiền của Clap là bạn chỉ cần khai báo một struct. Thư viện sẽ tự động lo phần parse tham số và tạo giao diện hướng dẫn (help menu).

Cấu trúc tham số

Hãy thay thế nội dung file src/main.rs bằng đoạn code dưới đây:

use clap::{Parser, Subcommand};
use std::path::PathBuf;

#[derive(Parser)]
#[command(author = "DevOps Team", version = "1.0", about = "Công cụ quản lý hệ thống siêu tốc")]
struct Cli {
    /// Đường dẫn file cấu hình
    #[arg(short, long, value_name = "FILE", default_value = "config.toml")]
    config: PathBuf,

    /// Bật chế độ log chi tiết
    #[arg(short, long, action = clap::ArgAction::SetTrue)]
    debug: bool,

    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Kiểm tra kết nối hệ thống
    Check { 
        #[arg(short, long)]
        remote: bool 
    },
    /// Khởi tạo dữ liệu mẫu
    Init { 
        name: String 
    },
}

Những điểm cần lưu ý

  • short, long: Tự động tạo flag kiểu -c hoặc --config.
  • default_value: Giúp người dùng không cần nhập mọi tham số một cách thủ công.
  • Subcommand: Biến CLI của bạn trở nên chuyên nghiệp như git hay docker. Bạn có thể gõ rtool check hoặc rtool init my-project cực kỳ mượt mà.

Xử lý logic và vận hành

Bây giờ là lúc đưa các tham số đã parse vào hoạt động thực tế trong hàm main:

fn main() {
    let cli = Cli::parse();

    if cli.debug {
        println!("[*] Đang chạy ở chế độ Debug...");
    }

    println!("File cấu hình: {:?}", cli.config);

    match &cli.command {
        Some(Commands::Check { remote }) => {
            if *remote {
                println!("Đang kết nối tới server từ xa...");
            } else {
                println!("Đang kiểm tra local...");
            }
        }
        Some(Commands::Init { name }) => {
            println!("Khởi tạo dự án: {}", name);
        }
        None => {
            println!("Gõ --help để xem hướng dẫn sử dụng.");
        }
    }
}

Kiểm thử và Phân phối

Đừng quên chạy thử lệnh help mà Clap vừa tự động tạo cho bạn:

cargo run -- --help

Giao diện hiện ra sẽ rất chỉn chu với đầy đủ thông tin version và hướng dẫn flag. Để đảm bảo code không bị lỗi khi nâng cấp, mình thường dùng crate assert_cmd để viết integration test cho binary.

#[test]
fn test_config_default() {
    let args = Cli::parse_from(&["test_app"]);
    assert_eq!(args.config.to_str().unwrap(), "config.toml");
}

Tối ưu cho Production

Khi mọi thứ đã ổn, hãy build bản release để tối ưu hiệu năng:

cargo build --release

File binary tại target/release/rtool thường chỉ nặng khoảng vài MB. Bạn có thể copy nó lên bất kỳ server Linux nào và chạy ngay lập tức. So với Python (cần venv, pip) hay Node.js (cần node_modules nặng nề), Rust gọn nhẹ hơn hẳn.

Chuyển từ script lỏng lẻo sang Rust giúp team mình giảm được 80% lỗi runtime do sai tham số. Nếu bạn đang đau đầu bảo trì những file Bash dài dằng dặc, hãy thử dành một buổi để “Rust-ify” chúng. Cảm giác an tâm khi vận hành hệ thống là thứ giá trị nhất mà Rust mang lại.

Share: