Danh sách bài viết

Bài 291: std::env — args, vars, current_dir

Bài 291 của series Rust Cơ Bản — module std::env là cầu nối giữa chương trình Rust và môi trường thực thi: command-line argument được truyền vào, environment variable (HOME, PATH, DATABASE_URL) do shell set sẵn, và current working directory mà process đang đứng. Học các hàm cốt lõi env::args(), env::args_os() cho path không phải UTF-8, env::var() trả Result, env::vars() duyệt toàn bộ environment, env::current_dir() và set_current_dir(), env::temp_dir(), và lý do env::home_dir() đã deprecated từ Rust 1.29 — phải dùng crate dirs. Cuối bài preview clap, crate de-facto cho CLI parser chuyên nghiệp.

10/06/2026
9 phút đọc
1 lượt xem
1

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ết args[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ợp unwrap_or_else cho fallback.
  • Duyệt toàn bộ env qua env::vars(), áp dụng filter/collect cho config inspector.
  • Truy vấn và thay đổi current working directory với env::current_dir()env::set_current_dir().
  • Biết env::temp_dir() trả thư mục tạm theo OS, và lý do env::home_dir() bị deprecated — chuyển sang crate dirs.
  • Hiểu lý do project thực tế dùng clap thay vì parse env::args() bằng tay.
2

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.

3

env::args_os() Cho Non-UTF8 Path

Anh em song sinh của args()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, rm phả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
    }
}
4

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_varunsafe để 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.

5

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.

6

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.

7

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.

8

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ế.

9

Tổng Kết

  • env::args() trả iterator String cá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 variant NotPresent/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ánh set_current_dir trong 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 crate dirs cho home/config/cache/data dir theo platform convention.
  • Project CLI thực tế dùng clap với #[derive(Parser)]: subcommand, flag, validation, help message auto-generated. Đây là chuẩn de-facto của Rust community.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Vì sao env::args() trả args[0] là path binary chứ không phải argument đầu tiên? Cách idiomatic để bỏ qua args[0]?
  2. Khi nào bắt buộc phải dùng env::args_os() thay vì args()? Cho ví dụ scenario cụ thể.
  3. Phân biệt env::var("PORT") với env::var_os("PORT"). Trường hợp nào trả error/None mà cái còn lại không?
  4. Vì sao env::set_current_dir() nguy hiểm trong app multi-thread? Đề xuất pattern thay thế.
  5. 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ỡ?
  6. Khi nào nên dùng clap thay vì parse env::args() bằng tay? Liệt kê 3 tính năng mà tự parse khó implement.
Đáp án
  1. Đó 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ùng argv[0] để biết đang được gọi như ls hay cat). Rust giữ nguyên cho nhất quán với main(argc, argv) trong C. Cách bỏ qua: env::args().skip(1) trả iterator các argument thật, hoặc env::args().collect::<Vec<_>>()[1..].
  2. 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 0xff từ 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ùng args() sẽ panic khi gặp những input đó. Ví dụ: viết clone cp nhận source và dest path — phải args_os(), sau đó PathBuf::from(&arg) giữ nguyên byte sequence để gọi fs::copy.
  3. 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ới LANG), var() trả Err(NotUnicode) còn var_os() trả Some(OsString) chứa raw byte. Dùng var() cho 99% case (env value gần như luôn UTF-8); dùng var_os() khi cần xử lý path hoặc encoding lạ.
  4. set_current_dir thay đổi CWD ở cấp process — toàn bộ thread chia sẻ một CWD. Thread A gọi set_current_dir("/tmp") sẽ làm thread B đang resolve relative path "data.csv" tự nhiên đọc từ /tmp/data.csv thay 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ợp fs::canonicalize đầu chương trình. (b) Truyền base directory như parameter qua function, dùng Path::join để build path local trong function. (c) Trong library, KHÔNG BAO GIỜ gọi set_current_dir — đó là quyết định của application.
  5. 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ùng HOMEDRIVE/HOMEPATH hoặc USERPROFILE theo 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: crate dirs (theo XDG Base Directory + Known Folder + Apple Standard Directories), hoặc directories-next. env::temp_dir() vẫn còn vì behavior của nó đơn giản và consistent — chỉ trả vị trí theo TMPDIR/TEMP/TMP env var, không có disagreement với platform convention.
  6. Dùng clap ngay 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ểu git commit, git push vớ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.
11

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.