Bỏ qua để đến nội dung

Đối số dòng lệnh trong Rust

Phần lớn những dự án nhỏ đáng làm để luyện tập — một danh sách việc cần làm, một công cụ đếm từ — đều cần nhận dữ liệu đầu vào từ dòng lệnh thay vì viết cứng giá trị trong mã nguồn. Bài này giới thiệu những điều cơ bản.

use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}

Nếu bạn biên dịch đoạn này thành một chương trình tên greet và chạy:

Terminal window
./greet Alice

Bạn sẽ thấy:

["greet", "Alice"]

Phần tử đầu tiên luôn là tên của chính chương trình — các đối số (argument) bạn thật sự gõ vào bắt đầu từ chỉ số 1.

use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage: greet <name>");
return;
}
let name = &args[1];
println!("Hello, {name}!");
}

Chạy bằng cargo run -- Alice và bạn sẽ nhận được:

Hello, Alice!

Chạy mà không có đối số nào, bạn sẽ thấy thông báo hướng dẫn cách dùng (“Usage”) thay vì chương trình bị crash — kiểm tra args.len() trước khi truy cập theo chỉ số giúp tránh panic khi ai đó quên truyền đối số.

Vì mọi đối số đều đến dưới dạng String, bạn thường cần .parse() (trong bài Ép kiểu) để biến nó thành một con số:

use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage: square <number>");
return;
}
match args[1].parse::<i32>() {
Ok(n) => println!("{}", n * n),
Err(_) => println!("That's not a valid number"),
}
}

Flag là một đối số dùng để bật một tính năng nào đó, như --shout. Trước khi dùng crate, sẽ có ích nếu bạn xem cách tự xử lý nó. Mẹo giúp việc này dễ thử nghiệm là đặt logic vào một hàm nhận các đối số dưới dạng slice, &[String]. Khi đó main có thể truyền đối số thật, còn bạn có thể truyền một danh sách tự tạo trong lúc thử:

fn run(args: &[String]) {
let mut shout = false;
let mut names = Vec::new();
// bỏ qua chỉ số 0, tức tên chương trình
for arg in &args[1..] {
if arg == "--shout" {
shout = true;
} else {
names.push(arg);
}
}
if names.is_empty() {
println!("Usage: greet [--shout] <name>...");
return;
}
for name in names {
let line = format!("Hello, {name}!");
if shout {
println!("{}", line.to_uppercase());
} else {
println!("{line}");
}
}
}
fn main() {
// Trong chương trình thật, bạn sẽ viết:
// let args: Vec<String> = std::env::args().collect();
// Ở đây ta tự tạo danh sách, để nó chạy được cả trong Playground.
let args = vec![
String::from("greet"),
String::from("--shout"),
String::from("Ana"),
String::from("Ben"),
];
run(&args);
}

Kết quả:

HELLO, ANA!
HELLO, BEN!
▶ Tự chạy thử

Lời chào ở đây được giữ tiếng Anh vì chương trình đổi nó sang chữ in hoa bằng to_uppercase(). Vài điều cần để ý:

  • &args[1..] là mọi đối số trừ tên chương trình. Flag có thể xuất hiện ở bất cứ đâu: greet Ana --shout Ben cũng chạy được.
  • Bỏ --shout khỏi danh sách rồi chạy lại: bạn nhận được lời chào bình thường. Bỏ luôn các cái tên, và bạn nhận được thông báo hướng dẫn cách dùng.
  • Vì run không quan tâm danh sách của nó đến từ đâu, chuyển sang std::env::args().collect() chỉ là thay đổi một dòng trong main.

Cách này ổn với một hai flag. Khi bạn muốn có thêm dạng viết tắt (-s), giá trị đi sau flag (--count 3) và thông báo --help, vòng lặp tự viết sẽ phình ra rất nhanh. Đó là lúc nên chuyển sang clap.

Tự truy cập args theo chỉ số sẽ nhanh chóng trở nên rối rắm khi bạn cần flag (--verbose), giá trị tùy chọn, hay thông báo --help hữu ích. Giải pháp chuẩn là crate clap (tiếng Anh), thêm vào bằng:

