コンテンツにスキップ

Rustの環境変数とプロセス

どんなプログラムも、ある環境の中で動いています。OSはプログラムに、PATH、HOME、PORT のような名前付きのテキストの値をひとそろい渡します。これが環境変数(environment variable)です。環境変数を使うと、コードを書き換えずにプログラムの設定を変えられます。同じバイナリが、手元のノートPCではポート8080を、サーバーではポート80を使う、といったことができるわけです。

また、プログラムはほかのプログラムに囲まれて動いています。終わるときには、呼び出し元に「うまくいった」か「失敗した」かを伝える終了コード(exit code)を返せますし、自分でほかのプログラムを起動することもできます。Rustでは、これらの道具は標準ライブラリの2つのモジュール、std::env と std::process にそろっています。クレートは必要ありません。

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(環境変数が見つからない)の部分は、Rustの標準ライブラリが用意しているエラーメッセージなので英語のままです。

変数がないケースに対応したいときは 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 は Result ではなく Option<OsString> を返します。おかしなバイト列でも失敗しないので、パスを扱うときに便利です。env::vars() は、すべての変数を (名前, 値) のペアとして返します。

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 なのでしょうか? 多くのOSでは、環境変数はプロセス全体で共有される1つの表です。あるスレッドがそれを書き換えている最中に、別のスレッド(やCのライブラリ)が読むと、プログラムがクラッシュすることがあります。Rustにはこれをチェックできないので、edition 2024 からは set_var と remove_var が unsafe な関数になりました。edition 2021では unsafe なしでもコンパイルできますが、ブロックを書いておけばどちらでも動きます。安全に使うルールは、変数を設定するのは main のいちばん最初、スレッドを起動する前だけにすることです。さらに言えば、自分自身の環境はそもそも変更しないのがいちばんです。子プログラムに変数を渡したいなら、後で紹介する Command::env を使いましょう。unsafe の意味については Rustのunsafe を見てください。

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 にありますが、こちらは専用のページがあります。Rustのコマンドライン引数を見てください。

プログラムは終わるときに、起動した側へ数値を1つ返します。0 は成功、それ以外は失敗という意味です。シェルスクリプトや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! で表示しているので、stdoutではなくstderr(標準エラー出力)に出ます。Playgroundでは、終了ステータスといっしょに別の欄に表示されます。

知っておきたいことが2つあります。1つ目に、process::exit はプログラムを即座に終わらせるので、まだ生きている値のデストラクタ(Drop)は実行されません。2つ目に、たいていは使わなくても済みます。main が Result<(), E> を返すようにして Err を返せば、Rustがエラーを表示して、コード 1 で終了してくれます。

Commandでほかのプログラムを実行する

Section titled “Commandでほかのプログラムを実行する”

std::process::Command は、別のプログラムを起動します。プログラム名を指定し、引数を .arg() や .args() で1つずつ追加して、少しずつ組み立てていきます。それから実行のしかたを選びます。

  • .output() は終わるまで待ち、stdoutとstderrをキャプチャ(取得)します。
  • .status() も終わるまで待ちますが、出力はそのままターミナルに表示されます。
use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("こんにちは")
.arg("子プロセスより")
.output()
.expect("echoを実行できませんでした");
let text = String::from_utf8_lossy(&output.stdout);
println!("成功: {}", output.status.success());
println!("stdout: {}", text.trim());
}

実行結果:

成功: true
stdout: こんにちは 子プロセスより
▶ 実際に試してみよう

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", "Hi")
.env("NAME", "Ferris")
.output()
.expect("shを実行できませんでした");
print!("{}", String::from_utf8_lossy(&output.stdout));
}

実行結果:

成功: false
コード: Some(3)
Hi, Ferris
▶ 実際に試してみよう

Windowsの場合: Windowsでは echo は独立したプログラムではなく cmd シェルに組み込まれたコマンドで、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" は数値ではないので、3000を使います
Config { host: "127.0.0.1", port: 3000, verbose: true }
▶ 実際に試してみよう

実際に使うときは set_var の行を消して、代わりにシェルで変数を設定します。

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 です。

edition 2024で unsafe なしに set_var を呼ぶ。 cargo new で作った新しいプロジェクトはedition 2024を使うので、これはエラーになります。

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な関数の呼び出しには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() で処理する。
  • 環境変数の値はテキスト。数値は自分でパースし、失敗したらデフォルト値に戻す。
  • 環境変数はすべてのスレッドで共有されているため、edition 2024では env::set_var は unsafe。子プログラムには Command::env を使うのがおすすめ。
  • process::exit(code) は終了コードを返してすぐにプログラムを終わらせる。0 は成功、それ以外は失敗。
  • Command::new("program").args([...]) でほかのプログラムを実行する。.output() は出力をキャプチャし、.status() は終了コードを返す。

理解度チェック

1. PORT が設定されていないとき、env::var("PORT") は何を返しますか?

2. edition 2024で env::set_var が unsafe な関数になっているのはなぜですか?

3. 伝統的に「成功」を意味する終了コードはどれですか?

4. Command で ls -l を実行するにはどう書きますか?

スコア: 0 / 4