Lewati ke konten

Environment Variable dan Proses di Rust

Setiap program berjalan di dalam sebuah environment (lingkungan). Sistem operasi memberinya sekumpulan nilai teks yang punya nama, seperti PATH, HOME, atau PORT. Inilah yang disebut environment variable. Environment variable adalah cara sederhana untuk mengonfigurasi program tanpa mengubah kodenya: binary yang sama bisa memakai port 8080 di laptopmu dan port 80 di server.

Sebuah program juga hidup berdampingan dengan program lain. Program bisa selesai dengan exit code yang memberi tahu pemanggilnya “berhasil” atau “gagal”, dan program juga bisa menjalankan program lain. Di Rust, alat untuk semua ini ada di dua modul standar: std::env dan std::process. Tidak perlu crate tambahan.

std::env::var menerima sebuah nama dan mengembalikan Result. Hasilnya Ok(String) kalau variabelnya ada, dan Err kalau tidak ada (atau bukan Unicode yang valid). Karena variabel yang tidak ada adalah situasi yang wajar, Rust mewajibkanmu menanganinya:

use std::env;
fn main() {
// Nama yang hampir pasti tidak di-set di komputer mana pun.
match env::var("RUSTTUTORIAL_API_KEY") {
Ok(value) => println!("Ketemu: {value}"),
Err(e) => println!("Tidak tersedia: {e}"),
}
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080"));
println!("Port: {port}");
}

Output:

Tidak tersedia: environment variable not found
Port: 8080
▶ Coba sendiri

“environment variable not found” adalah pesan bawaan Rust (artinya environment variable tidak ditemukan), jadi tetap berbahasa Inggris.

Pakai match kalau kamu ingin bereaksi terhadap kasus variabel yang tidak ada. Pakai .unwrap_or(default) kalau variabel yang tidak ada cukup berarti “pakai nilai default”.

Environment variable selalu berupa teks. Untuk mendapatkan angka, kamu harus mem-parse-nya, dan parsing juga bisa gagal. Pola yang umum adalah: ubah Result menjadi Option dengan .ok(), coba parse, lalu pakai nilai default kalau ada yang salah:

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}");
}

Output:

workers = 4
debug = false
▶ Coba sendiri

.is_ok() adalah cara cepat untuk memperlakukan sebuah variabel sebagai flag nyala/mati: asalkan variabelnya di-set, flag-nya menyala.

env::var_os mengembalikan Option<OsString>, bukan Result. Fungsi ini tidak pernah gagal karena byte yang aneh, jadi cocok untuk path. env::vars() memberimu semua variabel sebagai pasangan (nama, nilai).

env::set_var mengubah sebuah variabel untuk proses yang sedang berjalan. Perhatikan blok unsafe di sekitarnya:

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(); // urutan env::vars() tidak dijamin
for (key, value) in &mine {
println!("{key}={value}");
}
}

Output:

Some("blue")
None
MYAPP_COLOR=blue
MYAPP_SIZE=large
▶ Coba sendiri

Kenapa set_var itu unsafe? Di banyak sistem operasi, environment adalah satu tabel bersama untuk seluruh proses. Kalau satu thread mengubahnya sementara thread lain (atau library C) sedang membacanya, program bisa crash. Rust tidak bisa memeriksa hal ini untukmu, jadi sejak edition 2024, set_var dan remove_var adalah fungsi unsafe. Di edition 2021, keduanya masih bisa di-compile tanpa unsafe, tapi menulis bloknya tetap berfungsi di kedua edisi. Aturan amannya: hanya ubah variabel di awal main, sebelum kamu menjalankan thread apa pun. Lebih baik lagi, jangan ubah environment milikmu sendiri sama sekali. Untuk memberikan variabel ke program anak (child), pakai Command::env (dibahas di bawah). Lihat Unsafe Rust untuk memahami arti unsafe.

env::current_dir() mengembalikan folder tempat program dijalankan, sebagai PathBuf. Path relatif seperti "config.toml" dihitung dari folder ini. Path aslinya bergantung pada komputermu, jadi contoh ini hanya mencetak hal-hal yang benar di mana saja:

use std::env;
fn main() {
let dir = env::current_dir().expect("tidak bisa membaca direktori saat ini");
println!("Apakah path-nya absolut? {}", dir.is_absolute());
let config = dir.join("config.toml");
println!("Berakhiran config.toml? {}", config.ends_with("config.toml"));
}

Output:

Apakah path-nya absolut? true
Berakhiran config.toml? true
▶ Coba sendiri

Cetak dir.display() di komputermu sendiri untuk melihat path lengkapnya. Argumen command line juga ada di std::env; topik itu punya halaman sendiri: Argumen command line.

Saat program selesai, ia mengembalikan sebuah angka kepada siapa pun yang menjalankannya. 0 berarti berhasil; angka lain berarti gagal. Skrip shell dan sistem CI memeriksa angka ini. std::process::exit(code) langsung menghentikan program dengan kode tersebut:

use std::env;
use std::process;
fn main() {
println!("Memulai...");
let token = match env::var("RUSTTUTORIAL_TOKEN") {
Ok(t) => t,
Err(_) => {
eprintln!("error: RUSTTUTORIAL_TOKEN belum di-set");
process::exit(1);
}
};
println!("Memakai token {token}");
}

Output:

Memulai...
▶ Coba sendiri

Baris error dicetak dengan eprintln!, jadi masuknya ke stderr, bukan stdout. Playground menampilkannya di area terpisah, bersama dengan exit status-nya.