Terminal window
cargo add clap --features derive
use clap::Parser;
#[derive(Parser)]
struct Args {
name: String,
#[arg(short, long, default_value_t = 1)]
count: u32,
}
fn main() {
let args = Args::parse();
for _ in 0..args.count {
println!("Hello, {}!", args.name);
}
}

Nó cho bạn việc phân tích đối số, kiểm tra hợp lệ và một thông báo --help chạy được, tất cả đều được sinh ra từ định nghĩa struct. Bạn không cần clap cho các script nhanh, nhưng với bất cứ thứ gì bạn sẽ dùng nhiều hơn một lần, nó tiết kiệm rất nhiều công sức.

Playground không truyền được đối số thật, nên cả hai bài tập đều bắt đầu từ một Vec<String> tự tạo trông giống hệt thứ mà env::args().collect() sẽ trả về. Trên máy của bạn, hãy thay nó bằng đối số thật và thử cargo run -- ....

Cho vec!["notes", "add", "mua sữa"] (dạng String), in ra người dùng đã gõ bao nhiêu đối số (không tính tên chương trình), rồi in từng đối số kèm vị trí của nó, bắt đầu từ 1.

Gợi ý: số lượng là args.len() - 1. Vòng lặp for i in 1..args.len() cho bạn từng chỉ số sau tên chương trình.

Xem lời giải
fn main() {
// giả sử danh sách này đến từ std::env::args().collect()
let args = vec![
String::from("notes"),
String::from("add"),
String::from("mua sữa"),
];
println!("Bạn đã truyền {} đối số", args.len() - 1);
for i in 1..args.len() {
println!("{i}: {}", args[i]);
}
}

Kết quả:

Bạn đã truyền 2 đối số
1: add
2: mua sữa
▶ Tự chạy thử

"mua sữa" được tính là một đối số: trên dòng lệnh thật, bạn sẽ gõ nó trong dấu ngoặc kép (cargo run -- add "mua sữa"), và shell truyền nó vào như một chuỗi duy nhất. Chỉ số 0 vẫn là tên chương trình, vì thế vòng lặp bắt đầu từ 1.

Cho vec!["sum", "4", "10", "x", "6"], hãy cộng mọi đối số là số hợp lệ, và in cảnh báo cho những đối số không phải số. Cuối cùng in ra tổng.

Gợi ý: duyệt qua &args[1..] và dùng match arg.parse::<i32>(), như trong phần Chuyển một đối số thành số. Giữ let mut total = 0; bên ngoài vòng lặp.

Xem lời giải
fn main() {
let args = vec![
String::from("sum"),
String::from("4"),
String::from("10"),
String::from("x"),
String::from("6"),
];
let mut total = 0;
for arg in &args[1..] {
match arg.parse::<i32>() {
Ok(n) => total += n,
Err(_) => println!("Bỏ qua '{arg}': không phải số"),
}
}
println!("Tổng: {total}");
}

Kết quả:

Bỏ qua 'x': không phải số
Tổng: 20
▶ Tự chạy thử

Mọi đối số đều đến dưới dạng văn bản, kể cả "4", nên đối số nào cũng phải được parse. Bỏ qua dữ liệu sai kèm một thông báo thì thân thiện hơn là crash. Bạn cũng có thể dừng ngay ở đối số sai đầu tiên bằng return, nếu điều đó hợp với chương trình của bạn hơn.

  • std::env::args().collect() cho bạn một Vec<String> chứa các đối số dòng lệnh; chỉ số 0 luôn là tên chương trình.
  • Kiểm tra args.len() trước khi truy cập theo chỉ số, để thiếu đối số không làm chương trình panic.
  • .parse() biến một đối số String thành con số, giống như ở mọi chỗ khác.
  • Đặt phần xử lý đối số vào một hàm nhận &[String], để bạn thử được với một danh sách tự tạo.
  • Với bất cứ thứ gì nhiều hơn vài đối số đơn giản, crate clap là lựa chọn chuẩn và đã được kiểm chứng.

Kiểm tra nhanh

1. Khi gọi env::args().collect(), phần tử đầu tiên của Vec kết quả là gì?

2. Vì sao nên kiểm tra args.len() trước khi truy cập args theo chỉ số?

3. Crate clap cho bạn điều gì mà việc tự truy cập args theo chỉ số không có?

Điểm: 0 / 3