Process #
Every running program is a process. Rust provides std::process for interacting with processes — both themselves and launched child processes. This need arises in many scenarios: executing shell commands from within a program, building a CLI tool that calls another tool, reading configuration from environment variables, or controlling how a program exits when an error occurs. std::process also includes access to command line arguments and environment variables that form the program execution context. This article covers all these aspects comprehensively — from simple child process launches to more complex pipeline patterns.
std::process::exit and Exit Code #
The most basic way to interact with a process is to control how it exits. In Rust, main() returning () always exits with code 0 (success). To exit with another code, use process::exit.
use std::process;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Penggunaan: {} <nama>", args[0]);
process::exit(1); // exits with an error code
}
println!("Halo, {}!", args[1]);
// exits with code 0 implicitly
}
A more idiomatic way is to use main() which returns Result — this allows the use of the ? operator throughout the main function:
use std::error::Error;
use std::fs;
fn main() -> Result<(), Box<dyn Error>> {
let konten = fs::read_to_string("config.txt")?;
println!("Konfigurasi: {}", konten.trim());
Ok(())
}
// If an error occurs, Rust prints an error message and exits with code 1
// If Ok(()), exit with code 0
For more explicit control, implement the “main calling run” pattern:
use std::process;
fn run() -> Result<(), String> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 3 {
return Err(format!("Butuh 2 argumen, dapat {}", args.len() - 1));
}
let a: i32 = args[1].parse().map_err(|_| format!("'{}' bukan angka", args[1]))?;
let b: i32 = args[2].parse().map_err(|_| format!("'{}' bukan angka", args[2]))?;
println!("Hasil: {}", a + b);
Ok(())
}
fn main() {
if let Err(e) = run() {
eprintln!("Error: {}", e);
process::exit(1);
}
}
Exit Code Convention #
Kode keluar yang umum digunakan:
0 — Sukses
1 — Error umum
2 — Kesalahan penggunaan (argumen salah, dsb)
126 — Perintah ditemukan tapi tidak bisa dieksekusi
127 — Perintah tidak ditemukan
128 — Invalid exit argument
130 — Program dihentikan dengan Ctrl+C (SIGINT)
use std::process;
// Define a constant for a meaningful exit code
const EXIT_OK: i32 = 0;
const EXIT_ERROR: i32 = 1;
const EXIT_USAGE: i32 = 2;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.contains(&"--help".to_string()) {
println!("Bantuan penggunaan program...");
process::exit(EXIT_OK);
}
if args.len() < 2 {
eprintln!("Error: argumen tidak cukup");
eprintln!("Gunakan --help untuk bantuan");
process::exit(EXIT_USAGE);
}
match jalankan_program(&args[1]) {
Ok(_) => process::exit(EXIT_OK),
Err(e) => {
eprintln!("Error: {}", e);
process::exit(EXIT_ERROR);
}
}
}
fn jalankan_program(_arg: &str) -> Result<(), String> {
Ok(())
}
Command Line Arguments #
std::env::args() provides access to arguments provided when the program is run.
use std::env;
fn main() {
// args() returns an iterator — the first element is the program name
let args: Vec<String> = env::args().collect();
println!("Nama program: {}", args[0]);
println!("Jumlah argumen: {}", args.len() - 1);
for (i, arg) in args.iter().enumerate().skip(1) {
println!("Argumen {}: {}", i, arg);
}
// Parse simple arguments
let mut verbose = false;
let mut output_file: Option<String> = None;
let mut input_files: Vec<String> = Vec::new();
let mut iter = args.iter().skip(1); // skip program name
while let Some(arg) = iter.next() {
match arg.as_str() {
"-v" | "--verbose" => verbose = true,
"-o" | "--output" => {
output_file = iter.next().cloned();
}
arg if arg.starts_with("--output=") => {
output_file = Some(arg.trim_start_matches("--output=").to_string());
}
arg if arg.starts_with('-') => {
eprintln!("Flag tidak dikenal: {}", arg);
std::process::exit(2);
}
file => input_files.push(file.to_string()),
}
}
if verbose {
println!("Mode verbose aktif");
println!("Output: {:?}", output_file);
println!("Input files: {:?}", input_files);
}
}
For more complex command line argument parsing — subcommands, argument types, validation, and automatic help text — use theclapcrate. Almost all Rust CLI tools in production useclapbecause manual parsing quickly becomes unmaintainable for programs with more than a few flags.
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "myapp", about = "Contoh aplikasi CLI")]
struct Args {
/// Input file to be processed
#[arg(required = true)]
input: Vec<String>,
/// Output files
#[arg(short, long, default_value = "output.txt")]
output: String,
/// Enable verbose mode
#[arg(short, long)]
verbose: bool,
/// Number of worker threads
#[arg(short, long, default_value_t = 4)]
threads: u32,
}
fn main() {
let args = Args::parse();
println!("Input: {:?}", args.input);
println!("Output: {}", args.output);
println!("Verbose: {}", args.verbose);
println!("Threads: {}", args.threads);
}
Environment Variables #
Environment variables are a common way to configure programs without changing code or configuration files.
use std::env;
fn main() {
// Reads one environment variable
match env::var("HOME") {
Ok(nilai) => println!("HOME: {}", nilai),
Err(env::VarError::NotPresent) => println!("HOME tidak disetel"),
Err(env::VarError::NotUnicode(v)) => println!("HOME bukan UTF-8: {:?}", v),
}
// With default value
let port = env::var("PORT").unwrap_or_else(|_| String::from("8080"));
let host = env::var("HOST").unwrap_or(String::from("localhost"));
// Parse directly to the desired type
let maks_koneksi: u32 = env::var("MAX_CONNECTIONS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(100);
println!("Server: {}:{}", host, port);
println!("Maks koneksi: {}", maks_koneksi);
// Iterates all environment variables
println!("\nSemua environment variables:");
for (kunci, nilai) in env::vars() {
if kunci.starts_with("RUST") {
println!(" {} = {}", kunci, nilai);
}
}
// Change the environment variables for this process
// (only applies to current process, does not affect parent)
env::set_var("MY_APP_ENV", "production");
println!("{}", env::var("MY_APP_ENV").unwrap()); // "production"
// Delete environment variables
env::remove_var("MY_APP_ENV");
println!("{:?}", env::var("MY_APP_ENV")); // Err(NotPresent)
// Current directory
let cwd = env::current_dir().expect("Gagal mendapat direktori saat ini");
println!("CWD: {}", cwd.display());
// Executable directory
let exe = env::current_exe().expect("Gagal mendapat path executable");
println!("Executable: {}", exe.display());
}
Environment Variables Based Configuration #
A very common pattern in production applications is to read all configuration from environment variables at startup:
use std::env;
#[derive(Debug)]
struct Konfigurasi {
database_url: String,
port: u16,
debug: bool,
log_level: String,
jwt_secret: String,
}
impl Konfigurasi {
fn dari_env() -> Result<Self, String> {
Ok(Konfigurasi {
database_url: env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL harus disetel".to_string())?,
port: env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse::<u16>()
.map_err(|_| "PORT harus berupa angka 0-65535".to_string())?,
debug: env::var("DEBUG")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
log_level: env::var("LOG_LEVEL")
.unwrap_or_else(|_| "info".to_string()),
jwt_secret: env::var("JWT_SECRET")
.map_err(|_| "JWT_SECRET harus disetel".to_string())?,
})
}
}
fn main() {
// Load configuration on startup
let config = match Konfigurasi::dari_env() {
Ok(c) => c,
Err(e) => {
eprintln!("Error konfigurasi: {}", e);
std::process::exit(1);
}
};
println!("Konfigurasi dimuat: {:?}", config);
}
Command — Executes Child Process #
std::process::Command is an API for launching and interacting with child processes.
use std::process::Command;
fn main() {
// Executes simple commands
let status = Command::new("echo")
.arg("Halo dari proses anak!")
.status() // execute and wait, returns exit status
.expect("Gagal menjalankan echo");
println!("Exit status: {}", status.success());
// Command with multiple arguments
let status = Command::new("ls")
.args(["-la", "/tmp"])
.status()
.expect("Gagal menjalankan ls");
// Capture output — output is not displayed to the terminal
let output = Command::new("date")
.output() // run and catch stdout + stderr
.expect("Gagal menjalankan date");
println!("Status: {}", output.status.success());
println!("Stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("Stderr: {}", String::from_utf8_lossy(&output.stderr));
// ANTI-PATTERN: uses shell for simple commands — unnecessary overhead
let output = Command::new("sh")
.arg("-c")
.arg("echo hello")
.output()
.unwrap();
// CORRECT: call the binary directly if possible
let output = Command::new("echo")
.arg("hello")
.output()
.unwrap();
println!("{}", String::from_utf8_lossy(&output.stdout).trim());
}
Handling Output and Errors #
use std::process::Command;
fn jalankan_perintah(program: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|e| format!("Gagal menjalankan '{}': {}", program, e))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
Err(format!(
"'{}' keluar dengan kode {}: {}",
program,
output.status.code().unwrap_or(-1),
stderr
))
}
}
fn main() {
match jalankan_perintah("git", &["rev-parse", "--short", "HEAD"]) {
Ok(commit) => println!("Commit saat ini: {}", commit),
Err(e) => eprintln!("Error: {}", e),
}
match jalankan_perintah("cat", &["file_tidak_ada.txt"]) {
Ok(konten) => println!("Konten: {}", konten),
Err(e) => eprintln!("Error: {}", e),
}
// Checks whether the program is available in the PATH
fn program_tersedia(nama: &str) -> bool {
Command::new(nama)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
println!("git tersedia: {}", program_tersedia("git"));
println!("docker tersedia: {}", program_tersedia("docker"));
}
Environment and Working Directory for Child Processes #
Command allows granular configuration of child processes’ environments and working directories.
use std::process::Command;
use std::collections::HashMap;
fn main() {
// Set environment variables for child processes only
let output = Command::new("printenv")
.arg("MY_VAR")
.env("MY_VAR", "nilai_dari_parent")
.output()
.unwrap();
println!("{}", String::from_utf8_lossy(&output.stdout).trim());
// Remove the specified environment variable from the child process
let output = Command::new("printenv")
.arg("HOME")
.env_remove("HOME")
.output()
.unwrap();
println!("HOME di anak: '{}'", String::from_utf8_lossy(&output.stdout).trim());
// Clear all env vars, setting only what is needed — for isolated processes
let output = Command::new("env")
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("HOME", "/tmp")
.output()
.unwrap();
println!("Env terisolasi:\n{}", String::from_utf8_lossy(&output.stdout));
// Change the working directory of the child process
let output = Command::new("pwd")
.current_dir("/tmp")
.output()
.unwrap();
println!("CWD proses anak: {}", String::from_utf8_lossy(&output.stdout).trim());
// Combination: run cargo in the specified project directory
let output = Command::new("cargo")
.args(["build", "--release"])
.current_dir("/path/ke/proyek")
.env("RUST_LOG", "info")
.output();
match output {
Ok(o) if o.status.success() => println!("Build sukses"),
Ok(o) => eprintln!("Build gagal: {}", String::from_utf8_lossy(&o.stderr)),
Err(e) => eprintln!("Cargo tidak ditemukan: {}", e),
}
}
Piping stdin and stdout #
For two-way interaction with child processes — sending input and reading output — use Stdio::piped().
use std::process::{Command, Stdio};
use std::io::Write;
fn main() {
// Send input to child process stdin
let mut child = Command::new("cat") // cat reads stdin and prints to stdout
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn() // spawn runs without waiting
.expect("Gagal spawn cat");
// Send data to child process stdin
let stdin = child.stdin.as_mut().expect("Gagal ambil stdin");
stdin.write_all(b"halo dari parent\n").expect("Gagal tulis ke stdin");
stdin.write_all(b"baris kedua\n").expect("Gagal tulis ke stdin");
// stdin will close on drop — signaling EOF to the child process
// Wait for the process to complete and take the output
let output = child.wait_with_output().expect("Gagal tunggu proses anak");
println!("Output dari cat:\n{}", String::from_utf8_lossy(&output.stdout));
// Pipeline: output of one process to input of another process
// Equivalent to: echo "hello rust world" | tr 'a-z' 'A-Z' | wc -w
let echo = Command::new("echo")
.arg("halo dunia rust")
.stdout(Stdio::piped())
.spawn()
.expect("Gagal spawn echo");
let tr = Command::new("tr")
.args(["a-z", "A-Z"])
.stdin(echo.stdout.unwrap()) // output echo → stdin tr
.stdout(Stdio::piped())
.spawn()
.expect("Gagal spawn tr");
let output = tr.wait_with_output().expect("Gagal tunggu tr");
println!("Setelah tr: {}", String::from_utf8_lossy(&output.stdout).trim());
// "HELLO RUST WORLD"
}
Running Process in Background #
use std::process::{Command, Stdio};
use std::time::Duration;
use std::thread;
fn main() {
// spawn() returns Child — process running in the background
let mut child = Command::new("sleep")
.arg("10")
.spawn()
.expect("Gagal spawn sleep");
println!("Proses anak berjalan dengan PID: {}", child.id());
// Perform other work while the process is running
thread::sleep(Duration::from_millis(100));
// Check whether the process is still running
match child.try_wait() {
Ok(Some(status)) => println!("Proses sudah selesai: {}", status),
Ok(None) => println!("Proses masih berjalan"),
Err(e) => eprintln!("Error cek status: {}", e),
}
// Force stop the process
child.kill().expect("Gagal membunuh proses");
let status = child.wait().expect("Gagal tunggu setelah kill");
println!("Proses dihentikan: {:?}", status);
// Timeout: wait a maximum of N seconds, then kill
fn jalankan_dengan_timeout(
program: &str,
args: &[&str],
timeout: Duration,
) -> Result<std::process::Output, String> {
let mut child = Command::new(program)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Gagal spawn: {}", e))?;
let deadline = std::time::Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(_)) => {
return child.wait_with_output()
.map_err(|e| format!("Gagal ambil output: {}", e));
}
Ok(None) => {
if std::time::Instant::now() >= deadline {
child.kill().ok();
return Err(format!("Timeout setelah {:?}", timeout));
}
thread::sleep(Duration::from_millis(10));
}
Err(e) => return Err(format!("Error: {}", e)),
}
}
}
match jalankan_dengan_timeout("sleep", &["5"], Duration::from_millis(100)) {
Ok(output) => println!("Selesai: {:?}", output.status),
Err(e) => println!("Error: {}", e), // "Timeout after 100ms"
}
}
Shell Scripting Patterns in Rust #
Rust is often used as a shell script replacement for tasks that require higher reliability. The following are common patterns.
use std::process::Command;
use std::fs;
use std::path::Path;
// Helper to run commands like in shell script
fn sh(perintah: &str) -> Result<String, String> {
let output = Command::new("sh")
.arg("-c")
.arg(perintah)
.output()
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
}
}
// More secure: run binary directly without shell
fn jalankan(program: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|e| format!("Gagal jalankan '{}': {}", program, e))?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err(format!(
"Exit {}: {}",
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stderr).trim()
))
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Check required dependencies
for tool in &["git", "cargo", "docker"] {
match jalankan(tool, &["--version"]) {
Ok(versi) => println!("✓ {}: {}", tool, versi.lines().next().unwrap_or("")),
Err(_) => {
eprintln!("✗ {} tidak ditemukan — pastikan sudah terinstal", tool);
std::process::exit(1);
}
}
}
// Retrieve git information
let branch = jalankan("git", &["rev-parse", "--abbrev-ref", "HEAD"])?;
let commit = jalankan("git", &["rev-parse", "--short", "HEAD"])?;
let status = jalankan("git", &["status", "--porcelain"])?;
println!("\nStatus git:");
println!(" Branch: {}", branch);
println!(" Commit: {}", commit);
println!(" Perubahan: {}", if status.is_empty() { "tidak ada" } else { "ada perubahan" });
// Build project
println!("\nMemulai build...");
let output = Command::new("cargo")
.args(["build", "--release"])
.status()?;
if !output.success() {
return Err("Build gagal".into());
}
println!("Build sukses!");
// Safer file operations from shell
let versi = "1.0.0";
let nama_arsip = format!("release-v{}.tar.gz", versi);
// Create a directory if it doesn't already exist
fs::create_dir_all("dist")?;
// Copy binary
let src = Path::new("target/release/myapp");
let dst = Path::new("dist/myapp");
if src.exists() {
fs::copy(src, dst)?;
println!("Binary disalin ke dist/");
}
Ok(())
}
Capture Process Signal #
Rust standard library does not provide direct signal handling due to platform complexity. For this need, crate ctrlc (for Ctrl+C) or signal-hook (for complete signal) is the standard choice.
[dependencies]
ctrlc = "3"
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
fn main() {
let berjalan = Arc::new(AtomicBool::new(true));
let berjalan_clone = Arc::clone(&berjalan);
// Capture Ctrl+C
ctrlc::set_handler(move || {
println!("\nMenerima Ctrl+C, menghentikan...");
berjalan_clone.store(false, Ordering::SeqCst);
}).expect("Gagal menyetel handler Ctrl+C");
println!("Program berjalan. Tekan Ctrl+C untuk berhenti.");
// Stoppable main loop
let mut iterasi = 0;
while berjalan.load(Ordering::SeqCst) {
iterasi += 1;
println!("Iterasi {}", iterasi);
std::thread::sleep(std::time::Duration::from_millis(500));
}
// Cleanup before exiting
println!("Cleanup... selesai setelah {} iterasi.", iterasi);
// Resource, database connection, etc. cleaned here
}
Graceful Shutdown with Drop #
The RAII pattern ensures that cleanup always occurs, even when the program exits due to a panic or signal:
struct KoneksiDatabase {
url: String,
}
impl KoneksiDatabase {
fn baru(url: &str) -> Self {
println!("Koneksi database dibuka: {}", url);
KoneksiDatabase { url: url.to_string() }
}
}
impl Drop for KoneksiDatabase {
fn drop(&mut self) {
// Always called when Database Connection goes out of scope
println!("Koneksi database ditutup: {}", self.url);
}
}
fn main() {
let _db = KoneksiDatabase::baru("postgres://localhost/mydb");
// Do the work...
println!("Program berjalan...");
// Drop is called automatically here, even if there is panic
}
// Output:
// Database connection opened: postgres://localhost/mydb
// Program running...
// Database connection closed: postgres://localhost/mydb
Process Information #
use std::env;
use std::process;
fn main() {
// PID of the current process
println!("PID: {}", process::id());
// Current directory
let cwd = env::current_dir().unwrap();
println!("CWD: {}", cwd.display());
// Executable path
let exe = env::current_exe().unwrap();
println!("Executable: {}", exe.display());
// Program name of args[0]
let nama_program = env::args()
.next()
.and_then(|p| std::path::Path::new(&p)
.file_name()
.map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_else(|| "unknown".to_string());
println!("Nama program: {}", nama_program);
// All environment variables that start with the specified prefix
let rust_vars: Vec<_> = env::vars()
.filter(|(k, _)| k.starts_with("RUST"))
.collect();
println!("RUST* env vars: {:?}", rust_vars);
}
When to Use Command vs Alternative #
Gunakan std::process::Command jika:
✓ Perlu menjalankan program eksternal yang sudah ada
✓ Integrasi dengan tool CLI seperti git, docker, cargo
✓ Output dari program sudah cukup — tidak perlu parsing binary protocol
✓ Program hanya tersedia sebagai binary, bukan library
Pertimbangkan alternatif library Rust jika:
✗ Fungsi tersedia sebagai crate — lebih portable, tidak bergantung pada binary di PATH
✗ Perlu performa tinggi — subprocess overhead signifikan untuk operasi kecil
✗ Perlu cross-platform yang ketat — perintah shell berbeda di Windows dan Unix
✗ Perlu error handling yang detail — output stderr seringkali tidak terstruktur
Gunakan shell script biasa jika:
✗ Program hanya dijalankan di lingkungan Unix yang terkontrol
✗ Orkestrasi sederhana yang tidak butuh error handling kompleks
✗ Tim lebih familiar dengan shell daripada Rust untuk scripting
flowchart TD
A[Perlu menjalankan program eksternal?] --> B{Ada crate Rust yang melakukan hal sama?}
B -- Yes --> C[Use a crate — more portable and type-safe]
B -- No --> D{Perlu kontrol penuh atas I/O?}
D -- Yes --> E["Command with Stdio::piped()<br/>stdin + stdout + stderr"]
D -- No --> F{Hanya perlu exit status?}
F -- Yes --> G["Command::status()\nSederhana, tidak tangkap output"]
F -- No --> H["Command::output()\nTangkap stdout dan stderr"]
G --> I["Check status.success()"]
H --> J[Check output.status + parse stdout]
E --> K["spawn() + write stdin\n+ wait_with_output()"]
style C fill:#e8f5e9
style E fill:#e3f2fd
style G fill:#e8f5e9
style H fill:#e8f5e9Summary #
main() -> Result<(), Box<dyn Error>>— the most idiomatic way to handle errors in binary. Rust automatically prints an error message and exits with code 1 whenErris returned.process::exit(kode)— exits immediately without running the destructor. For the necessary cleanup, use the RAII pattern withDropor return an error frommain.env::varreturnsResult— environment variable may be missing or not UTF-8. Useunwrap_or_elsefor default values or?for error propagation.Command::output()for output capture,Command::status()for exit code only —output()buffers the entire stdout and stderr in memory; for large output, usespawn()+ streaming.Stdio::piped()for two-way interaction — send to child processstdin, read from itsstdout. Remember to closestdin(withdrop) to signal EOF.spawn()for background process,status()for blocking —spawn()returnsChildwhich can bekill()ed orwait()ed later. Don’t forget to callwait()so there are no zombie processes.- Always handle
ErrfromCommand— a binary not in the PATH producesErr, not a non-zero exit code. Two error levels: failed spawn (binary does not exist) and exit code non-zero (binary exists but failed).- Use crate
clapfor argument parsing — manual parsing is not scalable.clapwith derive API generates help text, validation, and correct types automatically.