Переменные окружения и процессы в Rust
Каждая программа работает внутри окружения (environment). Операционная система передаёт ей набор именованных текстовых значений, например PATH, HOME или PORT. Это переменные окружения. Они позволяют просто настроить программу, не меняя её код: одна и та же программа может слушать порт 8080 на твоём ноутбуке и порт 80 на сервере.
А ещё программа живёт среди других программ. Она может завершиться с кодом выхода (exit code), который говорит вызвавшему «получилось» или «не получилось», и может сама запускать другие программы. В Rust всё это лежит в двух стандартных модулях: std::env и std::process. Крейты не нужны.
Читаем переменную через env::var
Заголовок раздела «Читаем переменную через env::var»std::env::var принимает имя и возвращает Result. Это Ok(String), если переменная есть, и Err, если её нет (или она не является корректным Unicode). Отсутствующая переменная — обычная ситуация, поэтому 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), когда отсутствие просто значит «бери значение по умолчанию».
Значения по умолчанию, числа и флаги
Заголовок раздела «Значения по умолчанию, числа и флаги»Переменные окружения — это всегда текст. Чтобы получить число, его нужно разобрать, а разбор тоже может не получиться. Частый приём: превратить Result в Option через .ok(), попытаться разобрать и, если что-то пошло не так, взять значение по умолчанию:
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 и установка переменных
Заголовок раздела «var_os, vars и установка переменных»env::var_os возвращает Option<OsString> вместо Result. Он никогда не спотыкается о странные байты, поэтому удобен для путей. 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")NoneMYAPP_COLOR=blueMYAPP_SIZE=largeПочему set_var небезопасна? Во многих операционных системах окружение — это одна общая таблица на весь процесс. Если один поток меняет её, пока другой поток (или библиотека на C) её читает, программа может упасть. Rust не может проверить это за тебя, поэтому начиная с редакции 2024 set_var и remove_var — это unsafe-функции. В редакции 2021 они ещё компилируются без unsafe, но блок unsafe работает в обеих. Безопасное правило: задавай переменные только в самом начале main, до запуска любых потоков. А ещё лучше — вообще не меняй собственное окружение. Чтобы передать переменную дочерней программе, используй Command::env (показано ниже). Что значит unsafe, объясняется на странице 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, и у них есть своя страница: Аргументы командной строки.
Коды выхода и process::exit
Заголовок раздела «Коды выхода и 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
Заголовок раздела «Запускаем другие программы через Command»std::process::Command запускает другую программу. Её собирают по шагам: имя программы, затем каждый аргумент через .arg() или .args(). Потом выбираешь, как запускать:
.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());}Вывод:
успех: truestdout: Привет из дочернего процесса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", "Феррис") .output() .expect("не удалось запустить sh"); print!("{}", String::from_utf8_lossy(&output.stdout));}Вывод:
успех: falseкод: Some(3)Привет, ФеррисВ Windows: echo там не отдельная программа, а встроенная команда оболочки cmd, а 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", "восемьдесят"); env::set_var("APP_VERBOSE", "true"); }
let config = Config::from_env(); println!("{config:?}");}Вывод:
APP_PORT="восемьдесят" — не число, беру 3000Config { host: "127.0.0.1", port: 3000, verbose: true }В настоящей программе удали строки с set_var и задай переменные в своей оболочке:
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 — чтобы сообщить о проблеме.
Вызывать set_var без unsafe в редакции 2024. Новые проекты, созданные через cargo new, используют редакцию 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Компилятор говорит, что вызов небезопасной функции 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().- Значения окружения — это текст. Числа разбирай сам и бери значение по умолчанию, если разбор не удался.
env::set_varв редакции 2024 —unsafe, потому что окружение общее для всех потоков. Для дочерних программ лучшеCommand::env.process::exit(code)сразу завершает программу с кодом выхода:0— успех, всё остальное — неудача.Command::new("program").args([...])запускает другую программу..output()перехватывает её вывод,.status()даёт код выхода.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что возвращает env::var("PORT"), если PORT не задана?
2. Почему env::set_var в редакции 2024 — unsafe-функция?
3. Какой код выхода традиционно означает «успех»?
4. Как запустить ls -l через Command?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- std::env — стандартная библиотека Rust (на английском): все функции для переменных, аргументов и папок.
- Работа с переменными среды — книга «Язык программирования Rust»: проект из книги читает настройку из окружения (перевод на русский).
- std::process::Command (на английском): все способы запускать программы, с примерами для Windows и Unix.
- Newly unsafe functions — Edition Guide (на английском): официальное объяснение, почему
set_varсталаunsafe. - Дочерние процессы — Rust на примерах: ещё короткие примеры запуска программ и использования каналов (pipes) (перевод на русский).