跳转到内容

Rust 环境变量与进程

每个程序都运行在一个环境里。操作系统会交给它一组有名字的文本值,比如 PATH、HOME 或 PORT,这些就是环境变量(environment variables)。用它们配置程序很简单,不用改代码:同一个二进制文件,在你的笔记本上可以用 8080 端口,在服务器上可以用 80 端口。

程序还生活在其他程序中间。它结束时可以返回一个退出码(exit code),告诉调用者“成功了”还是“失败了”;它自己也可以启动别的程序。在 Rust 里,做这些事的工具都在两个标准库模块里:std::env 和 std::process。不需要任何 crate。

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)。

环境变量永远是文本。想得到数字,就要解析它,而解析也可能失败。常见的写法是:用 .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 = 4
debug = false
▶ 动手试一试

.is_ok() 可以快速地把一个变量当成开关:只要设置了它,开关就是打开的。

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")
None
MYAPP_COLOR=blue
MYAPP_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 里,它们有单独的一页:命令行参数。

程序结束时,会给启动它的那一方返回一个数字。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 退出。

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 process
▶ 动手试一试

echo 收到两个参数,用空格把它们连起来打印出来。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 上都能运行。

真实的程序通常会在启动时一次性读取所有设置,放进一个结构体。每个设置都有合理的默认值,遇到错误的值时会给出清楚的提示,而不是崩溃:

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" 不是数字,改用 3000
Config { host: "127.0.0.1", port: 3000, verbose: true }
▶ 动手试一试

实际使用时,删掉 set_var 那几行,改为在 shell 里设置变量:

Terminal window
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