Ada dua hal yang perlu kamu tahu. Pertama, process::exit langsung mengakhiri program, jadi destructor (Drop) untuk nilai-nilai yang masih hidup tidak dijalankan. Kedua, sering kali kamu tidak membutuhkannya: kalau main mengembalikan Result<(), E> dan kamu mengembalikan Err, Rust akan mencetak error-nya dan keluar dengan kode 1 untukmu.

std::process::Command menjalankan program lain. Kamu menyusunnya langkah demi langkah: nama program, lalu setiap argumen dengan .arg() atau .args(). Setelah itu, pilih cara menjalankannya:

  • .output() menunggu program selesai dan menangkap stdout dan stderr-nya.
  • .status() juga menunggu, tapi membiarkan output-nya langsung tampil di terminalmu.
use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("Halo")
.arg("dari proses anak")
.output()
.expect("gagal menjalankan echo");
let text = String::from_utf8_lossy(&output.stdout);
println!("berhasil: {}", output.status.success());
println!("stdout: {}", text.trim());
}

Output:

berhasil: true
stdout: Halo dari proses anak
▶ Coba sendiri

output.stdout berupa byte mentah (Vec<u8>), karena sebuah program bisa mencetak apa saja. String::from_utf8_lossy mengubahnya menjadi teks.

Berikut contoh .status() untuk membaca exit code, dan .env() untuk mengirim variabel hanya ke program anak:

use std::process::Command;
fn main() {
let status = Command::new("sh")
.args(["-c", "exit 3"])
.status()
.expect("gagal menjalankan sh");
println!("berhasil: {}", status.success());
println!("kode: {:?}", status.code());
let output = Command::new("sh")
.args(["-c", "echo \"$GREETING, $NAME\""])
.env("GREETING", "Hai")
.env("NAME", "Ferris")
.output()
.expect("gagal menjalankan sh");
print!("{}", String::from_utf8_lossy(&output.stdout));
}

Output:

berhasil: false
kode: Some(3)
Hai, Ferris
▶ Coba sendiri

Di Windows: echo bukan program terpisah; perintah itu sudah tertanam di shell cmd, dan sh biasanya tidak ada. Ide yang sama kira-kira ditulis seperti Command::new("cmd").args(["/C", "echo Halo"]). Contoh-contoh di atas berjalan di Linux, macOS, dan Rust Playground.

Menggabungkan semuanya: konfigurasi dari environment

Section titled “Menggabungkan semuanya: konfigurasi dari environment”

Program sungguhan biasanya membaca semua pengaturannya sekali saja, saat mulai berjalan, ke dalam sebuah struct. Setiap pengaturan punya nilai default yang masuk akal, dan nilai yang salah menghasilkan pesan yang jelas, bukan crash:

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:?} bukan angka, memakai 3000");
3000
}),
Err(_) => 3000,
};
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose }
}
}
fn main() {
// Simulasikan apa yang mungkin di-set pengguna sebelum menjalankan program.
unsafe {
env::set_var("APP_PORT", "delapan puluh");
env::set_var("APP_VERBOSE", "true");
}
let config = Config::from_env();
println!("{config:?}");
}

Output:

APP_PORT="delapan puluh" bukan angka, memakai 3000
Config { host: "127.0.0.1", port: 3000, verbose: true }
▶ Coba sendiri

Dalam pemakaian sungguhan, hapus baris-baris set_var dan set variabelnya di shell kamu:

Terminal window
APP_PORT=9000 APP_VERBOSE=1 cargo run

(Di Windows PowerShell: $env:APP_PORT="9000"; cargo run.)

Memperlakukan env::var seolah-olah mengembalikan String. Fungsi ini mengembalikan Result, karena variabelnya mungkin tidak ada:

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

Compiler bilang tipenya tidak cocok: yang diharapkan String, tapi yang didapat Result<String, VarError>. Solusinya: tentukan apa arti variabel yang tidak ada. Pakai env::var("PORT").unwrap_or(String::from("8080")) untuk nilai default, atau match untuk melaporkannya.

Memanggil set_var tanpa unsafe di edition 2024. Proyek baru yang dibuat dengan cargo new memakai edition 2024, jadi kode ini gagal:

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

Artinya, memanggil fungsi unsafe set_var membutuhkan blok unsafe. Solusinya: bungkus dengan unsafe { ... } dan panggil hanya sebelum ada thread yang berjalan, atau pakai Command::env untuk program anak.

Menaruh seluruh baris perintah di Command::new. Command::new("echo hello") mencari program yang namanya persis echo hello, yang tentu saja tidak ada. .output() akan mengembalikan Err(Os { code: 2, kind: NotFound, message: "No such file or directory" }) (file atau direktori tidak ditemukan). Solusinya: nama program ditaruh di new, dan setiap argumen ditaruh di .arg() atau .args(): Command::new("echo").arg("hello").

  • env::var("NAME") mengembalikan Result<String, VarError>. Tangani kasus variabel yang tidak ada dengan match, .unwrap_or(), atau .ok().
  • Nilai environment berupa teks. Parse sendiri angkanya, dan pakai nilai default kalau parsing gagal.
  • env::set_var bersifat unsafe di edition 2024 karena environment dipakai bersama oleh semua thread. Utamakan Command::env untuk program anak.
  • process::exit(code) langsung mengakhiri program dengan sebuah exit code: 0 berarti berhasil, angka lain berarti gagal.
  • Command::new("program").args([...]) menjalankan program lain. .output() menangkap apa yang dicetaknya; .status() memberimu exit code-nya.

Kuis singkat

1. Apa yang dikembalikan env::var("PORT") kalau PORT tidak di-set?

2. Kenapa env::set_var menjadi fungsi unsafe di edition 2024?

3. Exit code mana yang secara tradisional berarti "berhasil"?

4. Bagaimana cara menjalankan ls -l dengan Command?

Skor: 0 / 4