Khởi tạo Project Rust mới với Cargo
Mục tiêu: Học cách khởi tạo một ứng dụng Rust mới bằng lệnh cargo new. Bài học này bao gồm việc tìm hiểu cấu trúc dự án được tạo ra, file manifest Cargo.toml, và cách biên dịch cũng như chạy chương trình ban đầu với cargo run.
Chào mừng bạn đến với bước đầu tiên trong việc xây dựng “Markdown to HTML Converter”! Nền tảng của bất kỳ dự án phần mềm tuyệt vời nào cũng là một môi trường được cấu trúc tốt và dễ quản lý. Trong hệ sinh thái Rust, việc này được xử lý bởi Cargo, hệ thống build và trình quản lý package tuyệt vời của Rust. Hãy cùng bắt tay vào việc và tạo dự án của chúng ta.
Tìm hiểu về Cargo
Trước khi gõ lệnh đầu tiên, điều tối quan trọng là phải hiểu Cargo là gì và tại sao nó lại đóng vai trò trung tâm trong quá trình phát triển Rust. Cargo xử lý vô số tác vụ cho bạn:
- Tạo khung dự án (Scaffolding): Nó tạo ra một cấu trúc thư mục tiêu chuẩn cho các dự án mới của bạn. Sự nhất quán này giúp bạn dễ dàng điều hướng và hiểu bất kỳ dự án Rust nào.
- Quản lý Dependency: Nó tải xuống và biên dịch các thư viện (được gọi là các “crate” trong Rust) mà dự án của bạn phụ thuộc vào. Bạn sẽ chỉ định các dependency này trong một file đặc biệt, và Cargo sẽ lo phần còn lại.
- Build code của bạn: Nó điều phối trình biên dịch Rust (
rustc) để biên dịch mã nguồn của bạn thành một file thực thi (binary) hoặc một thư viện (library). - Chạy Test: Nó cung cấp một cách thuận tiện để chạy các unit test và integration test của dự án.
- Và nhiều hơn nữa…: Nó cũng có thể chạy benchmark, tạo tài liệu (documentation) và publish thư viện của bạn lên registry package trung tâm của Rust là
crates.io.
Hãy coi Cargo như một người trợ lý đáng tin cậy của bạn cho tất cả các tác vụ quản trị liên quan đến việc xây dựng phần mềm, cho phép bạn tập trung vào việc viết code thực sự.
Khởi tạo Dự án
Bây giờ, hãy bắt tay vào làm. Mở terminal hoặc command prompt yêu thích của bạn và chạy lệnh sau:
cargo new markdown_converter
Hãy phân tích lệnh đơn giản này:
cargo: Đây là chương trình chúng ta đang gọi, công cụ build của Rust.new: Đây là một subcommand báo cho Cargo biết chúng ta muốn tạo một dự án mới.markdown_converter: Đây là tên chúng ta đặt cho dự án. Cargo sẽ tạo một thư mục mới với tên này.
Khi bạn chạy lệnh này, Cargo sẽ in ra một thông báo xác nhận:
Created binary (application) `markdown_converter` package
Điều này cho bạn biết rằng Cargo đã tạo thành công một package ứng dụng binary mới. Một ứng dụng binary là một chương trình có thể chạy trực tiếp, giống như công cụ CLI của chúng ta. Lựa chọn thay thế là một package thư viện (library), được thiết kế để sử dụng như một dependency bởi các chương trình khác.
Khám phá Cấu trúc Dự án
Cargo vừa tạo ra một thư mục mới có tên markdown_converter. Hãy di chuyển vào trong đó bằng lệnh cd markdown_converter và bạn sẽ thấy cấu trúc sau:
markdown_converter/
├── .git/
├── .gitignore
├── Cargo.toml
└── src/
└── main.rs
Hãy kiểm tra từng phần:
- .git/ và .gitignore: Cargo rất chu đáo khi khởi tạo ngay một repository Git mới cho bạn, khuyến khích các thực hành kiểm soát phiên bản (version control) tốt ngay từ đầu. File
.gitignoređược cấu hình sẵn với các file và thư mục phổ biến liên quan đến Rust mà bạn thường không muốn commit, chẳng hạn như thư mụctargetnơi chứa các file đã được biên dịch. -
Cargo.toml: Đây là file manifest cho dự án Rust của bạn. Nó được viết theo định dạng TOML (Tom’s Obvious, Minimal Language). Nó chứa siêu dữ liệu (metadata) về dự án của bạn, chẳng hạn như tên, phiên bản và tác giả. Quan trọng nhất, đây là nơi bạn sẽ liệt kê các dependency (các crate) mà dự án của bạn cần. Nội dung ban đầu của nó sẽ trông như thế này:
[package] name = "markdown_converter" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -
src/main.rs: Đây là nơi chứa mã nguồn ứng dụng của bạn. Cargo đã tạo một thư mục
srcvà đặt một file có tênmain.rsbên trong đó. Đối với một dự án binary,main.rslà gốc (root) của crate và là điểm entry point của chương trình. Nó đi kèm với một chương trình “Hello, world!” kinh điển:// Đây là hàm main, điểm entry point cho mọi chương trình binary Rust. fn main() { // Macro println! in văn bản ra console. println!("Hello, world!"); }
Biên dịch và Chạy Dự án Mới
Bây giờ bạn đã có một chương trình Rust hoàn chỉnh, có thể chạy được. Để xem nó hoạt động, hãy đảm bảo bạn đang ở trong thư mục markdown_converter và chạy:
cargo run
Lệnh này thực hiện hai việc:
- Biên dịch (Compile): Nó gọi trình biên dịch Rust để build dự án của bạn. Nếu đây là lần đầu tiên, sẽ mất một chút thời gian vì nó biên dịch các thành phần của thư viện chuẩn. File thực thi kết quả sẽ được đặt trong thư mục mới
target/debug/. - Chạy (Run): Nó thực thi chương trình vừa được biên dịch.
Bạn sẽ thấy output sau trong terminal của mình:
Compiling markdown_converter v0.1.0 (/path/to/your/project/markdown_converter)
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
Running `target/debug/markdown_converter`
Hello, world!
Xin chúc mừng! Bạn đã thiết lập thành công môi trường dự án Rust của mình. Cấu trúc chuẩn, gọn gàng này là điểm khởi đầu để xây dựng công cụ CLI mạnh mẽ của chúng ta.
Các Bước Tiếp Theo
Bây giờ bộ khung dự án của chúng ta đã sẵn sàng, nhiệm vụ tiếp theo là thêm external dependency (dependency bên ngoài) đầu tiên, hay còn gọi là “crate”. Chúng ta sẽ thêm crate clap mạnh mẽ, crate này sẽ giúp chúng ta phân tích các đối số dòng lệnh (command-line arguments) như đường dẫn file input và output cho converter của chúng ta.
Tài Liệu Đọc Thêm
Để hiểu sâu hơn về các khái niệm chúng ta đã đề cập, tôi thực sự khuyên bạn nên xem qua tài liệu chính thức:
- Sách The Cargo Book: Tạo một Package Mới
- Sách The Rust Programming Language: Hello, World! - Giải phẫu một chương trình Rust
- Định dạng Manifest: Hướng dẫn chi tiết về
Cargo.toml
Thêm Clap Dependency bằng Cargo
Mục tiêu: Học cách thêm external dependency vào dự án Rust bằng lệnh cargo add. Bài học này giới thiệu crate clap với feature derive của nó để phân tích các đối số dòng lệnh, và giải thích các khái niệm như crate và feature.
Tuyệt vời! Với thư mục dự án đã được khởi tạo, đã đến lúc mang vào công cụ bên ngoài đầu tiên của chúng ta. Bạn đã sử dụng thành công Cargo để tạo dự án; bây giờ, chúng ta sẽ sử dụng nó để quản lý các dependency của dự án. Đây là một trong những tính năng mạnh mẽ nhất của Cargo, cho phép chúng ta tận dụng hệ sinh thái phong phú của các thư viện, hay các crate, mà cộng đồng Rust đã xây dựng.
Giao diện dòng lệnh (CLI) của chúng ta cần hiểu input của người dùng, chẳng hạn như chỉ định file Markdown input và file HTML output mong muốn. Mặc dù chúng ta có thể thử phân tích các đối số dòng lệnh một cách thủ công, nhưng đây là một tác vụ phổ biến với nhiều edge case. Một cách tiếp cận mạnh mẽ và hiệu quả hơn nhiều là sử dụng một crate được thiết kế riêng cho mục đích này. Đối với dự án này, chúng ta sẽ sử dụng clap, một trong những crate phân tích đối số dòng lệnh phổ biến và mạnh mẽ nhất trong hệ sinh thái Rust.
Hiểu về Crate và crates.io
Trước khi thêm dependency, hãy làm rõ “crate” là gì. Trong Rust, một crate là một gói code có thể được biên dịch thành một file thực thi (một binary crate, giống như dự án của chúng ta) hoặc một thư viện (một library crate, dùng để cho các dự án khác sử dụng).
Cộng đồng Rust chia sẻ và khám phá các crate trên một kho lưu trữ trung tâm có tên là crates.io. Hãy coi nó tương đương với npm của JavaScript hoặc PyPI của Python. Khi bạn báo cho Cargo biết bạn cần một crate, nó biết phải tìm nó trên crates.io, tải nó xuống và biên dịch nó cho dự án của bạn.
Thêm một Dependency bằng cargo add
Cách dễ nhất để thêm một dependency vào dự án Rust là sử dụng lệnh cargo add. Lệnh này tự động tìm phiên bản tương thích mới nhất của crate và thêm nó vào file manifest của dự án, Cargo.toml.
Hãy chạy lệnh cho nhiệm vụ hiện tại:
cargo add clap --features derive
Khi bạn chạy lệnh này trong terminal (từ bên trong thư mục markdown_converter), Cargo sẽ thực hiện một số việc:
- Nó kết nối đến index
crates.iođể tìm crateclap. - Nó xác định phiên bản tương thích mới nhất.
- Nó sửa đổi file
Cargo.tomlcủa bạn để bao gồmclapdưới dạng dependency, cùng với feature được chỉ định.
Phần [dependencies] trong file Cargo.toml của bạn, trước đây trống rỗng, bây giờ sẽ trông như thế này:
[dependencies]
clap = { version = "4.4.18", features = ["derive"] }
(Lưu ý: Số phiên bản, như 4.4.18, có thể khác nhau tùy thuộc vào phiên bản mới nhất hiện có khi bạn chạy lệnh.)
“Feature” là gì?
Bạn có thể đang thắc mắc về phần --features derive trong lệnh của chúng ta. Đây là một khái niệm mạnh mẽ trong hệ sinh thái Rust được gọi là các feature.
Nhiều crate được thiết kế để có khả năng cấu hình cao. Chúng có thể có các chức năng tùy chọn mà bạn chỉ muốn đưa vào nếu bạn thực sự cần nó. Điều này giúp giữ cho chương trình biên dịch cuối cùng nhỏ hơn và nhanh hơn bằng cách không bao gồm các code hoặc dependency không sử dụng. Các feature là các flag mà bạn có thể bật để opt-in vào chức năng bổ sung này.
Đối với crate clap, feature derive là một trong những tính năng phổ biến và hữu ích nhất. Nó kích hoạt một loại macro Rust đặc biệt cho phép chúng ta định nghĩa toàn bộ cấu trúc giao diện dòng lệnh chỉ bằng cách tạo một struct Rust. Chúng ta có thể “derive” (kế thừa/tự sinh) parser từ cấu trúc code của mình, đây là một cách định nghĩa đối số, flag và subcommand cực kỳ gọn gàng, dễ đọc và an toàn kiểu (type-safe). Chúng ta sẽ thấy điều này hoạt động trong nhiệm vụ ngay sau đây!
Bằng cách chạy cargo add clap --features derive, chúng ta đã báo cho Cargo: “Tôi cần crate clap, và tôi đặc biệt muốn bật feature derive của nó để tôi có thể sử dụng cách tiếp cận dựa trên struct mạnh mẽ này.”
Điều gì xảy ra tiếp theo?
Bạn đã khai báo dependency thành công, nhưng code của crate clap vẫn chưa có trong dự án của bạn. Lần tới khi bạn chạy cargo build hoặc cargo run, Cargo sẽ thấy mục nhập mới trong Cargo.toml, tải xuống clap (và bất kỳ dependency nào mà bản thân clap cần), biên dịch chúng và liên kết chúng vào ứng dụng của bạn. Bạn không phải quản lý bất kỳ điều gì trong số đó một cách thủ công.
Các Bước Tiếp Theo
Với dependency clap hiện đã có sẵn cho dự án, chúng ta đang ở vị trí hoàn hảo cho nhiệm vụ tiếp theo: tạo một struct đại diện cho các đối số dòng lệnh mà chúng ta mong đợi từ người dùng. Chúng ta sẽ sử dụng feature derive mà chúng ta vừa bật để biến struct đó thành một trình phân tích đối số dòng lệnh hoạt động một cách kỳ diệu.
Tài Liệu Đọc Thêm
Để tìm hiểu thêm về các khái niệm được đề cập trong bước này, hãy khám phá các tài liệu sau:
- Sách The Cargo Book về Dependency: Thêm Dependencies
- Tài liệu Crate
clap: Tài liệuclapchính thức trên docs.rs - Sách The Cargo Book về Feature: Giải thích về Feature
Tạo Struct cho các Đối số CLI
Mục tiêu: Định nghĩa một Rust struct mới có tên CliArgs trong src/main.rs. Struct này sẽ đóng vai trò là bản thiết kế cho giao diện dòng lệnh của bạn bằng cách sử dụng declarative API của clap.
Tuyệt vời, bạn đã thêm thành công crate clap vào các dependency của dự án. Bây giờ là lúc tận dụng feature derive mạnh mẽ mà chúng ta đã bật. Chúng ta sẽ định nghĩa toàn bộ hình hài của giao diện dòng lệnh bằng cách tạo một Rust struct đơn giản. Đây là cốt lõi trong API khai báo (declarative API) hiện đại của clap.
Sức mạnh của Phương pháp Tiếp cận Khai báo (Declarative)
Thay vì viết code mệnh lệnh (imperative code) bảo rằng “kiểm tra đối số đầu tiên, sau đó kiểm tra đối số thứ hai, xem có flag nào không…”, chúng ta sẽ đơn giản là khai báo dữ liệu mà chúng ta mong đợi nhận được từ người dùng. Chúng ta sẽ tạo một cấu trúc dữ liệu tùy chỉnh, và clap sẽ tự động tạo tất cả logic phân tích cú pháp, validation (xác thực) và thông báo trợ giúp (help message) cho chúng ta dựa trên định nghĩa của cấu trúc đó.
Phương pháp này có một số lợi ích chính:
- Tính dễ đọc: Tất cả các đối số CLI của bạn được định nghĩa ở một nơi sạch sẽ, dễ đọc.
- Tính bảo trì: Thêm một flag hoặc option mới đơn giản như thêm một field mới vào struct.
- An toàn kiểu (Type Safety): Bằng cách sử dụng hệ thống kiểu mạnh mẽ của Rust, bạn có được các đảm bảo tại thời điểm biên dịch (compile-time). Nếu bạn khai báo một đối số phải là một số,
clapsẽ xử lý việc phân tích cú pháp và báo lỗi nếu người dùng cung cấp văn bản chữ.
Tạo CliArgs Struct
Bước đầu tiên của chúng ta là tạo bản thiết kế này. Trong Rust, cách chính để tạo một cấu trúc dữ liệu tùy chỉnh là sử dụng từ khóa struct. Một struct (cấu trúc) là một kiểu dữ liệu tùy chỉnh cho phép bạn đặt tên và đóng gói nhiều giá trị có liên quan với nhau thành một nhóm có ý nghĩa. Đối với dự án của chúng ta, “nhóm có ý nghĩa” này là tập hợp các đối số dòng lệnh mà chúng ta mong đợi.
Chúng ta sẽ thêm struct mới này vào file src/main.rs. Hiện tại, nó sẽ là một cái vỏ rỗng, nhưng trong các tác vụ tiếp theo, chúng ta sẽ điền vào nó các field đại diện cho đường dẫn file input và output.
Mở file src/main.rs của bạn và thêm định nghĩa struct.
// src/main.rs
// Đây là hàm main, điểm entry point cho mọi chương trình binary Rust.
fn main() {
// Macro println! in văn bản ra console.
println!("Hello, world!");
}
// Code mới bắt đầu từ đây
/// Một công cụ CLI để chuyển đổi file Markdown sang HTML.
struct CliArgs {
// Chúng ta sẽ thêm các field vào đây ở các bước tiếp theo.
}
// Code mới kết thúc tại đây
Hãy phân tích phần bổ sung này:
/// Một công cụ CLI...: Đây là một bình luận tài liệu (documentation comment). Các bình luận bắt đầu bằng ba dấu gạch chéo (///) là đặc biệt. Chúng được sử dụng để tạo tài liệu cho code của bạn.clapvô cùng thông minh và sẽ sử dụng bình luận này làm mô tả chính cho chương trình của bạn khi nó tạo help message (ví dụ: khi người dùng chạymarkdown_converter --help).struct CliArgs { ... }: Dòng này khai báo một cấu trúc mới có tênCliArgs. Theo quy ước, tên struct trong Rust sử dụngUpperCamelCase. Struct này bây giờ là một kiểu mới trong chương trình của chúng ta, giống nhưStringhoặci32là các kiểu. Hiện tại nó không có field nào bên trong cặp ngoặc nhọn{}, nhưng nó đóng vai trò là container mà chúng ta sẽ xây dựng lên.
Bây giờ bạn đã đặt nền tảng cấu trúc cho CLI của mình. Struct CliArgs trống rỗng này là bộ xương để chúng ta sẽ xây dựng một giao diện dòng lệnh mạnh mẽ và thân thiện với người dùng.
Các Bước Tiếp Theo
Struct này hiện tại chỉ là một struct Rust thông thường. Nó chưa làm bất cứ điều gì liên quan đến phân tích dòng lệnh. Phép thuật sẽ đến ở tác vụ tiếp theo, nơi chúng ta sẽ sử dụng thuộc tính #[derive(clap::Parser)] để hướng dẫn clap biến struct khiêm tốn này thành một parser phân tích đối số đầy đủ tính năng.
Tài Liệu Đọc Thêm
Để hiểu sâu hơn về cấu trúc dữ liệu của Rust, hãy xem cuốn sách chính thức:
- Sách The Rust Programming Language: Chương 5: Sử dụng Struct để cấu trúc dữ liệu liên quan
Kích hoạt Derive Parser của clap cho một CLI Struct
Mục tiêu: Chuyển đổi một Rust struct cơ bản thành một bộ phân tích đối số dòng lệnh (command-line argument parser) bằng cách thêm thuộc tính #[derive(clap::Parser)] và câu lệnh use clap::Parser; cần thiết.
Bạn vừa tạo struct CliArgs, đóng vai trò là bản thiết kế cho dữ liệu mà chúng ta muốn thu thập từ command line. Tuy nhiên, ở trạng thái hiện tại, nó chỉ là một struct Rust thông thường không có sức mạnh đặc biệt. Bây giờ, chúng ta sẽ thực hiện bước quan trọng nhất để thổi hồn vào nó: chúng ta sẽ “dạy” nó cách phân tích các đối số dòng lệnh bằng cách sử dụng một tính năng phép thuật của Rust được gọi là derive macro.
Hiểu về Thuộc tính (Attribute) và Macro derive
Trong Rust, bạn sẽ thường thấy cú pháp #[...] được viết ngay phía trên một struct, enum hoặc fn. Đây được gọi là một thuộc tính (attribute). Các attribute là một cách để thêm siêu dữ liệu (metadata) vào code của bạn. Siêu dữ liệu này có thể được bản thân trình biên dịch Rust sử dụng, hoặc trong trường hợp của chúng ta, là bởi các macro mạnh mẽ có thể tạo ra code thay cho bạn.
Attribute phổ biến nhất mà bạn sẽ gặp là #[derive]. Attribute derive báo cho trình biên dịch tự động implement (triển khai) một số trait nhất định cho cấu trúc dữ liệu của bạn. Trait là một tính năng của Rust định nghĩa một tập hợp các hành vi được chia sẻ (hãy nghĩ về nó giống như một interface trong các ngôn ngữ khác). Ví dụ, bạn có thể derive trait Debug để làm cho struct của bạn có thể in ra được nhằm mục đích debug, hoặc trait Clone để cho phép tạo bản sao của nó.
Quan trọng nhất, các thư viện có thể cung cấp các trait và macro derive tùy chỉnh của riêng chúng. Đây chính xác là những gì clap làm. Khi chúng ta thêm clap với feature derive, chúng ta đã mở khóa khả năng sử dụng #[derive(clap::Parser)].
Biến Struct thành một Parser
Bằng cách chú thích struct CliArgs của chúng ta với #[derive(clap::Parser)], chúng ta đang gọi một procedural macro từ thư viện clap. Tại thời điểm biên dịch, macro này sẽ kiểm tra struct CliArgs của chúng ta—tên của nó, các bình luận tài liệu của nó, và (sắp tới là) các field của nó—và tự động tạo ra tất cả code phức tạp cần thiết để:
- Phân tích các đối số từ command line.
- Xử lý các flag và option.
- Tạo các thông báo trợ giúp chuyên nghiệp (ví dụ: cho
--help). - Cung cấp thông tin phiên bản (từ
Cargo.toml). - Báo cáo lỗi thân thiện với người dùng cho các input không hợp lệ.
- Cuối cùng, điền vào một instance (bản thể) của
CliArgsvới các giá trị đã được phân tích.
Đây là một cách vô cùng mạnh mẽ và có tính khai báo (declarative) để xây dựng một CLI. Chúng ta chỉ cần mô tả dữ liệu chúng ta muốn, và clap sẽ viết parser cho chúng ta.
Hãy sửa đổi src/main.rs để thêm chú thích này. Chúng ta cũng cần đưa trait Parser vào scope (phạm vi) bằng câu lệnh use để Rust biết chúng ta đang đề cập đến Parser nào.
// src/main.rs
// Thêm dòng này để đưa trait Parser vào scope
use clap::Parser;
// Đây là hàm main, điểm entry point cho mọi chương trình binary Rust.
fn main() {
// Macro println! in văn bản ra console.
println!("Hello, world!");
}
/// Một công cụ CLI để chuyển đổi file Markdown sang HTML.
// Đây là attribute biến struct của chúng ta thành một bộ phân tích dòng lệnh.
#[derive(Parser)]
struct CliArgs {
// Chúng ta sẽ thêm các field vào đây ở các bước tiếp theo.
}
Phân tích Code
Hãy xem xét hai thay đổi nhỏ nhưng đáng chú ý này:
-
use clap::Parser;- Dòng này rất quan trọng. Thuộc tính
derivecần biết nơi tìm định nghĩa của traitParser. Bằng cách viếtuse clap::Parser;, chúng ta đang import trait cụ thể này từ crateclapvào local scope của filemain.rs. Điều này cho phép chúng ta chỉ cần viết#[derive(Parser)]thay vì dài dòng#[derive(clap::Parser)]. Đó là một thực hành tiêu chuẩn để cải thiện khả năng đọc.
- Dòng này rất quan trọng. Thuộc tính
-
#[derive(Parser)]- Đây chính là derive macro, được đặt ngay phía trên định nghĩa
structcủa chúng ta. Nó là chỉ thị cho trình biên dịch Rust chạy logic tạo code củaclaptrên structCliArgs. Với một dòng này,CliArgskhông chỉ còn là một data container nữa; nó là một trình phân tích đối số dòng lệnh đầy đủ chức năng.
- Đây chính là derive macro, được đặt ngay phía trên định nghĩa
Giờ đây bạn đã chuyển đổi thành công một struct đơn giản thành phần lõi của giao diện dòng lệnh cho ứng dụng của bạn. Dù nó chưa phân tích bất kỳ đối số cụ thể nào, nhưng bộ máy đã sẵn sàng.
Các Bước Tiếp Theo
Parser của chúng ta đã sẵn sàng, nhưng nó không biết cái gì để phân tích. Bước hợp lý tiếp theo là thêm các field vào struct CliArgs. Chúng ta sẽ thêm các field để đại diện cho đường dẫn file Markdown input và đường dẫn file HTML output, về cơ bản là định nghĩa các đối số mà công cụ CLI của chúng ta sẽ chấp nhận.
Tài Liệu Đọc Thêm
Để hiểu sâu hơn về hệ thống macro mạnh mẽ trong Rust và derive API của clap, những tài liệu sau rất được khuyến nghị:
- Sách Rust Book về Procedural Macro: Chương về Procedural Macro để tạo code từ code
- Tài liệu
clapchính thức: Hướng dẫn Derive
Định nghĩa các Đối số CLI Vị trí (Positional Arguments) trong Rust với Clap
Mục tiêu: Học cách định nghĩa các đối số dòng lệnh theo vị trí (positional command-line arguments) cho một ứng dụng CLI Rust bằng cách thêm các field vào một struct. Bài học này giới thiệu việc sử dụng std::path::PathBuf cho các đường dẫn file và chỉ ra cách derive macro của clap sử dụng các struct field và documentation comment để xây dựng parser.
Làm rất tốt! Bạn đã kết nối thành công derive macro mạnh mẽ của clap với struct CliArgs của bạn. Giờ là lúc giao việc cho parser đó. Một parser sẽ vô dụng nếu nó không biết mình phải tìm kiếm điều gì. Trong tác vụ này, chúng ta sẽ định nghĩa các đối số dòng lệnh cụ thể mà chương trình của chúng ta sẽ chấp nhận bằng cách thêm các field vào struct CliArgs.
Từ Bản thiết kế đến Các Đối số Dòng lệnh
Điều kỳ diệu của derive macro từ clap là nó sẽ kiểm tra các field bên trong struct đã được chú thích của bạn và coi mỗi field như một đối số dòng lệnh mà nó cần phân tích. Tên của field, kiểu của nó, và bất kỳ thuộc tính hay bình luận tài liệu đặc biệt nào bạn thêm vào đều sẽ được clap sử dụng để tạo logic phân tích và các help message.
Converter markdown của chúng ta cần hai thông tin thiết yếu từ người dùng:
- Vị trí của file Markdown input.
- Vị trí mà file HTML output nên được lưu.
Chúng ta sẽ đại diện cho chúng dưới dạng hai positional arguments (đối số theo vị trí). Các đối số theo vị trí là loại cơ bản nhất; chúng được xác định bởi vị trí của chúng trên dòng lệnh thay vì bởi một cái tên (như một --flag). Ví dụ, trong một lệnh như markdown_converter input.md output.html, thì input.md là đối số vị trí đầu tiên và output.html là đối số vị trí thứ hai.
Chọn đúng Type (Kiểu) cho Đường dẫn File: String vs. PathBuf
Chúng ta có thể sử dụng kiểu String để lưu trữ các đường dẫn file, và điều đó vẫn sẽ hoạt động. Tuy nhiên, Rust cung cấp một kiểu đặc biệt và mạnh mẽ hơn nhiều để xử lý các đường dẫn hệ thống file: std::path::PathBuf.
Việc sử dụng PathBuf được coi là một best practice vì một số lý do:
- Tính đúng đắn đa nền tảng (Cross-Platform): Nó xử lý chính xác các dấu phân cách đường dẫn (
/vs.\) và các sắc thái khác trên các hệ điều hành khác nhau (như Windows, macOS, và Linux). - API thao tác đường dẫn: Nó cung cấp một tập hợp phong phú các phương thức để làm việc với đường dẫn, chẳng hạn như nối các đoạn đường dẫn, lấy phần mở rộng của file, hoặc chuyển hướng đến thư mục cha.
- Mục đích rõ ràng (Clarity of Intent): Khi ai đó đọc code của bạn và nhìn thấy
PathBuf, họ sẽ biết ngay rằng biến này được dùng để đại diện cho một đường dẫn hệ thống file, chứ không chỉ là một văn bản tùy ý nào đó.
Chúng ta sẽ sử dụng PathBuf cho các field của mình. Hãy cập nhật src/main.rs để bao gồm các field này và câu lệnh use cần thiết.
Đây là code đã cập nhật cho file src/main.rs của bạn. Hãy chú ý kỹ đến các dòng được thêm vào bên trong struct CliArgs và câu lệnh use mới ở trên cùng.
// src/main.rs
use clap::Parser;
// Thêm dòng này để đưa PathBuf vào scope
use std::path::PathBuf;
// Đây là hàm main, điểm entry point cho mọi chương trình binary Rust.
fn main() {
// Macro println! in văn bản ra console.
println!("Hello, world!");
}
/// Một công cụ CLI để chuyển đổi file Markdown sang HTML.
#[derive(Parser)]
struct CliArgs {
/// Đường dẫn tới file markdown input.
input_path: PathBuf,
/// Đường dẫn cho file HTML output.
output_path: PathBuf,
}
Phân tích các Thay đổi
Hãy phân tích chính xác những gì chúng ta đã thêm và tại sao nó lại có ý nghĩa lớn như vậy.
-
use std::path::PathBuf;- Giống như việc chúng ta cần
use clap::Parser;để sử dụng traitParser, chúng ta cần dòng này để đưa kiểuPathBufvào scope (phạm vi) của file. Điều này cho phép chúng ta chỉ cần viếtPathBufthay vì viết đầy đủstd::path::PathBufmỗi lần.
- Giống như việc chúng ta cần
-
/// Đường dẫn tới file markdown input.- Đây là một documentation comment (bình luận tài liệu) cho field ngay theo sau nó.
clapvô cùng thông minh và sẽ tự động sử dụng đoạn text này làm mô tả cho đối số cụ thể này khi người dùng chạy chương trình của bạn với--help. Đây là một tính năng tuyệt vời khuyến khích bạn viết code có tài liệu tốt.
- Đây là một documentation comment (bình luận tài liệu) cho field ngay theo sau nó.
-
input_path: PathBuf,- Dòng này định nghĩa field đầu tiên của struct, có tên là
input_path. - Theo quy ước,
clapsẽ chuyển đổi tên field dạngsnake_case(input_path) thành định dạngkebab-case(input-path) cho các help message. - Vì nó là field đầu tiên trong struct,
clapsẽ coi nó như là đối số vị trí đầu tiên. - Type là
PathBuf, vì vậyclapsẽ kỳ vọng một giá trị từ command line mà có thể được hiểu như một đường dẫn file.
- Dòng này định nghĩa field đầu tiên của struct, có tên là
-
/// Đường dẫn cho file HTML output.vàoutput_path: PathBuf,- Tương tự, phần này định nghĩa đối số vị trí thứ hai của chúng ta.
clapsẽ kỳ vọng một giá trị thứ hai trên command line và sẽ lưu nó vào fieldoutput_path. Nó cũng có help text của riêng mình.
- Tương tự, phần này định nghĩa đối số vị trí thứ hai của chúng ta.
Với vài dòng code này, bạn đã định nghĩa đầy đủ interface public (giao diện công khai) của công cụ dòng lệnh của mình. Bạn đã nói với clap: “Chương trình của tôi yêu cầu hai đối số vị trí, cả hai đều là đường dẫn file. Đây là tên và mô tả của chúng.” Derive macro bây giờ sẽ tạo ra tất cả code cần thiết để thực thi bản hợp đồng này.
Các Bước Tiếp Theo
Struct CliArgs của chúng ta hiện là một bản thiết kế hoàn chỉnh cho giao diện dòng lệnh. Tác vụ cuối cùng trong bước này là thực sự sử dụng nó. Trong hàm main, chúng ta sẽ gọi một method do trait Parser của clap cung cấp để thực thi parser, đọc các đối số từ người dùng và điền vào một instance của struct CliArgs bằng các kết quả thu được.
Tài Liệu Đọc Thêm
Để củng cố sự hiểu biết của bạn về các khái niệm này, tôi rất khuyên bạn nên khám phá các tài nguyên sau:
- Sách The Rust Programming Language về Struct: Định nghĩa và Khởi tạo Struct
- Tài liệu Standard Library cho
PathBuf:std::path::PathBuf - Hướng dẫn Derive của
clapvề Đối số vị trí: Hướng dẫn Derive - Positional Arguments
Kích hoạt Command-Line Parser trong Rust
Mục tiêu: Sửa đổi hàm main để gọi method CliArgs::parse(). Việc này sẽ kích hoạt logic phân tích đối số dòng lệnh được cung cấp bởi crate clap và lưu trữ các đối số đã được phân tích vào một biến.
Bạn đã làm một công việc xuất sắc khi định nghĩa bản thiết kế cho giao diện dòng lệnh của mình. Struct CliArgs giờ đây đã mô tả hoàn hảo dữ liệu bạn cần từ người dùng: một đường dẫn input và một đường dẫn output. Chú thích #[derive(Parser)] đã tạo ra tất cả logic phân tích phía sau hậu trường. Bây giờ, đã đến lúc đưa logic đó vào hoạt động. Mảnh ghép cuối cùng là thực sự gọi parser từ điểm entry point của chương trình, hàm main.
Kích hoạt Parser với CliArgs::parse()
Thuộc tính #[derive(Parser)] mà chúng ta đã thêm trong tác vụ trước đã implement trait clap::Parser cho struct CliArgs của chúng ta. Một trait trong Rust là cách để định nghĩa các chức năng chia sẻ chung, tương tự như một interface trong các ngôn ngữ khác. Nhờ implement trait này, struct CliArgs của chúng ta đã có thêm một số associated functions mới (các hàm liên kết với chính kiểu dữ liệu đó, chứ không phải một instance của nó).
Quan trọng nhất trong số các hàm này là parse(). Hàm này là động cơ thúc đẩy toàn bộ quá trình phân tích cú pháp. Khi bạn gọi CliArgs::parse(), nó sẽ thực hiện một chuỗi các thao tác quan trọng:
- Nó truy cập các đối số dòng lệnh đã được cung cấp khi ứng dụng của bạn được thực thi.
- Nó kiểm tra tính hợp lệ (validate) của các đối số này so với các field bạn đã định nghĩa trong
CliArgs(trong trường hợp của chúng ta là hai đối sốPathBufvị trí). - Nó tự động xử lý các flag tiêu chuẩn như
--helpvà--versionmà không cần bất kỳ code bổ sung nào từ bạn. Nếu người dùng cung cấp--help,clapsẽ in ra một thông báo trợ giúp được định dạng đẹp mắt và thoát. - Nếu người dùng cung cấp các đối số không hợp lệ (ví dụ: quên đường dẫn output),
clapsẽ in ra một thông báo lỗi thân thiện với người dùng và thoát. - Nếu mọi thứ thành công, nó tạo một instance mới của struct
CliArgscủa bạn, điền vào các field của nó (input_pathvàoutput_path) bằng các giá trị từ dòng lệnh và trả về nó.
Hãy cập nhật hàm main của chúng ta để gọi method này. Chúng ta sẽ lưu instance CliArgs được trả về vào một biến để chúng ta có thể sử dụng nó sau này.
Mở src/main.rs và sửa đổi hàm main như hình bên dưới.
// src/main.rs
use clap::Parser;
use std::path::PathBuf;
fn main() {
// Đây là dòng thực thi việc phân tích đối số dòng lệnh.
// Hàm `parse()` khả dụng trên struct `CliArgs` của chúng ta bởi vì
// chúng ta đã derive trait `Parser`. Nó sẽ đọc các đối số từ command line,
// kiểm tra xem chúng có hợp lệ hay không, và trả về một instance `CliArgs` đã được điền.
let args = CliArgs::parse();
// Chương trình bây giờ sẽ chờ ở đây. Trong tác vụ tiếp theo, chúng ta sẽ kiểm tra
// biến 'args' để xem kết quả.
}
/// Một công cụ CLI để chuyển đổi file Markdown sang HTML.
#[derive(Parser)]
struct CliArgs {
/// Đường dẫn tới file markdown input.
input_path: PathBuf,
/// Đường dẫn cho file HTML output.
output_path: PathBuf,
}
Phân tích Sự thay đổi
Sự thay đổi bên trong main tuy nhỏ nhưng lại có sức mạnh vô cùng lớn.
let args = CliArgs::parse();
Chúng ta đã thay thế câu lệnh println!("Hello, world!"); cũ bằng dòng duy nhất này. Đây là lời gọi (invocation) toàn bộ đoạn code mà clap đã tạo cho chúng ta. Chương trình bây giờ sẽ khởi động, lập tức thử phân tích các đối số dòng lệnh, và nếu thành công, dữ liệu thu được sẽ được lưu trữ trong biến args. Biến args này là một instance của CliArgs, và các field của nó lưu giữ các đường dẫn do người dùng cung cấp. Nếu việc phân tích thất bại vì bất kỳ lý do gì, clap sẽ xử lý nó và chương trình sẽ thoát trước khi bất kỳ dòng code nào khác của chúng ta chạy.
Với dòng lệnh này, logic CLI cốt lõi của ứng dụng của chúng ta hiện đã hoạt động.
Các Bước Tiếp Theo
Chương trình của chúng ta hiện đã phân tích được các đối số, nhưng nó đang làm điều đó một cách thầm lặng. Làm sao chúng ta có thể chắc chắn nó đang hoạt động bình thường? Trong tác vụ cuối cùng của bước thiết lập ban đầu này, chúng ta sẽ kiểm tra biến args và in ra các đường dẫn đã được phân tích ra console. Điều này sẽ mang lại cho chúng ta một vòng phản hồi (feedback loop) hoàn chỉnh và xác nhận rằng giao diện dòng lệnh của chúng ta đang hoạt động đúng như thiết kế.
Tài Liệu Đọc Thêm
Để tìm hiểu thêm về cơ chế của việc phân tích đối số và hàm main, hãy tham khảo các tài liệu này:
- Tài liệu
clapvề TraitParser: Trait Parser trong clap - Sách The Rust Programming Language: Đọc các đối số dòng lệnh (Bài này chỉ cách thực hiện thủ công, điều này sẽ giúp bạn hiểu rõ hơn về những gì
clapđã làm thay bạn!)
Kiểm tra Phân tích Đối số CLI với Debug
Mục tiêu: Cập nhật struct CliArgs để có thể in ra được bằng cách derive trait Debug. Sau đó, sử dụng macro println! trong hàm main để hiển thị các đối số đã được phân tích, xác minh rằng giao diện dòng lệnh đang hoạt động như mong đợi.
Xin chúc mừng bạn đã đạt đến tác vụ cuối cùng của bước thiết lập ban đầu! Bạn đã định nghĩa thành công giao diện dòng lệnh của mình và hướng dẫn clap phân tích input của người dùng vào biến args. Câu hỏi hợp lý tiếp theo là: “Nó có hoạt động không?” Trong lập trình, chúng ta không bao giờ phỏng đoán; chúng ta phải xác minh. Tác vụ này tập trung vào việc tạo ra một feedback loop để xác nhận rằng ứng dụng của chúng ta đang tiếp nhận và diễn giải các đối số dòng lệnh một cách chính xác.
Kiểm tra các Đối số đã được phân tích
Trong tác vụ trước, hàm main của bạn hiện đã chứa dòng này:
let args = CliArgs::parse();
Sau khi dòng này thực thi thành công, biến args sẽ lưu giữ một instance của struct CliArgs của chúng ta. Các field của nó, input_path và output_path, đã được điền bằng các giá trị do người dùng cung cấp trên command line. Để truy cập dữ liệu bên trong các field của struct trong Rust, bạn sử dụng cú pháp dấu chấm đơn giản, giống như trong nhiều ngôn ngữ khác. Ví dụ: args.input_path sẽ cho bạn PathBuf của file input.
Làm cho Struct có thể In ra được bằng #[derive(Debug)]
Mục tiêu của chúng ta là in các giá trị này ra console. Chúng ta có thể sử dụng macro println! quen thuộc cho việc này. Tuy nhiên, nếu chúng ta muốn in một struct tùy chỉnh ra để gỡ lỗi (debugging), chúng ta cần nói cho Rust biết cách định dạng nó. Theo mặc định, các struct tùy chỉnh không thể được in ra ở một định dạng thân thiện với lập trình viên.
Đây là lúc một thuộc tính derive vô cùng hữu ích khác xuất hiện: #[derive(Debug)].
Bằng cách thêm Debug vào danh sách derive của chúng ta, chúng ta yêu cầu trình biên dịch Rust tự động implement trait std::fmt::Debug cho struct của chúng ta. Trait này cung cấp một cách tiêu chuẩn, tập trung vào lập trình viên để biểu diễn struct dưới dạng chuỗi string, rất hoàn hảo cho việc debugging và logging. Việc derive Debug trên hầu như mọi struct và enum tùy chỉnh mà bạn tạo ra là một best practice mang tính phổ quát trong Rust.
Hãy cập nhật struct CliArgs của chúng ta để làm cho nó “có thể in ra dưới dạng debug”.
// src/main.rs
use clap::Parser;
use std::path::PathBuf;
fn main() {
let args = CliArgs::parse();
// Chúng ta sẽ thêm logic in ra tại đây
}
/// Một công cụ CLI để chuyển đổi file Markdown sang HTML.
#[derive(Parser, Debug)] // Chúng ta đã thêm `Debug` ở đây
struct CliArgs {
/// Đường dẫn tới file markdown input.
input_path: PathBuf,
/// Đường dẫn cho file HTML output.
output_path: PathBuf,
}
Bằng cách đổi #[derive(Parser)] thành #[derive(Parser, Debug)], struct CliArgs của chúng ta giờ đây có thể dễ dàng được in ra bằng cách sử dụng một format specifier đặc biệt trong println!: {:?}.
In ra để Xác nhận
Bây giờ chúng ta có thể thêm các lời gọi println! vào hàm main để hiển thị các đường dẫn đã thu được. Điều này mang lại cho chúng ta xác nhận trực quan ngay lập tức rằng phân tích CLI của chúng ta đang hoạt động như mong đợi.
Dưới đây là phiên bản cuối cùng của src/main.rs cho bước này:
// src/main.rs
use clap::Parser;
use std::path::PathBuf;
fn main() {
// Dòng này thực thi việc phân tích đối số dòng lệnh.
let args = CliArgs::parse();
// In toàn bộ struct ra vì mục đích debugging.
// Kí hiệu định dạng `:?` báo cho `println!` sử dụng định dạng `Debug`.
println!("Parsed arguments: {:?}", args);
// Bạn cũng có thể truy cập và in từng field riêng lẻ.
println!("Input file path: {:?}", args.input_path);
println!("Output file path: {:?}", args.output_path);
}
/// Một công cụ CLI để chuyển đổi file Markdown sang HTML.
// Nhờ derive `Debug`, chúng ta có thể dễ dàng in struct ra để kiểm tra.
#[derive(Parser, Debug)]
struct CliArgs {
/// Đường dẫn tới file markdown input.
input_path: PathBuf,
/// Đường dẫn cho file HTML output.
output_path: PathBuf,
}
Chạy và Xác minh Ứng dụng
Bây giờ là lúc chứng minh sự thật. Để chạy chương trình của bạn và truyền các đối số dòng lệnh cho nó, bạn cần sử dụng một cú pháp đặc biệt với Cargo:
cargo run -- <ARGUMENT_1> <ARGUMENT_2>
Dấu gạch ngang đôi (--) là rất quan trọng. Nó báo cho Cargo: “Hãy ngừng việc tự phân tích các đối số đi, và hãy truyền trực tiếp mọi thứ đi sau dấu này cho chương trình của tôi.”
Hãy thử xem sao. Trong terminal của bạn, chạy lệnh sau. Bạn không cần phải tạo các file này; chúng ta chỉ đang kiểm tra xem chương trình có thể bắt được tên của chúng hay không.
cargo run -- sample.md converted.html
Nếu mọi thứ được thiết lập chính xác, Cargo sẽ biên dịch chương trình của bạn, chạy nó với sample.md và converted.html làm đối số, và bạn sẽ thấy kết quả đầu ra tương tự như thế này:
Compiling markdown_converter v0.1.0 (/path/to/your/project/markdown_converter)
Finished dev [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/markdown_converter sample.md converted.html`
Parsed arguments: CliArgs { input_path: "sample.md", output_path: "converted.html" }
Input file path: "sample.md"
Output file path: "converted.html"
Thành công! Đầu ra cho thấy rõ ràng rằng:
- Một instance của
CliArgsđã được tạo. - Field
input_pathđã thu được chính xác đối số đầu tiên,"sample.md". - Field
output_pathđã thu được chính xác đối số thứ hai,"converted.html".
Bây giờ bạn đã xây dựng được một giao diện dòng lệnh hoàn chỉnh, mạnh mẽ và thân thiện với người dùng. Bạn thậm chí có thể thử chạy nó với --help để xem thông báo trợ giúp chuyên nghiệp mà clap đã tự động tạo cho bạn!
Các Bước Tiếp Theo
Bạn đã hoàn thành xuất sắc bước quan trọng đầu tiên của dự án! Bạn đã có một nền tảng vững chắc cho công cụ CLI của mình. Bước tiếp theo là làm cho nó hoạt động thực sự. Chúng ta sẽ chuyển sang phần logic cốt lõi: đọc nội dung từ đường dẫn file input mà struct CliArgs cung cấp, và chuẩn bị ghi vào đường dẫn file output.
Tài Liệu Đọc Thêm
Để làm sâu sắc thêm hiểu biết của bạn về các khái niệm chúng ta đã học, tôi rất khuyên bạn nên khám phá tài liệu chính thức:
- Trait
Debug: Rust by Example: Debug - Macro
println!và Formatting: Tài liệu modulestd::fmt - Truyền các đối số cho chương trình của bạn bằng Cargo: Tài liệu về lệnh
cargo run