Rust 环境变量与进程
每个程序都运行在一个环境里。操作系统会交给它一组有名字的文本值,比如 PATH、HOME 或 PORT,这些就是环境变量(environment variables)。用它们配置程序很简单,不用改代码:同一个二进制文件,在你的笔记本上可以用 8080 端口,在服务器上可以用 80 端口。
程序还生活在其他程序中间。它结束时可以返回一个退出码(exit code),告诉调用者“成功了”还是“失败了”;它自己也可以启动别的程序。在 Rust 里,做这些事的工具都在两个标准库模块里:std::env 和 std::process。不需要任何 crate。
用 env::var 读取变量
Section titled “用 env::var 读取变量”std::env::var 接收一个名字,返回一个 Result。变量存在时是 Ok(String),不存在(或者不是合法的 Unicode)时是 Err。变量不存在是很常见的情况,所以 Rust 要求你处理它:
use std::env;
fn main() { // 一个几乎肯定在任何机器上都没有设置的名字。 match env::var("RUSTTUTORIAL_API_KEY") { Ok(value) => println!("找到了:{value}"), Err(e) => println!("无法获取:{e}"), }
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080")); println!("端口:{port}");}输出:
无法获取:environment variable not found端口:8080错误信息 environment variable not found 来自标准库,意思是“找不到环境变量”。想对变量不存在的情况做出反应时,用 match。如果变量不存在只意味着“用默认值”,就用 .unwrap_or(default)。
默认值、数字和开关
Section titled “默认值、数字和开关”环境变量永远是文本。想得到数字,就要解析它,而解析也可能失败。常见的写法是:用 .ok() 把 Result 变成 Option,尝试解析,任何一步出错就退回到默认值:
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}");}输出:
workers = 4debug = false.is_ok() 可以快速地把一个变量当成开关:只要设置了它,开关就是打开的。
var_os、vars 和设置变量
Section titled “var_os、vars 和设置变量”env::var_os 返回 Option<OsString>,而不是 Result。遇到奇怪的字节它也不会失败,所以很适合处理路径。env::vars() 会以 (name, value) 对的形式给出所有变量。
env::set_var 会修改当前进程的某个变量。注意它外面的 unsafe 块:
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(); // env::vars() 的顺序没有保证
for (key, value) in &mine { println!("{key}={value}"); }}输出:
Some("blue")NoneMYAPP_COLOR=blueMYAPP_SIZE=large为什么 set_var 是 unsafe 的? 在很多操作系统上,环境是整个进程共用的一张表。如果一个线程在修改它,而另一个线程(或某个 C 库)正在读取它,程序就可能崩溃。Rust 没法替你检查这件事,所以从 2024 edition 开始,set_var 和 remove_var 都是 unsafe 函数。在 2021 edition 里,不加 unsafe 也能编译,但加上这个块在两个版本里都没问题。安全的规则是:只在 main 的最开头、启动任何线程之前设置变量。更好的做法是根本不改自己的环境。想把变量传给子程序,就用 Command::env(下文会讲)。unsafe 是什么意思,请看 Unsafe Rust。
env::current_dir() 以 PathBuf 的形式返回启动程序时所在的文件夹。像 "config.toml" 这样的相对路径,就是相对于它来解析的。真实的路径取决于你的机器,所以这个例子只打印在任何地方都成立的事实:
use std::env;
fn main() { let dir = env::current_dir().expect("无法读取当前目录"); println!("路径是绝对路径吗?{}", dir.is_absolute());
let config = dir.join("config.toml"); println!("以 config.toml 结尾吗?{}", config.ends_with("config.toml"));}输出:
路径是绝对路径吗?true以 config.toml 结尾吗?true在你自己的机器上打印 dir.display(),就能看到完整的路径。命令行参数也在 std::env 里,它们有单独的一页:命令行参数。
用 process::exit 设置退出码
Section titled “用 process::exit 设置退出码”程序结束时,会给启动它的那一方返回一个数字。0 表示成功,其他任何数字都表示失败。Shell 脚本和 CI 系统都会检查这个数字。std::process::exit(code) 会立刻用这个退出码结束程序:
use std::env;use std::process;
fn main() { println!("正在启动……");
let token = match env::var("RUSTTUTORIAL_TOKEN") { Ok(t) => t, Err(_) => { eprintln!("错误:没有设置 RUSTTUTORIAL_TOKEN"); process::exit(1); } };
println!("使用令牌 {token}");}输出:
正在启动……错误那一行是用 eprintln! 打印的,所以它输出到 stderr(标准错误),而不是 stdout(标准输出)。Playground 会把它和退出状态一起显示在单独的区域里。
有两点要知道。第一,process::exit 会立刻结束程序,所以仍然存活的值的析构函数(Drop)不会运行。第二,你经常并不需要它:如果 main 返回 Result<(), E>,而你返回了 Err,Rust 会替你打印错误,并以退出码 1 退出。
用 Command 运行其他程序
Section titled “用 Command 运行其他程序”std::process::Command 用来启动另一个程序。你一步一步地构建它:先写程序名,再用 .arg() 或 .args() 加上每个参数。然后选择运行方式:
.output()等它运行结束,并捕获它的 stdout 和 stderr。.status()也会等它结束,但让它的输出直接显示在你的终端里。
use std::process::Command;
fn main() { let output = Command::new("echo") .arg("Hello") .arg("from a child process") .output() .expect("无法运行 echo");
let text = String::from_utf8_lossy(&output.stdout); println!("是否成功:{}", output.status.success()); println!("标准输出:{}", text.trim());}输出:
是否成功:true标准输出:Hello from a child processecho 收到两个参数,用空格把它们连起来打印出来。output.stdout 是原始字节(Vec<u8>),因为程序可能打印任何东西。String::from_utf8_lossy 把它转换成文本。
下面用 .status() 读取退出码,再用 .env() 只把变量传给子程序:
use std::process::Command;
fn main() { let status = Command::new("sh") .args(["-c", "exit 3"]) .status() .expect("无法运行 sh");
println!("是否成功:{}", status.success()); println!("退出码:{:?}", status.code());
let output = Command::new("sh") .args(["-c", "echo \"$GREETING, $NAME\""]) .env("GREETING", "你好") .env("NAME", "Ferris") .output() .expect("无法运行 sh"); print!("{}", String::from_utf8_lossy(&output.stdout));}输出:
是否成功:false退出码:Some(3)你好, Ferris在 Windows 上:echo 不是一个独立的程序,而是 cmd shell 的内置命令,而且通常也没有 sh。同样的思路写出来是 Command::new("cmd").args(["/C", "echo Hello"])。上面的例子在 Linux、macOS 和 Rust Playground 上都能运行。
综合运用:从环境读取配置
Section titled “综合运用:从环境读取配置”真实的程序通常会在启动时一次性读取所有设置,放进一个结构体。每个设置都有合理的默认值,遇到错误的值时会给出清楚的提示,而不是崩溃:
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:?} 不是数字,改用 3000"); 3000 }), Err(_) => 3000, };
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose } }}
fn main() { // 模拟用户在运行程序之前可能设置的变量。 unsafe { env::set_var("APP_PORT", "eighty"); env::set_var("APP_VERBOSE", "true"); }
let config = Config::from_env(); println!("{config:?}");}输出:
APP_PORT="eighty" 不是数字,改用 3000Config { host: "127.0.0.1", port: 3000, verbose: true }实际使用时,删掉 set_var 那几行,改为在 shell 里设置变量:
APP_PORT=9000 APP_VERBOSE=1 cargo run(在 Windows PowerShell 里写成:$env:APP_PORT="9000"; cargo run。)
把 env::var 当成返回 String 的函数。 它返回的是 Result,因为变量可能不存在:
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 this这是类型不匹配:期望的是 String,得到的却是 Result<String, VarError>。修法:先想清楚变量不存在意味着什么。要默认值就写 env::var("PORT").unwrap_or(String::from("8080")),要报告问题就用 match。
在 2024 edition 里不加 unsafe 就调用 set_var。 用 cargo new 新建的项目都使用 2024 edition,所以下面这样会失败:
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 function意思是“调用 unsafe 函数 set_var 必须放在 unsafe 块里”。修法:用 unsafe { ... } 包起来,而且只在启动任何线程之前调用;或者给子程序传变量时改用 Command::env。
把整条命令行都塞进 Command::new。 Command::new("echo hello") 会去找一个名字就叫 echo hello 的程序,而它并不存在。.output() 会返回 Err(Os { code: 2, kind: NotFound, message: "No such file or directory" })(找不到这个文件或目录)。修法:程序名放进 new,每个参数都放进 .arg() 或 .args():Command::new("echo").arg("hello")。
env::var("NAME")返回Result<String, VarError>。用match、.unwrap_or()或.ok()处理变量不存在的情况。- 环境变量的值都是文本。数字要自己解析,解析失败时退回到默认值。
- 在 2024 edition 里,
env::set_var是unsafe的,因为环境由所有线程共享。给子程序传变量时,优先用Command::env。 process::exit(code)会带着退出码立刻结束程序:0表示成功,其他数字表示失败。Command::new("program").args([...])运行另一个程序。.output()捕获它打印的内容;.status()给出它的退出码。
小测验
1. 没有设置 PORT 时,env::var("PORT") 返回什么?
2. 为什么在 2024 edition 里 env::set_var 是一个 unsafe 函数?
3. 按照惯例,哪个退出码表示“成功”?
4. 怎样用 Command 运行 ls -l?
得分: 0 / 4
- std::env — 标准库文档(英文):处理环境变量、命令行参数和目录的所有函数。
- 处理环境变量 — Rust 程序设计语言:官方教程的项目从环境中读取一个设置。
- std::process::Command — 标准库文档(英文):启动程序的所有选项,附 Windows 和 Unix 的示例。
- Newly unsafe functions — Edition Guide(英文):官方解释
set_var为什么变成了unsafe。 - 子进程 — 通过例子学 Rust:更多运行程序、使用管道的简短示例。