Mục lục
- Mục Tiêu Bài Học
- env::args() — Iterator CLI Argument
- env::args_os() Cho Non-UTF8 Path
- env::var() Trả Result
- env::vars() Iterator Toàn Bộ Environment
- env::current_dir & set_current_dir
- env::temp_dir, home_dir Deprecated — Dùng dirs Crate
- Preview clap Cho CLI Parser
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Đọc CLI argument bằng
env::args()— iterator trả vềString, biếtargs[0]là path tới binary chứ không phải arg đầu tiên. - Hiểu khi nào phải dùng
env::args_os()thay vìargs()— khi path có thể không phải UTF-8 (đặc biệt trên Windows hoặc filename rác trên Linux). - Đọc environment variable an toàn qua
env::var("HOME")trảResult, kết hợpunwrap_or_elsecho fallback. - Duyệt toàn bộ env qua
env::vars(), áp dụngfilter/collectcho config inspector. - Truy vấn và thay đổi current working directory với
env::current_dir()vàenv::set_current_dir(). - Biết
env::temp_dir()trả thư mục tạm theo OS, và lý doenv::home_dir()bị deprecated — chuyển sang cratedirs. - Hiểu lý do project thực tế dùng
clapthay vì parseenv::args()bằng tay.
env::args() — Iterator CLI Argument
Hàm std::env::args() trả về một iterator Args sinh ra từng String đại diện cho một argument trên dòng lệnh. Lưu ý quan trọng: args[0] là đường dẫn tới binary đang chạy, không phải argument đầu tiên người dùng truyền. Đây là quy ước Unix cổ điển (argv[0]), Rust giữ nguyên cho nhất quán.
use std::env;
fn main() {
for (i, arg) in env::args().enumerate() {
println!("arg[{i}] = {arg}");
}
}
Chạy cargo run -- hello world ra:
arg[0] = target/debug/myapp
arg[1] = hello
arg[2] = world
Hai pattern phổ biến để skip binary path và lấy "real arguments":
// Cách 1: skip 1 phần tử đầu
let args: Vec<String> = env::args().skip(1).collect();
// Cách 2: collect tất cả rồi index từ 1
let all: Vec<String> = env::args().collect();
let first_arg = all.get(1); // Option<&String>
Pitfall đáng chú ý: args() sẽ panic nếu bất kỳ argument nào không phải valid UTF-8. Trên Linux, filename không bắt buộc UTF-8 — user có thể truyền tên file chứa byte sequence rác (ví dụ từ filesystem cũ encode Latin-1). Trên Windows, argument là UTF-16, được chuyển sang UTF-8 với lossless conversion nhưng surrogate đơn lẻ có thể gây vấn đề. Khi tool xử lý filename, dùng args_os() ở section sau.
env::args_os() Cho Non-UTF8 Path
Anh em song sinh của args() là env::args_os() trả về iterator các OsString — kiểu string "platform native", giữ nguyên byte sequence của OS mà không ép thành UTF-8.
use std::env;
use std::path::PathBuf;
fn main() {
for arg in env::args_os().skip(1) {
// OsString → PathBuf cho thao tác file
let path = PathBuf::from(&arg);
println!("processing: {}", path.display());
}
}
Vì sao cần phân biệt? Đó là rule sống còn cho CLI tool xử lý file:
- Linux/macOS: filename là chuỗi byte tuỳ ý kết thúc bằng NUL, có thể không decode được UTF-8. Tool như
ls,cp,rmphải xử lý được mọi byte sequence. - Windows: filename là chuỗi 16-bit unit (UTF-16) cho phép unpaired surrogate — không hợp lệ UTF-8.
Nguyên tắc: tool nhận filename từ CLI nên dùng args_os() để không "ăn" mất argument đặc biệt và không bao giờ panic. Convert sang String chỉ khi chắc chắn là text (option flag, value parse number, v.v.).
// Pattern an toàn cho mixed CLI tool
let mut iter = env::args_os().skip(1);
while let Some(arg) = iter.next() {
if let Some(s) = arg.to_str() {
// s là &str — xử lý như option flag
match s {
"--verbose" => { /* ... */ }
_ => { /* path? */ }
}
} else {
// arg là path không UTF-8 — chuyển PathBuf để dùng OS API
}
}
env::var() Trả Result
Environment variable đọc qua env::var(name) trả Result<String, VarError>. Hai variant của VarError:
NotPresent— biến không tồn tại trong môi trường.NotUnicode(OsString)— biến tồn tại nhưng giá trị không phải UTF-8.
use std::env;
fn main() {
// Đọc HOME, fallback "/tmp" nếu không có
let home = env::var("HOME").unwrap_or_else(|_| String::from("/tmp"));
println!("home = {home}");
// Đọc DATABASE_URL bắt buộc
let db_url = env::var("DATABASE_URL")
.expect("DATABASE_URL phải được set trước khi chạy");
println!("db = {db_url}");
// Pattern với match cho phân biệt nguyên nhân
match env::var("PORT") {
Ok(v) => println!("port = {v}"),
Err(env::VarError::NotPresent) => println!("PORT chưa set, dùng default 8080"),
Err(env::VarError::NotUnicode(_)) => eprintln!("PORT chứa ký tự lạ"),
}
}
Set environment variable từ trong code qua env::set_var(key, value), xoá qua env::remove_var(key). Lưu ý: thao tác này không thread-safe trên Unix — nếu chương trình có thread khác đang đọc environment thì có thể race. Rust 1.85 đã đánh dấu set_var/remove_var là unsafe để cảnh báo rõ rủi ro này.
Khi đọc giá trị có khả năng không UTF-8 (hiếm, nhưng có thể với LANG, LC_* trên hệ thống cấu hình lạ), dùng env::var_os(name) trả Option<OsString> — không bao giờ fail vì encoding.
env::vars() Iterator Toàn Bộ Environment
Khi cần inspect toàn bộ environment — debug deployment, viết tool snapshot config, hoặc forward biến vào subprocess — dùng env::vars(). Iterator này trả về tuple (String, String) tương ứng key và value, snapshot tại thời điểm gọi.
use std::env;
fn main() {
// In tất cả env theo định dạng KEY=VALUE
for (k, v) in env::vars() {
println!("{k}={v}");
}
// Lọc các biến bắt đầu bằng "APP_"
let app_config: Vec<(String, String)> = env::vars()
.filter(|(k, _)| k.starts_with("APP_"))
.collect();
println!("app config: {app_config:?}");
}
Tương tự cặp var() / var_os(), có env::vars_os() trả tuple (OsString, OsString) khi cần xử lý môi trường có thể chứa giá trị không UTF-8. Trong code application bình thường, vars() đủ dùng — môi trường được set bởi shell hoặc orchestrator (Docker, systemd) gần như luôn là UTF-8.
Một use case hay: viết wrapper script — đọc env::vars(), filter bớt biến nhạy cảm (token, password), in còn lại để log audit trước khi exec subprocess.
env::current_dir & set_current_dir
Process Unix/Windows có khái niệm current working directory (CWD) — thư mục mặc định cho relative path. Rust expose qua hai hàm trong std::env:
use std::env;
use std::path::PathBuf;
fn main() -> std::io::Result<()> {
// Đọc CWD hiện tại
let cwd: PathBuf = env::current_dir()?;
println!("CWD = {}", cwd.display());
// Đổi sang thư mục khác
env::set_current_dir("/tmp")?;
println!("CWD mới = {}", env::current_dir()?.display());
Ok(())
}
current_dir() trả io::Result<PathBuf> — có thể fail nếu process không có quyền đọc CWD hoặc directory đã bị xoá khỏi dưới chân (trên Linux process vẫn chạy được nhưng getcwd sẽ fail). set_current_dir(path) nhận bất kỳ AsRef<Path> nào và trả io::Result<()>, fail khi path không tồn tại hoặc không phải directory.
Pitfall: thay đổi CWD ảnh hưởng toàn process, không phải thread-local. Trong app multi-thread, một thread gọi set_current_dir sẽ thay đổi CWD cho tất cả thread khác — rất dễ gây bug. Best practice: tránh dùng set_current_dir trong code library; thay vào đó truyền absolute path cho mọi I/O operation, hoặc dùng Path::join với base directory đã canonicalize.
env::temp_dir, home_dir Deprecated — Dùng dirs Crate
env::temp_dir() trả về PathBuf cho thư mục tạm của OS — /tmp trên Unix (hoặc theo TMPDIR nếu set), C:\Users\<user>\AppData\Local\Temp trên Windows. Hàm này không fail và không tạo thư mục; chỉ trả vị trí chuẩn theo platform.
use std::env;
use std::fs;
fn main() -> std::io::Result<()> {
let tmp = env::temp_dir();
let scratch = tmp.join("myapp-scratch.txt");
fs::write(&scratch, b"hello")?;
println!("ghi vào {}", scratch.display());
Ok(())
}
Trái lại, env::home_dir() đã deprecated từ Rust 1.29 (2018) với lý do behavior không nhất quán giữa các platform — trên Windows hàm này dùng USERPROFILE theo cách khác với Win32 API chuẩn. Standard library quyết định không sửa mà gỡ khỏi API ổn định, chuyển trách nhiệm cho crate ngoài.
Replacement là crate dirs (rất phổ biến, ~50M download). Nó cung cấp API đầy đủ và đúng đắn theo XDG Base Directory (Linux), Known Folder API (Windows), Standard Directories (macOS):
# Cargo.toml
[dependencies]
dirs = "5"
fn main() {
let home = dirs::home_dir().expect("không xác định được home");
let config = dirs::config_dir().unwrap(); // ~/.config trên Linux
let cache = dirs::cache_dir().unwrap(); // ~/.cache trên Linux
let data = dirs::data_dir().unwrap(); // ~/.local/share trên Linux
println!("home = {}", home.display());
println!("config = {}", config.display());
println!("cache = {}", cache.display());
println!("data = {}", data.display());
}
Nguyên tắc nhớ: env::temp_dir() OK dùng, env::home_dir() không tồn tại trong code mới — luôn dùng dirs::home_dir() hoặc tương đương.
Preview clap Cho CLI Parser
Parse argument bằng tay với env::args() tốt cho script nhỏ, nhưng CLI thực tế cần: subcommand, short/long flag, validation, default value, help message auto-generated, version flag, shell completion. Tự viết tất cả không khả thi. Cộng đồng Rust thống nhất chọn clap — crate được dùng bởi cargo chính nó.
# Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "myapp", version, about = "Demo CLI với clap")]
struct Cli {
/// File input để xử lý
#[arg(short, long)]
input: String,
/// Số worker thread
#[arg(short, long, default_value_t = 4)]
workers: usize,
/// Verbose mode
#[arg(short, long)]
verbose: bool,
}
fn main() {
let cli = Cli::parse();
println!("input={}, workers={}, verbose={}", cli.input, cli.workers, cli.verbose);
}
Chạy cargo run -- --input data.csv -w 8 -v, hoặc cargo run -- --help để xem help message clap tự sinh. Toàn bộ parse + validate + error message đẹp được derive từ struct. Bài Group 38 (Async I/O & Tokio) và rust-restful series sẽ dùng clap cho mọi tool CLI thực tế.
Tổng Kết
env::args()trả iteratorStringcác CLI argument;args[0]là path tới binary, dùng.skip(1)để lấy real args. Panic nếu argument không phải UTF-8.env::args_os()trảOsString— bắt buộc dùng cho tool xử lý filename để không panic với path non-UTF-8 trên Linux hoặc Windows.env::var(name)trảResult<String, VarError>với 2 variantNotPresent/NotUnicode. Pattern phổ biến:.unwrap_or_else(|_| default)hoặc.expect("msg")cho biến bắt buộc.env::vars()trả iterator(String, String)toàn bộ environment — dùng để debug deployment, filter config, audit trước exec subprocess.env::current_dir()/env::set_current_dir()đọc/ghi CWD của process. Tránhset_current_dirtrong app multi-thread vì thay đổi global.env::temp_dir()dùng tốt;env::home_dir()đã deprecated từ Rust 1.29 — thay bằng cratedirscho home/config/cache/data dir theo platform convention.- Project CLI thực tế dùng
clapvới#[derive(Parser)]: subcommand, flag, validation, help message auto-generated. Đây là chuẩn de-facto của Rust community.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Vì sao
env::args()trảargs[0]là path binary chứ không phải argument đầu tiên? Cách idiomatic để bỏ quaargs[0]? - Khi nào bắt buộc phải dùng
env::args_os()thay vìargs()? Cho ví dụ scenario cụ thể. - Phân biệt
env::var("PORT")vớienv::var_os("PORT"). Trường hợp nào trả error/None mà cái còn lại không? - Vì sao
env::set_current_dir()nguy hiểm trong app multi-thread? Đề xuất pattern thay thế. - Tại sao
env::home_dir()bị deprecated màenv::temp_dir()vẫn còn? Crate gì replace, và lý do API chính thức không "sửa lại" thay vì gỡ? - Khi nào nên dùng
clapthay vì parseenv::args()bằng tay? Liệt kê 3 tính năng mà tự parse khó implement.
Đáp án
- Đó là quy ước Unix cổ điển:
argv[0]luôn là tên hoặc path của program đang chạy. Quy ước này có ích cho self-aware tool (busybox dùngargv[0]để biết đang được gọi nhưlshaycat). Rust giữ nguyên cho nhất quán vớimain(argc, argv)trong C. Cách bỏ qua:env::args().skip(1)trả iterator các argument thật, hoặcenv::args().collect::<Vec<_>>()[1..]. - Khi CLI tool xử lý filename là tham số. Trên Linux, filename là chuỗi byte tuỳ ý không bắt buộc UTF-8 — user có thể có file tên chứa byte
0xfftừ filesystem cũ encode Latin-1, hoặc filename từ archive trans-coding sai. Trên Windows, filename UTF-16 cho phép unpaired surrogate. Dùngargs()sẽ panic khi gặp những input đó. Ví dụ: viết clonecpnhận source và dest path — phảiargs_os(), sau đóPathBuf::from(&arg)giữ nguyên byte sequence để gọifs::copy. var("PORT")trảResult<String, VarError>với 2 variant:NotPresent(biến chưa set) vàNotUnicode(OsString)(set nhưng không phải UTF-8).var_os("PORT")trảOption<OsString>— chỉ phân biệt set/unset, không bao giờ fail vì encoding. Trường hợp khác biệt: nếu biến set thành byte sequence non-UTF8 (hiếm với env standard, nhưng có thể vớiLANG),var()trảErr(NotUnicode)cònvar_os()trảSome(OsString)chứa raw byte. Dùngvar()cho 99% case (env value gần như luôn UTF-8); dùngvar_os()khi cần xử lý path hoặc encoding lạ.set_current_dirthay đổi CWD ở cấp process — toàn bộ thread chia sẻ một CWD. Thread A gọiset_current_dir("/tmp")sẽ làm thread B đang resolve relative path "data.csv" tự nhiên đọc từ/tmp/data.csvthay vì$OLD_CWD/data.csv— race khó debug, không deterministic. Pattern thay thế: (a) Luôn dùng absolute path cho I/O, kết hợpfs::canonicalizeđầu chương trình. (b) Truyền base directory như parameter qua function, dùngPath::joinđể build path local trong function. (c) Trong library, KHÔNG BAO GIỜ gọiset_current_dir— đó là quyết định của application.env::home_dir()bị deprecated vì behavior trên Windows không khớp với Known Folder API chuẩn của Microsoft — hàm dùngHOMEDRIVE/HOMEPATHhoặcUSERPROFILEtheo cách khác với hệ thống mong đợi. Sửa lại sẽ break code hiện có. Standard library quyết định không gánh trách nhiệm này; gỡ khỏi API ổn định để crate ngoài xử lý. Replacement: cratedirs(theo XDG Base Directory + Known Folder + Apple Standard Directories), hoặcdirectories-next.env::temp_dir()vẫn còn vì behavior của nó đơn giản và consistent — chỉ trả vị trí theoTMPDIR/TEMP/TMPenv var, không có disagreement với platform convention.- Dùng
clapngay khi CLI có nhiều hơn 1-2 flag hoặc cần help/version message. Ba tính năng tự parse rất khó: (a) Subcommand kiểugit commit,git pushvới mỗi sub có flag riêng — clap hỗ trợ qua enum với#[derive(Subcommand)]. (b) Auto-generated help/version đẹp, format đúng convention POSIX, kèm usage line, default value, possible value cho enum. (c) Validation + error message hữu ích: parse number, check range, mutually exclusive flag, required-when-X. Tự code phải maintain rất nhiều state machine. Ngoài ra clap còn có shell completion (clap_complete), env var integration (env = "MY_VAR"), config from file — gần như mọi nhu cầu CLI production.
Bài Tiếp Theo
Bài 292: std::process — Command Exec — học cách spawn subprocess từ Rust qua std::process::Command. Builder pattern để set program, argument, environment, working directory; phân biệt spawn(), output(), status(); bắt stdout/stderr qua pipe; chờ process kết thúc và đọc exit code. Đây là API nền tảng cho mọi tool wrapper, build script, automation pipeline viết bằng Rust.
