Biến môi trường và tiến trình trong Rust
Mọi chương trình đều chạy trong một môi trường (environment). Hệ điều hành trao cho nó một tập các giá trị văn bản có tên, như PATH, HOME hay PORT. Đó là các biến môi trường (environment variable). Chúng là cách đơn giản để cấu hình một chương trình mà không phải sửa code: cùng một file chạy có thể dùng cổng 8080 trên laptop của bạn và cổng 80 trên máy chủ.
Một chương trình cũng sống giữa những chương trình khác. Nó có thể kết thúc với một mã thoát (exit code) để báo cho nơi gọi biết “thành công” hay “thất bại”, và nó cũng có thể tự khởi động chương trình khác. Trong Rust, công cụ cho tất cả những việc này nằm trong hai module chuẩn: std::env và std::process. Không cần crate nào cả.
Đọc một biến bằng env::var
Phần tiêu đề “Đọc một biến bằng env::var”std::env::var nhận một cái tên và trả về một Result. Nó là Ok(String) khi biến tồn tại, và Err khi biến không có (hoặc không phải Unicode hợp lệ). Vì thiếu biến là tình huống bình thường, Rust bắt bạn phải xử lý nó:
use std::env;
fn main() { // Một cái tên gần như chắc chắn không được đặt trên máy nào. match env::var("RUSTTUTORIAL_API_KEY") { Ok(value) => println!("Tìm thấy: {value}"), Err(e) => println!("Không có: {e}"), }
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080")); println!("Cổng: {port}");}Kết quả:
Không có: environment variable not foundCổng: 8080“environment variable not found” là thông báo tiếng Anh của Rust: không tìm thấy biến môi trường.
Dùng match khi bạn muốn phản ứng với trường hợp thiếu biến. Dùng .unwrap_or(default) khi thiếu biến đơn giản nghĩa là “dùng giá trị mặc định”.
Giá trị mặc định, số và cờ bật tắt
Phần tiêu đề “Giá trị mặc định, số và cờ bật tắt”Biến môi trường luôn là văn bản. Muốn có một con số, bạn phải chuyển đổi, và việc chuyển đổi cũng có thể thất bại. Một khuôn mẫu phổ biến là: biến Result thành Option bằng .ok(), thử chuyển đổi, và lùi về giá trị mặc định nếu có gì trục trặc:
use std::env;
fn main() { let workers: u32 = env::var("RUSTTUTORIAL_WORKERS") .ok() .and_then(|text| text.parse().ok()) .unwrap_or(4);
let debug = env::var("RUSTTUTORIAL_DEBUG").is_ok();
println!("workers = {workers}"); println!("debug = {debug}");}Kết quả:
workers = 4debug = false.is_ok() là cách nhanh để coi một biến như cờ bật/tắt: chỉ cần biến được đặt, dù giá trị gì, là cờ được bật.
var_os, vars và đặt giá trị cho biến
Phần tiêu đề “var_os, vars và đặt giá trị cho biến”env::var_os trả về Option<OsString> thay vì một Result. Nó không bao giờ thất bại vì những byte lạ, nên rất tiện cho đường dẫn. env::vars() cho bạn mọi biến dưới dạng các cặp (tên, giá trị).
env::set_var thay đổi một biến cho tiến trình hiện tại. Hãy để ý khối unsafe bao quanh nó:
use std::env;
fn main() { unsafe { env::set_var("MYAPP_COLOR", "blue"); env::set_var("MYAPP_SIZE", "large"); }
println!("{:?}", env::var_os("MYAPP_COLOR")); println!("{:?}", env::var_os("MYAPP_MISSING"));
let mut mine: Vec<(String, String)> = env::vars() .filter(|(key, _)| key.starts_with("MYAPP_")) .collect(); mine.sort(); // thứ tự của env::vars() không được đảm bảo
for (key, value) in &mine { println!("{key}={value}"); }}Kết quả:
Some("blue")NoneMYAPP_COLOR=blueMYAPP_SIZE=largeVì sao set_var lại unsafe? Trên nhiều hệ điều hành, môi trường là một bảng dùng chung cho cả tiến trình. Nếu một luồng thay đổi nó trong lúc một luồng khác (hoặc một thư viện C) đang đọc, chương trình có thể bị sập. Rust không kiểm tra điều này giúp bạn được, nên từ edition 2024, set_var và remove_var là các hàm unsafe. Ở edition 2021 chúng vẫn biên dịch được mà không cần unsafe, nhưng viết sẵn khối unsafe thì chạy được ở cả hai. Quy tắc an toàn: chỉ đặt biến ngay ở đầu main, trước khi bạn khởi động bất kỳ luồng nào. Tốt hơn nữa, đừng thay đổi môi trường của chính mình. Để đưa một biến cho chương trình con, hãy dùng Command::env (xem bên dưới). Xem Unsafe Rust để biết unsafe nghĩa là gì.
Thư mục hiện tại
Phần tiêu đề “Thư mục hiện tại”env::current_dir() trả về thư mục mà chương trình được khởi động từ đó, dưới dạng một PathBuf. Những đường dẫn tương đối như "config.toml" được tính dựa trên nó. Đường dẫn thật phụ thuộc vào máy của bạn, nên ví dụ này chỉ in ra những điều đúng ở mọi nơi:
use std::env;
fn main() { let dir = env::current_dir().expect("không đọc được thư mục hiện tại"); println!("Đường dẫn có phải tuyệt đối không? {}", dir.is_absolute());
let config = dir.join("config.toml"); println!("Kết thúc bằng config.toml? {}", config.ends_with("config.toml"));}Kết quả:
Đường dẫn có phải tuyệt đối không? trueKết thúc bằng config.toml? trueHãy in dir.display() trên máy của bạn để thấy đường dẫn đầy đủ. Tham số dòng lệnh cũng nằm trong std::env; chúng có trang riêng: Tham số dòng lệnh.
Mã thoát với process::exit
Phần tiêu đề “Mã thoát với process::exit”Khi kết thúc, một chương trình trả về một con số cho bên đã khởi động nó. 0 nghĩa là thành công; mọi số khác nghĩa là thất bại. Script shell và hệ thống CI kiểm tra con số này. std::process::exit(code) dừng chương trình ngay lập tức với mã đó:
use std::env;use std::process;
fn main() { println!("Đang khởi động...");
let token = match env::var("RUSTTUTORIAL_TOKEN") { Ok(t) => t, Err(_) => { eprintln!("lỗi: chưa đặt RUSTTUTORIAL_TOKEN"); process::exit(1); } };
println!("Dùng token {token}");}Kết quả:
Đang khởi động...Dòng báo lỗi được in bằng eprintln!, nên nó đi vào stderr, không phải stdout. Playground hiện nó ở một khu vực riêng, cùng với trạng thái thoát.
Có hai điều nên biết. Thứ nhất, process::exit kết thúc chương trình ngay lập tức, nên các destructor (Drop) của những giá trị còn sống sẽ không chạy. Thứ hai, thường thì bạn không cần nó: nếu main trả về Result<(), E> và bạn trả về Err, Rust sẽ in lỗi và thoát với mã 1 giúp bạn.
Chạy chương trình khác với Command
Phần tiêu đề “Chạy chương trình khác với Command”std::process::Command khởi động một chương trình khác. Bạn dựng nó từng bước: tên chương trình, rồi từng tham số bằng .arg() hoặc .args(). Sau đó bạn chọn cách chạy:
.output()chờ nó chạy xong và thu lại stdout lẫn stderr của nó..status()chờ nó, nhưng để kết quả in thẳng ra terminal của bạn.
use std::process::Command;
fn main() { let output = Command::new("echo") .arg("Xin chào") .arg("từ một tiến trình con") .output() .expect("không chạy được echo");
let text = String::from_utf8_lossy(&output.stdout); println!("thành công: {}", output.status.success()); println!("stdout: {}", text.trim());}Kết quả:
thành công: truestdout: Xin chào từ một tiến trình conoutput.stdout là các byte thô (Vec<u8>), vì một chương trình có thể in ra bất cứ thứ gì. String::from_utf8_lossy biến chúng thành văn bản.
Đây là .status() đọc một mã thoát, và .env() truyền một biến chỉ cho tiến trình con:
use std::process::Command;
fn main() { let status = Command::new("sh") .args(["-c", "exit 3"]) .status() .expect("không chạy được sh");
println!("thành công: {}", status.success()); println!("mã: {:?}", status.code());
let output = Command::new("sh") .args(["-c", "echo \"$GREETING, $NAME\""]) .env("GREETING", "Chào") .env("NAME", "Ferris") .output() .expect("không chạy được sh"); print!("{}", String::from_utf8_lossy(&output.stdout));}Kết quả:
thành công: falsemã: Some(3)Chào, FerrisTrên Windows: ở đó echo không phải một chương trình riêng; nó được tích hợp sẵn trong shell cmd, và thường không có sh. Cùng ý tưởng đó trông như Command::new("cmd").args(["/C", "echo Hello"]). Các ví dụ trên chạy được trên Linux, macOS và Rust Playground.
Ghép tất cả lại: cấu hình từ môi trường
Phần tiêu đề “Ghép tất cả lại: cấu hình từ môi trường”Một chương trình thật thường đọc toàn bộ thiết lập một lần, lúc khởi động, vào một struct. Mỗi thiết lập có một giá trị mặc định hợp lý, và một giá trị sai sẽ cho ra thông báo rõ ràng thay vì làm sập chương trình:
use std::env;
#[derive(Debug)]struct Config { host: String, port: u16, verbose: bool,}
impl Config { fn from_env() -> Config { let host = env::var("APP_HOST").unwrap_or_else(|_| String::from("127.0.0.1"));
let port = match env::var("APP_PORT") { Ok(text) => text.parse().unwrap_or_else(|_| { println!("APP_PORT={text:?} không phải là số, dùng 3000"); 3000 }), Err(_) => 3000, };
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose } }}
fn main() { // Mô phỏng những gì người dùng có thể đặt trước khi chạy chương trình. unsafe { env::set_var("APP_PORT", "tám mươi"); env::set_var("APP_VERBOSE", "true"); }
let config = Config::from_env(); println!("{config:?}");}Kết quả:
APP_PORT="tám mươi" không phải là số, dùng 3000Config { host: "127.0.0.1", port: 3000, verbose: true }Khi dùng thật, hãy xoá các dòng set_var và đặt biến trong shell của bạn:
APP_PORT=9000 APP_VERBOSE=1 cargo run(Trên Windows PowerShell: $env:APP_PORT="9000"; cargo run.)
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Coi env::var như thể nó trả về một String. Nó trả về một Result, vì biến có thể không tồn tại:
let port: String = env::var("PORT");error[E0308]: mismatched types --> src/main.rs:4:24 |4 | let port: String = env::var("PORT"); | ------ ^^^^^^^^^^^^^^^^ expected `String`, found `Result<String, VarError>` | | | expected due to thisRustc báo kiểu không khớp: nó mong một String nhưng nhận được Result<String, VarError>. Cách sửa: quyết định xem thiếu biến thì nghĩa là gì. Dùng env::var("PORT").unwrap_or(String::from("8080")) để có giá trị mặc định, hoặc match để báo lỗi.
Gọi set_var mà không có unsafe ở edition 2024. Các dự án mới tạo bằng cargo new dùng edition 2024, nên đoạn này thất bại:
error[E0133]: call to unsafe function `set_var` is unsafe and requires unsafe block --> src/main.rs:2:5 |2 | std::env::set_var("MODE", "fast"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe functionRustc báo rằng gọi hàm unsafe set_var cần nằm trong một khối unsafe. Cách sửa: bọc nó trong unsafe { ... } và chỉ gọi trước khi có luồng nào khởi động, hoặc dùng Command::env cho chương trình con.
Đặt cả dòng lệnh vào Command::new. Command::new("echo hello") tìm một chương trình có tên đúng y là echo hello, mà chương trình đó không tồn tại. .output() trả về Err(Os { code: 2, kind: NotFound, message: "No such file or directory" }) (không có file hay thư mục như vậy). Cách sửa: tên chương trình đặt trong new, còn mỗi tham số đặt trong .arg() hoặc .args(): Command::new("echo").arg("hello").
Tóm tắt
Phần tiêu đề “Tóm tắt”env::var("NAME")trả vềResult<String, VarError>. Xử lý trường hợp thiếu biến bằngmatch,.unwrap_or()hoặc.ok().- Giá trị môi trường là văn bản. Hãy tự chuyển đổi sang số và lùi về giá trị mặc định khi chuyển đổi thất bại.
env::set_varlàunsafeở edition 2024 vì môi trường được mọi luồng dùng chung. Ưu tiênCommand::envcho chương trình con.process::exit(code)kết thúc chương trình ngay với một mã thoát:0là thành công, mọi số khác là thất bại.Command::new("program").args([...])chạy một chương trình khác..output()thu lại những gì nó in ra;.status()cho bạn mã thoát của nó.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. env::var("PORT") trả về gì khi PORT chưa được đặt?
2. Vì sao env::set_var là hàm unsafe ở edition 2024?
3. Theo truyền thống, mã thoát nào nghĩa là “thành công”?
4. Bạn chạy ls -l bằng Command thế nào?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- std::env — Rust standard library (tiếng Anh): mọi hàm cho biến môi trường, tham số dòng lệnh và thư mục.
- Working with Environment Variables — The Rust Book (tiếng Anh): dự án trong sách đọc một thiết lập từ môi trường.
- std::process::Command (tiếng Anh): mọi tuỳ chọn để khởi động chương trình, với ví dụ cho Windows và Unix.
- Newly unsafe functions — Edition Guide (tiếng Anh): giải thích chính thức vì sao
set_vartrở thànhunsafe. - Child processes — Rust by Example (tiếng Anh): thêm ví dụ ngắn về chạy chương trình và dùng pipe.