Arguments, Environment, Time and Processes
Standard library · 4 of 5
Talking to the world outside your program: command-line arguments and environment variables (std::env), measuring time (std::time), and running other programs (std::process).
Coming from Goenv::args() is os.Args, program name first. Instant is the monotonic reading that a time.Time from time.Now() carries (time.Since), and Command is exec.Command. Go has a flag package in its standard library; Rust's has no flag parser (the clap crate does that). env::var returns a Result, where os.Getenv returns an empty string.
Coming from Kotlinenv::var is System.getenv, with a Result instead of a platform-type String! that may be null. Kotlin's main can take args without the program name, while env::args() has it first. Instant is TimeSource.Monotonic (or System.nanoTime), and Command is ProcessBuilder.
Coming from Pythonenv::args() is sys.argv, program first. env::var returns a Result, where os.environ.get gives None and os.environ[...] raises KeyError. Instant is time.monotonic, SystemTime is time.time, and Command is subprocess.run.
Coming from TypeScriptprocess.argv starts with node and then your script; env::args() starts at your program, so argv.slice(2) is args().skip(1). env::var returns a Result instead of string | undefined. Instant is performance.now(), Command is child_process.spawnSync, and thread::sleep blocks its thread.
Coming from Javaenv::var returns a Result instead of null. Java's main(String[] args) leaves out the program name; env::args() has it first. Instant is System.nanoTime(), not java.time.Instant (that one is wall-clock time, Rust's SystemTime). Command is ProcessBuilder.
Coming from C#env::var returns a Result, where GetEnvironmentVariable returns null. The args of Main leave out the program name; Environment.GetCommandLineArgs() has it first, like env::args(). Instant is Stopwatch, SystemTime is DateTime.UtcNow, and Command is Process with ProcessStartInfo.
Coming from Cenv::args() is argv, program name first, and env::var is getenv with a Result instead of NULL. Instant is clock_gettime(CLOCK_MONOTONIC) on Linux. Command starts the program directly with separate arguments; system and popen hand one string to /bin/sh.
Coming from C++Instant is std::chrono::steady_clock and SystemTime is system_clock. env::var is std::getenv with a Result instead of nullptr. The C++ standard library can only run a program through std::system, which hands a string to the shell; Rust's Command starts it directly, one argument at a time.
The quick comparison
use std::env;use std::process::Command;use std::time::{Duration, Instant};
fn main() { // args: the first one is the program itself let args: Vec<String> = env::args().collect(); println!("{} arg(s) after the program name", args.len() - 1);
// env vars return Result, because the variable might not be set let port = env::var("RBC_PORT").unwrap_or_else(|_| "8080".to_string()); println!("port {port}"); match env::var("RBC_MISSING") { Ok(v) => println!("RBC_MISSING={v}"), Err(e) => println!("RBC_MISSING: {e}"), }
// Instant measures, Duration is an amount of time let start = Instant::now(); std::thread::sleep(Duration::from_millis(20)); let took = start.elapsed(); println!("slept at least 20ms: {}", took >= Duration::from_millis(20));
// run another program and capture what it prints let out = Command::new("echo") .arg("hello from echo") .output() .expect("echo should run"); print!("{}", String::from_utf8_lossy(&out.stdout)); println!("exit ok: {}", out.status.success());}0 arg(s) after the program name port 8080 RBC_MISSING: environment variable not found slept at least 20ms: true hello from echo exit ok: true
package main
import ( "fmt" "os" "os/exec" "time")
func main() { fmt.Println(len(os.Args)-1, "arg(s) after the program name")
port := os.Getenv("RBC_PORT") if port == "" { port = "8080" } fmt.Println("port", port) if v, ok := os.LookupEnv("RBC_MISSING"); ok { fmt.Println("RBC_MISSING=" + v) } else { fmt.Println("RBC_MISSING: not set") }
start := time.Now() time.Sleep(20 * time.Millisecond) took := time.Since(start) fmt.Println("slept at least 20ms:", took >= 20*time.Millisecond)
out, err := exec.Command("echo", "hello from echo").Output() if err != nil { panic(err) } fmt.Print(string(out)) fmt.Println("exit ok:", err == nil)}0 arg(s) after the program name port 8080 RBC_MISSING: not set slept at least 20ms: true hello from echo exit ok: true
fun main(args: Array<String>) { println("${args.size} arg(s) after the program name")
val port = System.getenv("RBC_PORT") ?: "8080" println("port $port") val missing = System.getenv("RBC_MISSING") println(if (missing != null) "RBC_MISSING=$missing" else "RBC_MISSING: not set")
val start = System.nanoTime() Thread.sleep(20) val tookMs = (System.nanoTime() - start) / 1_000_000 println("slept at least 20ms: ${tookMs >= 20}")
val p = ProcessBuilder("echo", "hello from echo").start() print(p.inputStream.bufferedReader().readText()) println("exit ok: ${p.waitFor() == 0}")}0 arg(s) after the program name port 8080 RBC_MISSING: not set slept at least 20ms: true hello from echo exit ok: true
import osimport subprocessimport sysimport time
# argv: the first one is the script itselfprint(f"{len(sys.argv) - 1} arg(s) after the program name")
# environ.get returns None (or your default) when the variable isn't setport = os.environ.get("RBC_PORT", "8080")print(f"port {port}")missing = os.environ.get("RBC_MISSING")print(f"RBC_MISSING={missing}" if missing is not None else "RBC_MISSING: not set")
# monotonic clock for measuring, in nanosecondsstart = time.monotonic_ns()time.sleep(0.020)took_ms = (time.monotonic_ns() - start) / 1_000_000print(f"slept at least 20ms: {took_ms >= 20}")
# run another program and capture what it printsout = subprocess.run(["echo", "hello from echo"], capture_output=True, text=True)print(out.stdout, end="")print(f"exit ok: {out.returncode == 0}")0 arg(s) after the program name port 8080 RBC_MISSING: not set slept at least 20ms: True hello from echo exit ok: True
import { spawnSync } from "node:child_process";
// argv: the first two are node and the scriptconsole.log(`${process.argv.length - 2} arg(s) after the program name`);
// env values are string | undefinedconst port = process.env.RBC_PORT ?? "8080";console.log(`port ${port}`);const missing = process.env.RBC_MISSING;console.log(missing !== undefined ? `RBC_MISSING=${missing}` : "RBC_MISSING: not set");
// performance.now() is a monotonic clock in millisecondsconst start = performance.now();// no blocking sleep in std: Atomics.wait is the honest stand-in (setTimeout can fire up to 1ms early)Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);const took = performance.now() - start;console.log(`slept at least 20ms: ${took >= 20}`);
// run another program and capture what it printsconst out = spawnSync("echo", ["hello from echo"], { encoding: "utf8" });process.stdout.write(out.stdout);console.log(`exit ok: ${out.status === 0}`);0 arg(s) after the program name port 8080 RBC_MISSING: not set slept at least 20ms: true hello from echo exit ok: true
import java.time.Duration;
public class Main { public static void main(String[] args) throws Exception { // args doesn't include the program name System.out.println(args.length + " arg(s) after the program name");
// getenv returns null when the variable isn't set String port = System.getenv().getOrDefault("RBC_PORT", "8080"); System.out.println("port " + port); String missing = System.getenv("RBC_MISSING"); System.out.println(missing != null ? "RBC_MISSING=" + missing : "RBC_MISSING: not set");
// nanoTime is a monotonic clock, Duration is an amount of time long start = System.nanoTime(); Thread.sleep(20); Duration took = Duration.ofNanos(System.nanoTime() - start); System.out.println("slept at least 20ms: " + (took.compareTo(Duration.ofMillis(20)) >= 0));
// run another program and capture what it prints Process p = new ProcessBuilder("echo", "hello from echo").start(); System.out.print(new String(p.getInputStream().readAllBytes())); System.out.println("exit ok: " + (p.waitFor() == 0)); }}0 arg(s) after the program name port 8080 RBC_MISSING: not set slept at least 20ms: true hello from echo exit ok: true
using System.Diagnostics;
// args doesn't include the program nameConsole.WriteLine($"{args.Length} arg(s) after the program name");
// GetEnvironmentVariable returns null when the variable isn't setvar port = Environment.GetEnvironmentVariable("RBC_PORT") ?? "8080";Console.WriteLine($"port {port}");var missing = Environment.GetEnvironmentVariable("RBC_MISSING");Console.WriteLine(missing is not null ? $"RBC_MISSING={missing}" : "RBC_MISSING: not set");
// Stopwatch measures, TimeSpan is an amount of timevar sw = Stopwatch.StartNew();Thread.Sleep(20);var took = sw.Elapsed;Console.WriteLine($"slept at least 20ms: {took >= TimeSpan.FromMilliseconds(20)}");
// run another program and capture what it printsvar info = new ProcessStartInfo("echo") { RedirectStandardOutput = true };info.ArgumentList.Add("hello from echo");using var p = Process.Start(info)!;Console.Write(p.StandardOutput.ReadToEnd());p.WaitForExit();Console.WriteLine($"exit ok: {p.ExitCode == 0}");0 arg(s) after the program name port 8080 RBC_MISSING: not set slept at least 20ms: True hello from echo exit ok: True
#define _POSIX_C_SOURCE 200809L#include <stdio.h>#include <stdlib.h>#include <sys/wait.h>#include <time.h>
int main(int argc, char *argv[]) { (void)argv; // argc counts the program itself, so one is subtracted printf("%d arg(s) after the program name\n", argc - 1);
// getenv returns NULL when the variable is not set const char *port = getenv("RBC_PORT"); if (port == NULL) port = "8080"; printf("port %s\n", port); const char *missing = getenv("RBC_MISSING"); if (missing) { printf("RBC_MISSING=%s\n", missing); } else { printf("RBC_MISSING: not set\n"); }
// CLOCK_MONOTONIC is not affected when the system clock is set struct timespec start, stop; clock_gettime(CLOCK_MONOTONIC, &start); nanosleep(&(struct timespec){.tv_sec = 0, .tv_nsec = 20 * 1000000L}, NULL); clock_gettime(CLOCK_MONOTONIC, &stop); long took_ms = (stop.tv_sec - start.tv_sec) * 1000 + (stop.tv_nsec - start.tv_nsec) / 1000000; printf("slept at least 20ms: %s\n", took_ms >= 20 ? "true" : "false");
// popen runs the command through /bin/sh and lets you read its output FILE *child = popen("echo 'hello from echo'", "r"); if (!child) { perror("popen"); return 1; } char buf[128]; while (fgets(buf, sizeof buf, child)) fputs(buf, stdout); int status = pclose(child); printf("exit ok: %s\n", WIFEXITED(status) && WEXITSTATUS(status) == 0 ? "true" : "false"); return 0;}0 arg(s) after the program name port 8080 RBC_MISSING: not set slept at least 20ms: true hello from echo exit ok: true
#include <chrono>#include <cstdio>#include <cstdlib>#include <iostream>#include <string>#include <sys/wait.h>#include <thread>
using namespace std::chrono_literals;
int main(int argc, char *argv[]) { (void)argv; // argc counts the program itself, so one is subtracted std::cout << argc - 1 << " arg(s) after the program name\n";
// std::getenv returns nullptr when the variable is not set const char *p = std::getenv("RBC_PORT"); std::string port = p ? p : "8080"; std::cout << "port " << port << "\n"; if (const char *v = std::getenv("RBC_MISSING")) { std::cout << "RBC_MISSING=" << v << "\n"; } else { std::cout << "RBC_MISSING: not set\n"; }
// steady_clock never jumps when the system clock changes auto start = std::chrono::steady_clock::now(); std::this_thread::sleep_for(20ms); auto took = std::chrono::steady_clock::now() - start; std::cout << std::boolalpha << "slept at least 20ms: " << (took >= 20ms) << "\n";
// std has no process API: POSIX popen runs the command through /bin/sh FILE *child = popen("echo 'hello from echo'", "r"); if (!child) return 1; std::string out; char buf[128]; while (std::fgets(buf, sizeof buf, child)) out += buf; int status = pclose(child); std::cout << out; std::cout << "exit ok: " << (WIFEXITED(status) && WEXITSTATUS(status) == 0) << "\n";}0 arg(s) after the program name port 8080 RBC_MISSING: not set slept at least 20ms: true hello from echo exit ok: true
What's different
env::args()includes the program name first, likeargvin C,sys.argvin Python andos.Argsin Go. Java, Kotlin and C#mainarrays leave it out. The standard library does not parse flags: the `clap` guide does.env::varreturnsResult<String, VarError>, so "unset" is an error you can see, separate from "set to empty". In many languages a missing variable comes back as an empty string or null, and nothing makes you check. The gotcha below shows where that bites.Instantis for measuring: the docs call it a clock that is "monotonically nondecreasing", so setting the system clock does not move it.SystemTimeis wall-clock time, for timestamps, and can go backwards. Neither knows about dates or time zones: for those you need a crate such asjiff,chronoortime.Commanddoesn't go through a shell.Command::new("ls -la")looks for a program literally namedls -la. Pass each argument separately with.arg()..output()waits and captures,.status()waits and lets the child print straight to your terminal, and.spawn()starts it and lets you carry on.- What a program prints is bytes, not a
String:String::from_utf8_lossyturns the bytes into text and replaces anything that isn't valid UTF-8.std::process::exit(1)stops the program right away and skips cleanup (destructors don't run), so returning frommainis usually nicer.
Using an environment variable as if it's always set
Looks fine: in Go, Kotlin, Python, TypeScript, Java, C# and C it runs but gives a wrong answer; in C++ it crashes at runtime. Rust won't compile it.
Broken in Go. Rust catches it.Broken in Kotlin. Rust catches it.Broken in Python. Rust catches it.Broken in TypeScript. Rust catches it.Broken in Java. Rust catches it.Broken in C#. Rust catches it.Broken in C. Rust catches it.Broken in C++. Rust catches it.use std::env;
fn main() { let port = env::var("RBC_PORT"); println!("listening on port {}", port);}package main
import ( "fmt" "os")
func main() { port := os.Getenv("RBC_PORT") fmt.Println("listening on port", port)}listening on port
fun main() { val port = System.getenv("RBC_PORT") println("listening on port " + port)}listening on port null
import os
port = os.getenv("RBC_PORT")print("listening on port", port)listening on port None
const port = process.env.RBC_PORT;console.log("listening on port", port);listening on port undefined
public class Main { public static void main(String[] args) { String port = System.getenv("RBC_PORT"); System.out.println("listening on port " + port); }}listening on port null
var port = Environment.GetEnvironmentVariable("RBC_PORT");Console.WriteLine($"listening on port {port}");listening on port
#include <stdio.h>#include <stdlib.h>
int main(void) { const char *port = getenv("RBC_PORT"); printf("listening on port %s\n", port); return 0;}listening on port (null)
#include <cstdlib>#include <iostream>#include <string>
int main() { std::string port = std::getenv("RBC_PORT"); std::cout << "listening on port " << port << "\n";}terminate called after throwing an instance of 'std::logic_error' what(): basic_string: construction from null is not valid
What does the compiler say? Guess first, then open.
error[E0277]: `Result<String, VarError>` doesn't implement `std::fmt::Display` --> src/main.rs:5:38 | 5 | println!("listening on port {}", port); | -- ^^^^ `Result<String, VarError>` cannot be formatted with the default formatter | | | required by this formatting parameter | = help: the trait `std::fmt::Display` is not implemented for `Result<String, VarError>` = note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
Go prints listening on port with nothing after it. An unset variable comes back as an empty string, with no error.
Kotlin prints listening on port null. System.getenv is a Java method, so Kotlin sees its result as a platform type String!: nothing makes you check for null, and adding it to a string turns null into the text "null". port.length would throw a NullPointerException instead.
Python prints listening on port None. os.getenv returns None for a missing variable, and it prints happily. Writing os.environ["RBC_PORT"] instead would crash with a KeyError.
Prints listening on port undefined. process.env.RBC_PORT is typed string | undefined, but console.log takes anything, so TypeScript lets the missing value through.
getenv returns null for a missing variable, and string concatenation turns it into the text "null". It runs fine and prints listening on port null, just like Kotlin.
GetEnvironmentVariable returns null for a missing variable, and string interpolation prints null as nothing. It runs and prints listening on port with an empty port, the same bug as Go.
getenv returns NULL for a missing variable, and passing NULL to %s is undefined behaviour. glibc happens to print listening on port (null); other C libraries can crash, and gcc doesn't warn.
std::getenv returns nullptr for a missing variable, and building a std::string from it is undefined behaviour. libstdc++ catches it and throws std::logic_error, so the program aborts before printing anything.
env::var returns a Result, because the variable might be missing or not valid UTF-8. A Result can't be printed with {}, so you have to decide what missing means.
Run the versions in other languages: seven of them run and print a blank, null, None, undefined or (null) where the port should be, and C++ aborts. That is the classic bug that works on a machine where the variable is set and fails in a container where it is not. In Rust you choose on purpose: a default with unwrap_or_else, or a clear error with ? or expect.
The fix
Compiles and runs.
use std::env;
fn main() { let port = env::var("RBC_PORT").unwrap_or_else(|_| "8080".to_string()); println!("listening on port {port}");}listening on port 8080
The toolkit
Outputs come from Linux on 64-bit ARM with Rust 1.99.1, in debug mode. A program here starts with no arguments and an environment of its own, so several start a copy of themselves with Command (it starts another program, and .output() runs it and captures what it printed; Running other programs teaches it). env::current_exe() is the path of the running program, and an argument tells the copy what to do. The tools they run (echo, sort, sleep, sh, pwd) are common on Linux and macOS; Windows has different ones. A Duration is an amount of time (see Time).
Arguments
env::args() is an iterator of Strings. Item 0 is the program, then one item per argument. Here args[1] is the --child marker, so the parent’s arguments start at args[2]:
use std::env;use std::process::Command;
fn main() { let args: Vec<String> = env::args().collect(); if args.get(1).map(String::as_str) == Some("--child") { // args[0] is the program and args[1] is "--child"; the rest came from the parent println!("args.len(): {}", args.len()); for (i, arg) in args.iter().enumerate().skip(2) { println!("args[{i}]: {arg:?}"); } println!("nth(10): {:?}", env::args().nth(10)); return; }
// the part that starts a copy of this program with arguments let me = env::current_exe().expect("the path of this program"); let out = Command::new(me) .args(["--child", "hello world", "*", "--flag=1", "naïve"]) .output() .expect("the child should start"); print!("{}", String::from_utf8_lossy(&out.stdout));}args.len(): 6 args[2]: "hello world" args[3]: "*" args[4]: "--flag=1" args[5]: "naïve" nth(10): None
The docs call the first item “traditionally the path of the executable” and say not to rely on it for security (docs). "hello world" is one argument because the parent passed it as one, and * arrives as * because no shell is involved (on Unix a shell expands globs before the program starts). --flag=1 is only text: std has no flag parser, so use clap (guide). A missing argument is None from nth, not an index error. args() panics on an argument that is not valid UTF-8, and a Unix file name can hold any bytes; args_os() does not (run below).
Environment variables
env::var returns a Result<String, VarError>, and “not set” differs from “set to an empty string”. This program starts copies of itself with the variable removed, then set to empty:
use std::env;use std::process::Command;
fn report() { // an unset variable is an Err you can see match env::var("RBC_LEVEL") { Ok(value) => println!(" RBC_LEVEL = {value:?}"), Err(e) => println!(" RBC_LEVEL: {e} / {e:?}"), } println!(" var_os: {:?}", env::var_os("RBC_LEVEL"));
// a default for a missing variable: it does not replace an empty one let level = env::var("RBC_LEVEL").unwrap_or_else(|_| "info".to_string()); println!(" level: {level:?}"); let level = env::var("RBC_LEVEL") .ok() .filter(|v| !v.is_empty()) .unwrap_or_else(|| "info".to_string()); println!(" level, empty counts as missing: {level:?}");
// a number that must parse; anything else gets the default let workers: usize = env::var("RBC_WORKERS") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(4); println!(" workers: {workers}");}
fn main() { if env::args().nth(1).as_deref() == Some("--child") { report(); return; }
// start copies of this program with a chosen environment, to see each case let me = env::current_exe().expect("the path of this program"); for (label, level, workers) in [("unset", None, None), ("empty", Some(""), Some("many"))] { let mut child = Command::new(&me); child .arg("--child") .env_remove("RBC_LEVEL") .env_remove("RBC_WORKERS"); if let Some(value) = level { child.env("RBC_LEVEL", value); } if let Some(value) = workers { child.env("RBC_WORKERS", value); } let out = child.output().expect("the child should start"); println!("RBC_LEVEL {label}:"); print!("{}", String::from_utf8_lossy(&out.stdout)); }
// vars() lists every variable, in no promised order, so sort before you rely on one let mut names: Vec<String> = env::vars().map(|(name, _)| name).collect(); names.sort(); println!( "vars() has PATH: {}", names.binary_search(&"PATH".to_string()).is_ok() );}RBC_LEVEL unset:
RBC_LEVEL: environment variable not found / NotPresent
var_os: None
level: "info"
level, empty counts as missing: "info"
workers: 4
RBC_LEVEL empty:
RBC_LEVEL = ""
var_os: Some("")
level: ""
level, empty counts as missing: "info"
workers: 4
vars() has PATH: trueunwrap_or_else supplies a default for a missing variable only: an empty RBC_LEVEL stays "", unless .filter(|v| !v.is_empty()) comes first. .ok() turns the Result into an Option, so .and_then(|v| v.parse().ok()) turns a bad number into the default too. vars() is a snapshot in no promised order.
A program can change its own environment with set_var and remove_var. In the 2024 edition (the edition of every program here) both are unsafe:
use std::env;
fn main() { env::set_var("RBC_MODE", "fast"); println!("mode: {:?}", env::var("RBC_MODE"));}error[E0133]: call to unsafe function `set_var` is unsafe and requires unsafe block --> src/main.rs:4:5 | 4 | env::set_var("RBC_MODE", "fast"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function | = note: consult the function's documentation for information on how to avoid undefined behavior
The Edition Guide says so from 2024, “while not requiring unsafe in previous editions”. The fix is an unsafe block with a comment saying why it is sound (what you promise):
use std::env;
fn main() { // SAFETY: no other thread exists yet, so nothing else can read or write the environment. unsafe { env::set_var("RBC_MODE", "fast"); } println!("after set_var: {:?}", env::var("RBC_MODE"));
unsafe { env::remove_var("RBC_MODE"); } println!("after remove_var: {:?}", env::var("RBC_MODE"));}after set_var: Ok("fast")
after remove_var: Err(NotPresent)To give a child a variable, set it on the Command; your own process is unchanged:
use std::env;use std::process::Command;
fn main() { if env::args().nth(1).as_deref() == Some("--child") { println!("RBC_A: {:?}", env::var("RBC_A")); println!("RBC_GONE: {:?}", env::var("RBC_GONE")); println!("PATH set: {}", env::var_os("PATH").is_some()); let inherited = env::vars().any(|(name, _)| !name.starts_with("RBC_")); println!("other variables, inherited: {inherited}"); return; }
let me = env::current_exe().expect("the path of this program");
// the child inherits our environment, plus what we add or remove for it alone let out = Command::new(&me) .arg("--child") .env("RBC_A", "1") .env_remove("PATH") .output() .expect("the child should start"); println!("-- env, env_remove:"); print!("{}", String::from_utf8_lossy(&out.stdout));
// env_clear starts the child with nothing but what you add next let out = Command::new(&me) .arg("--child") .env_clear() .env("RBC_A", "2") .output() .expect("the child should start"); println!("-- env_clear:"); print!("{}", String::from_utf8_lossy(&out.stdout));
// our own environment did not change println!("parent RBC_A: {:?}", env::var("RBC_A"));}-- env, env_remove:
RBC_A: Ok("1")
RBC_GONE: Err(NotPresent)
PATH set: false
other variables, inherited: true
-- env_clear:
RBC_A: Ok("2")
RBC_GONE: Err(NotPresent)
PATH set: false
other variables, inherited: false
parent RBC_A: Err(NotPresent)A child inherits your variables; env adds one, env_remove hides one, env_clear starts from nothing.
Where am I running?
use std::env;use std::process::{self, Command};
fn main() -> std::io::Result<()> { println!("OS {}, architecture {}", env::consts::OS, env::consts::ARCH); println!("process id is positive: {}", process::id() > 0); println!( "current_dir is absolute: {}", env::current_dir()?.is_absolute() ); println!( "current_exe is absolute: {}", env::current_exe()?.is_absolute() );
// the child's directory is its own setting; ours does not change let before = env::current_dir()?; let out = Command::new("pwd").current_dir("/").output()?; println!( "pwd in the child: {:?}", String::from_utf8_lossy(&out.stdout) ); println!("ours unchanged: {}", env::current_dir()? == before);
// set_current_dir moves the whole process, every thread, and every relative path with it env::set_current_dir("/")?; println!("after set_current_dir: {:?}", env::current_dir()?); Ok(())}OS linux, architecture aarch64 process id is positive: true current_dir is absolute: true current_exe is absolute: true pwd in the child: "/\n" ours unchanged: true after set_current_dir: "/"
env::consts::OS and ARCH are fixed at build time. A Command can have its own current_dir; set_current_dir is chdir on Unix (docs), which moves the whole process, so every relative path changes meaning.
Exit codes
The Errors lesson shows which endings give which code. ExitCode::from(n) returns any u8 from main. On Unix the operating system keeps only part of the number: the exit docs say only the eight least significant bits count on most Unix-like platforms, and 0x0100 is 0 on Linux but 256 on Windows:
use std::env;use std::process::{self, Command, ExitCode};
fn main() -> ExitCode { match env::args().nth(1).as_deref() { Some("from7") => ExitCode::from(7), Some("exit256") => process::exit(256), Some("exit300") => process::exit(300), Some("exit-1") => process::exit(-1), _ => { // no argument: start this same program once per case and report the exit code let me = env::current_exe().expect("the path of this program"); for mode in ["from7", "exit256", "exit300", "exit-1"] { let status = Command::new(&me).arg(mode).status().expect("the child"); println!("{mode:>7} -> {:?}", status.code()); } ExitCode::SUCCESS } }}from7 -> Some(7) exit256 -> Some(0) exit300 -> Some(44) exit-1 -> Some(255)
process::exit runs no destructors. This program returns once, exits once, and exits with text inside a BufWriter:
use std::env;use std::io::{BufWriter, Write, stdout};use std::process::{self, Command};
struct Guard(&'static str);
impl Drop for Guard { fn drop(&mut self) { println!(" dropped {}", self.0); }}
fn run(how: &str) { let _guard = Guard("the guard"); println!(" running, how = {how}"); if how == "exit" { process::exit(0); }}
fn main() { if let Some(how) = env::args().nth(1) { if how == "exit-buffered" { println!(" running, how = {how}"); // a BufWriter hands its bytes over when it is flushed or dropped let mut out = BufWriter::new(stdout()); writeln!(out, " written to a BufWriter").unwrap(); process::exit(0); } run(&how); return; } let me = env::current_exe().expect("the path of this program"); for how in ["return", "exit", "exit-buffered"] { println!("{how}:"); let out = Command::new(&me).arg(how).output().expect("child"); print!("{}", String::from_utf8_lossy(&out.stdout)); }}return: running, how = return dropped the guard exit: running, how = exit exit-buffered: running, how = exit-buffered
The guard’s message and the buffered line never arrived: a BufWriter writes what it holds when it is flushed or dropped, and exit does neither. Returning from main (with an ExitCode or a Result too) runs the destructors first (Drop).
Time
Elapsed times change on every run, so these programs print comparisons, never the numbers.
Instant: measuring
An Instant is a reading of a clock that is “monotonically nondecreasing”, in the docs’ words. You subtract two readings. It is CLOCK_MONOTONIC on Linux and CLOCK_UPTIME_RAW on macOS, and whether a system suspend counts “varies across platforms and Rust versions” (docs). thread::sleep sleeps for at least the time asked (docs, Threads), so the first check is safe:
use std::thread;use std::time::{Duration, Instant};
fn main() { // measuring: the exact value changes every run, so only a bound is printed let start = Instant::now(); thread::sleep(Duration::from_millis(20)); let elapsed = start.elapsed(); println!("elapsed >= 20ms: {}", elapsed >= Duration::from_millis(20));
let first = Instant::now(); let second = Instant::now(); println!("second >= first: {}", second >= first);
// an Instant built by adding a Duration, so these answers do not depend on the clock let later = start + Duration::from_secs(1); println!("later - start: {:?}", later.duration_since(start)); println!("start - later: {:?}", start.duration_since(later)); println!( "saturating, wrong order: {:?}", start.saturating_duration_since(later) ); println!( "checked, right order: {:?}", later.checked_duration_since(start) ); println!( "checked, wrong order: {:?}", start.checked_duration_since(later) );}elapsed >= 20ms: true second >= first: true later - start: 1s start - later: 0ns saturating, wrong order: 0ns checked, right order: Some(1s) checked, wrong order: None
Instant + Duration builds a later instant without waiting, so the other lines do not depend on the clock. duration_since the wrong way round gives 0ns today, and the docs add that “Future versions may reintroduce the panic in some circumstances”. Say what you mean instead: checked_duration_since gives None, so you notice a mistake, and saturating_duration_since gives 0ns on purpose.
SystemTime: the wall clock
A SystemTime is the system clock, which a person can set, so two readings can come out in the wrong order and duration_since returns a Result. UNIX_EPOCH is 1970-01-01 00:00:00 UTC:
use std::time::{Duration, SystemTime, UNIX_EPOCH};
fn main() { let now = SystemTime::now(); println!("now is after the epoch: {}", now > UNIX_EPOCH);
// a time built by hand: one day after 1970-01-01 00:00:00 UTC let next_day = UNIX_EPOCH + Duration::from_secs(86_400); let since = next_day .duration_since(UNIX_EPOCH) .expect("later than the epoch"); println!("seconds since the epoch: {}", since.as_secs());
// the wall clock can go backwards, so asking "how long since?" can fail match UNIX_EPOCH.duration_since(next_day) { Ok(d) => println!("positive: {d:?}"), Err(e) => println!("Err: {e}; it is off by {:?}", e.duration()), }}now is after the epoch: true seconds since the epoch: 86400 Err: second time provided was later than self; it is off by 86400s
std has no calendar or time zones: “Sunday, 25 October” takes a crate. chrono and time are long established; jiff is newer, handles time zones, and its README compares it with both. Finding and using crates shows how to add one.
Duration: an amount of time
use std::time::Duration;
fn main() { let d = Duration::from_millis(1500); println!("as_secs: {}", d.as_secs()); println!("subsec_millis: {}", d.subsec_millis()); println!("as_millis: {}", d.as_millis()); println!("as_secs_f64: {}", d.as_secs_f64()); println!( "Debug: {d:?}, {:?}, {:?}", Duration::from_micros(250), Duration::from_nanos(7) );
let total = Duration::from_secs(90) + Duration::from_millis(250); println!("90s + 250ms: {total:?}"); println!("times 3: {:?}, divided by 4: {:?}", total * 3, total / 4); println!("mul_f64(1.5): {:?}", total.mul_f64(1.5));
// a Duration is never negative let (small, big) = (Duration::from_secs(1), Duration::from_secs(2)); println!("1s checked_sub 2s: {:?}", small.checked_sub(big)); println!("1s saturating_sub 2s: {:?}", small.saturating_sub(big)); println!( "try_from_secs_f64(-1.0): {:?}", Duration::try_from_secs_f64(-1.0) );}as_secs: 1
subsec_millis: 500
as_millis: 1500
as_secs_f64: 1.5
Debug: 1.5s, 250µs, 7ns
90s + 250ms: 90.25s
times 3: 270.75s, divided by 4: 22.5625s
mul_f64(1.5): 135.375s
1s checked_sub 2s: None
1s saturating_sub 2s: 0ns
try_from_secs_f64(-1.0): Err(TryFromFloatSecsError { kind: Negative })as_secs cuts off the fraction (1.5 s is 1), so read subsec_millis or as_secs_f64. A Duration has no Display, deliberately (docs); {:?} prints 1.5s. It is never negative, unlike the durations of Go, Java, Kotlin, C#, C++ and Python (their sections run one), so a plain - that would go below zero panics:
use std::time::Duration;
fn main() { let (small, big) = (Duration::from_secs(1), Duration::from_secs(2)); println!("about to subtract"); println!("{:?}", small - big);}about to subtract thread 'main' (1) panicked at /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/time.rs:1277:31: overflow when subtracting durations note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Use checked_sub or saturating_sub.
Running other programs
Command runs another program. output() waits and captures what it printed:
use std::process::Command;
fn main() { // output() waits, captures stdout and stderr, and gives the exit status let out = Command::new("echo") .arg("hello from echo") .output() .expect("echo should run"); println!("stdout: {:?}", String::from_utf8_lossy(&out.stdout)); println!("success: {}", out.status.success());
// arguments go to the program as they are: no shell splits, expands or runs them let out = Command::new("echo") .args(["a b", "$HOME", "*", "one; echo two"]) .output() .expect("echo should run"); println!("literal: {:?}", String::from_utf8_lossy(&out.stdout));
// the whole string is taken as a file name let err = Command::new("echo hello").output().unwrap_err(); println!("\"echo hello\": {:?}, {err}", err.kind());
// a program that runs and fails is Ok(status), not an Err let status = Command::new("false").status().expect("false should run"); println!( "false: success {}, code {:?}", status.success(), status.code() );
// stdout and stderr are separate byte buffers let out = Command::new("sh") .args(["-c", "echo on-stdout; echo on-stderr >&2; exit 4"]) .output() .expect("sh should run"); println!("stdout {:?}", String::from_utf8_lossy(&out.stdout)); println!("stderr {:?}", String::from_utf8_lossy(&out.stderr)); println!("code {:?}", out.status.code());}stdout: "hello from echo\n" success: true literal: "a b $HOME * one; echo two\n" "echo hello": NotFound, No such file or directory (os error 2) false: success false, code Some(1) stdout "on-stdout\n" stderr "on-stderr\n" code Some(4)
A program name is a file name, so "echo hello" is not found. On Unix, arguments arrive as they are: $HOME, * and ; did nothing, because no shell reads them. For a shell’s pipes or globs, run sh -c "..." yourself and take care with the string. A program that fails is Ok(status); Err means it could not be started or its output could not be read. A name without a slash is looked up in PATH, as execvp does; with a slash it is a path:
use std::process::Command;
fn main() { // a name without a slash is looked up in PATH, like execvp does let out = Command::new("echo").arg("found").output().expect("echo"); println!("echo: {:?}", String::from_utf8_lossy(&out.stdout));
// PATH set for the child alone: now the lookup finds nothing let err = Command::new("echo") .env("PATH", "/nonexistent") .output() .unwrap_err(); println!("PATH=/nonexistent: {:?}", err.kind());
// a name with a slash is a path: nothing is searched let out = Command::new("/bin/echo") .arg("by path") .env("PATH", "/nonexistent") .output() .expect("/bin/echo"); println!("/bin/echo: {:?}", String::from_utf8_lossy(&out.stdout));}echo: "found\n" PATH=/nonexistent: NotFound /bin/echo: "by path\n"
output() gives the child an input already at its end (docs). To feed it, pipe its stdin, write, and close. New here: Stdio::piped() asks for a pipe, spawn starts the child and returns at once, child.stdin.take() moves the pipe’s write end out of the Child so you own it, and try_wait asks “finished?” without waiting (None: not yet).
use std::io::Write;use std::process::{self, Command, Stdio};
fn main() -> std::io::Result<()> { let mut child = Command::new("sort") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?;
let mut stdin = child.stdin.take().expect("stdin was piped"); stdin.write_all(b"pear\napple\nfig\n")?;
// sort cannot start its answer until its input ends, and the pipe is still open println!("exited yet: {:?}", child.try_wait()?);
drop(stdin); // closing our end of the pipe is how the child sees "end of input" let out = child.wait_with_output()?; println!("sorted: {:?}", String::from_utf8_lossy(&out.stdout)); println!("status: {:?}", out.status.code());
// the child is another process with its own id let mut other = Command::new("true").spawn()?; println!( "child id differs from ours: {}", other.id() != process::id() ); other.wait()?; // collect the exit status, so no zombie is left behind Ok(())}exited yet: None sorted: "apple\nfig\npear\n" status: Some(0) child id differs from ours: true
sort cannot answer before its input ends, and the pipe stays open until stdin is dropped, so try_wait finds it running. wait and wait_with_output close a stdin still inside the Child (docs), but after take() it is yours to drop. Reading sort’s output before closing its input waits forever.
How the child ended
ExitStatus wraps the wait status Unix gives the parent. code() is Some when the program exited (C’s WIFEXITED, WEXITSTATUS); signal() is Some when a signal ended it (WIFSIGNALED, WTERMSIG), and then there is no exit code. signal and into_raw come from ExitStatusExt, an extension trait whose methods exist only after use std::os::unix::process::ExitStatusExt. into_raw is the number C’s system returns. Child::kill is the one signal stable std can send:
use std::os::unix::process::ExitStatusExt;use std::process::Command;
fn main() -> std::io::Result<()> { for script in ["exit 0", "exit 5", "kill -9 $$"] { let status = Command::new("sh").args(["-c", script]).status()?; println!( "{script:>10} -> code {:?}, signal {:?}, raw {}, shown as \"{status}\"", status.code(), status.signal(), status.into_raw() ); }
// stable std can send one signal to a child: kill, which is SIGKILL on Unix let mut child = Command::new("sleep").arg("60").spawn()?; child.kill()?; let status = child.wait()?; println!( "after kill -> code {:?}, signal {:?}", status.code(), status.signal() ); Ok(())} exit 0 -> code Some(0), signal None, raw 0, shown as "exit status: 0"
exit 5 -> code Some(5), signal None, raw 1280, shown as "exit status: 5"
kill -9 $$ -> code None, signal Some(9), raw 9, shown as "signal: 9 (SIGKILL)"
after kill -> code None, signal Some(9)exit 5 is raw 1280, 5 times 256: the exit code is shifted up one byte, while a signal number sits in the lowest bits.
A deadline for a child
std has no timeout for a child: wait blocks until it exits. The loop is yours: spawn, try_wait every few milliseconds until Some or you give up, then kill and wait. This one counts polls, so it prints no times:
use std::io;use std::os::unix::process::ExitStatusExt;use std::process::{Child, Command, ExitStatus};use std::thread;use std::time::Duration;
/// Asks the child up to `polls` times, 10 ms apart, whether it has finished.fn wait_up_to(child: &mut Child, polls: u32) -> io::Result<Option<ExitStatus>> { for _ in 0..polls { if let Some(status) = child.try_wait()? { return Ok(Some(status)); } thread::sleep(Duration::from_millis(10)); } Ok(None)}
fn main() -> io::Result<()> { let mut quick = Command::new("true").spawn()?; let status = wait_up_to(&mut quick, 300)?; println!("true finished in time: {}", status.is_some());
let mut slow = Command::new("sleep").arg("60").spawn()?; if wait_up_to(&mut slow, 5)?.is_none() { println!("sleep 60: still running when the polls ran out"); slow.kill()?; let status = slow.wait()?; // collect it, so no zombie is left behind println!( "after kill -> code {:?}, signal {:?}", status.code(), status.signal() ); } Ok(())}true finished in time: true sleep 60: still running when the polls ran out after kill -> code None, signal Some(9)
The wait-timeout crate does this for you. Go, Python, Java and Node build a deadline in; their sections run it.
Going deeper
Strings that are not String
A String is always valid UTF-8 (Strings), but arguments, variable values and file names arrive as bytes that on Unix need not be. OsString and OsStr hold them (Files and I/O uses them for paths). This program passes the bytes caf and 0xE9 (a lone byte, not UTF-8) as an argument and a variable, built with OsStrExt from std::os::unix:
use std::env;use std::ffi::OsStr;use std::os::unix::ffi::OsStrExt;use std::process::Command;
fn main() { match env::args().nth(1).as_deref() { Some("os") => { let arg = env::args_os().nth(2).expect("one argument"); println!("to_str(): {:?}", arg.to_str()); println!("to_string_lossy(): {}", arg.to_string_lossy()); println!("var: {:?}", env::var("RBC_NAME")); println!("var_os is some: {}", env::var_os("RBC_NAME").is_some()); } Some("args") => println!("{} arguments", env::args().count()), Some("vars") => println!("{} variables", env::vars().count()), _ => { // 0xE9 is "é" in Latin-1, but a lone 0xE9 is not valid UTF-8 let bad = OsStr::from_bytes(b"caf\xe9"); let me = env::current_exe().expect("the path of this program"); for mode in ["os", "args", "vars"] { let out = Command::new(&me) .arg(mode) .arg(bad) .env("RBC_NAME", bad) .output() .expect("the child should start"); println!("{mode}: exit code {:?}", out.status.code()); print!("{}", String::from_utf8_lossy(&out.stdout)); let err = String::from_utf8_lossy(&out.stderr); if let Some(line) = err.lines().find(|l| l.contains("Err")) { println!("stderr: {line}"); } } } }}os: exit code Some(0)
to_str(): None
to_string_lossy(): caf�
var: Err(NotUnicode("caf\xE9"))
var_os is some: true
args: exit code Some(101)
stderr: called `Result::unwrap()` on an `Err` value: "caf\xE9"
vars: exit code Some(101)
stderr: called `Result::unwrap()` on an `Err` value: "caf\xE9"var returns NotUnicode; args() and vars() panic while iterating, as their docs say. args_os, var_os and vars_os hand over the value, and to_str() or to_string_lossy() makes text of it. A tool that takes file names should use args_os.
Why set_var is unsafe
The environment is one global for the whole process. The set_var docs say that, outside Windows, there is no thread-safe way to read it, since “most C libraries, including libc itself, do not advertise which functions read from the environment”, and that in multi-threaded programs on those systems “the only sound option is to not use set_var or remove_var at all”. So unsafe marks a promise: no other thread is running that could read or write the environment.
C programmers know this: glibc marks setenv “MT-Unsafe” (setenv(3)), and the string getenv returns “can be modified by a subsequent call to getenv(), putenv(3), setenv(3), or unsetenv(3)” (getenv(3)). env::var copies into an owned String, so no pointer goes stale. Go’s os.Setenv and Python’s os.environ[k] = v are plain functions. Go guards its own copy with a lock (its syscall source), which protects Go code, but no lock inside one runtime stops C code in the same process calling getenv on another thread. Java has no call to change its own environment (its section runs the map throwing). For your own settings, pass a parameter.
Who closes the pipe
The child sees “end of input” when the parent’s write end is closed, which is why command-pipe.rs drops stdin. Go, Python and Node write and close inside the call (a reader as Stdin, input=, the input option; their sections run each). Rust has no input=; you write and close yourself, as run-script does.
The Stdio docs add a trap: writing more than a pipe buffer’s worth to the child’s stdin without reading its stdout “may cause a deadlock” for a program that writes before it has read everything. The child’s output pipe fills, so it stops reading, so the parent’s write blocks. Such a program hangs, so the page does not run it. The docs’ example avoids it by writing from a second thread (Threads) while the first reads.
std does not wait for a child on its own, even when the Child is dropped. On some systems a finished, unwaited child stays in the process table as a “zombie” (Child docs), so long-running programs call wait.
What std leaves to crates
ctrlc and signal-hook catch Ctrl-C and other signals, which std cannot. dotenvy loads a .env file. wait-timeout gives a child a timeout, and Tokio covers timers in async code (guide).
Translating from what you know
Practice
Try it
Replace each todo!() and run until it prints all checks passed. No check depends on timing.
Argument, else variable, else default
Write port: the first argument after the program name if it is a valid port number, else the RBC_PORT value if it is one, else 8080. The function receives the arguments and the env::var result as parameters, so the checks control both. An empty or non-numeric value counts as not given.
use std::env::{self, VarError};use std::ffi::OsString;
/// The port to listen on: the first argument after the program name if it is a valid port/// number, else the value of `RBC_PORT` (passed in as `var`) if it is one, else 8080./// An empty or non-numeric value counts as not given.fn port(args: &[String], var: Result<String, VarError>) -> u16 { todo!( "args[0] is the program, not an argument; a value that does not parse counts as not given" )}
fn main() { let args = |list: &[&str]| list.iter().map(|a| a.to_string()).collect::<Vec<String>>(); let set = |value: &str| Ok(value.to_string()); let unset = || Err(VarError::NotPresent);
assert_eq!(port(&args(&["prog"]), unset()), 8080); assert_eq!(port(&args(&["prog", "9000"]), unset()), 9000); assert_eq!(port(&args(&["prog"]), set("7000")), 7000); // an argument beats the variable assert_eq!(port(&args(&["prog", "9000"]), set("7000")), 9000); // an empty variable is not a port, and neither is an argument that is not a number assert_eq!(port(&args(&["prog"]), set("")), 8080); assert_eq!(port(&args(&["prog", "abc"]), set("7000")), 7000); assert_eq!(port(&args(&["prog", "abc"]), unset()), 8080); // 70000 does not fit in a u16 assert_eq!(port(&args(&["prog", "70000"]), set("7000")), 7000); // the program name is not an argument, even when it looks like a number assert_eq!(port(&args(&["4000"]), unset()), 8080); assert_eq!(port(&args(&["4000"]), set("7000")), 7000); // a variable that is not valid UTF-8 assert_eq!( port( &args(&["prog"]), Err(VarError::NotUnicode(OsString::from("x"))) ), 8080 ); // the real thing: this process has no arguments, and no such variable let real: Vec<String> = env::args().collect(); assert_eq!(port(&real, env::var("RBC_SURELY_NOT_SET_9F3A")), 8080);
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::env::{self, VarError};use std::ffi::OsString;
/// The port to listen on: the first argument after the program name if it is a valid port/// number, else the value of `RBC_PORT` (passed in as `var`) if it is one, else 8080./// An empty or non-numeric value counts as not given.fn port(args: &[String], var: Result<String, VarError>) -> u16 { args.get(1) .and_then(|arg| arg.parse().ok()) .or_else(|| var.ok().and_then(|value| value.parse().ok())) .unwrap_or(8080)}
fn main() { let args = |list: &[&str]| list.iter().map(|a| a.to_string()).collect::<Vec<String>>(); let set = |value: &str| Ok(value.to_string()); let unset = || Err(VarError::NotPresent);
assert_eq!(port(&args(&["prog"]), unset()), 8080); assert_eq!(port(&args(&["prog", "9000"]), unset()), 9000); assert_eq!(port(&args(&["prog"]), set("7000")), 7000); // an argument beats the variable assert_eq!(port(&args(&["prog", "9000"]), set("7000")), 9000); // an empty variable is not a port, and neither is an argument that is not a number assert_eq!(port(&args(&["prog"]), set("")), 8080); assert_eq!(port(&args(&["prog", "abc"]), set("7000")), 7000); assert_eq!(port(&args(&["prog", "abc"]), unset()), 8080); // 70000 does not fit in a u16 assert_eq!(port(&args(&["prog", "70000"]), set("7000")), 7000); // the program name is not an argument, even when it looks like a number assert_eq!(port(&args(&["4000"]), unset()), 8080); assert_eq!(port(&args(&["4000"]), set("7000")), 7000); // a variable that is not valid UTF-8 assert_eq!( port( &args(&["prog"]), Err(VarError::NotUnicode(OsString::from("x"))) ), 8080 ); // the real thing: this process has no arguments, and no such variable let real: Vec<String> = env::args().collect(); assert_eq!(port(&real, env::var("RBC_SURELY_NOT_SET_9F3A")), 8080);
println!("all checks passed");}all checks passed
Run a script
Write run_script: run a shell script with sh -c in a given directory, with extra variables and text on its standard input, and give back what it printed and its exit code. A directory that does not exist is an Err; a script that exits non-zero is not. A watchdog ends a run that waits 10 seconds for input, since forgetting to close the pipe waits forever.
use std::io;use std::process::{self, Command};use std::thread;use std::time::Duration;
/// Runs `script` with `sh -c` in the directory `dir`. The child inherits this process's/// environment, also sees every `(name, value)` pair in `vars`, and reads `input` as its/// standard input. Returns what the script printed on standard output and its exit code/// (`None` if a signal ended it). Standard error is left alone. A directory that does not/// exist is an `Err`; a script that exits non-zero is not.fn run_script( script: &str, dir: &str, vars: &[(&str, &str)], input: &str,) -> io::Result<(String, Option<i32>)> { todo!( "start sh -c in dir with the extra variables, give it a stdin pipe you write the input to, and make sure it sees the end of that input before you wait for it" )}
fn main() { // A safety net for a wrong answer that waits for input that never ends. The checks // themselves use no clock. thread::spawn(|| { thread::sleep(Duration::from_secs(10)); eprintln!("stuck for 10 seconds: is the child's stdin closed after you wrote to it?"); process::exit(1); });
assert_eq!( run_script("echo hi", "/", &[], "").unwrap(), ("hi\n".to_string(), Some(0)) ); // the directory is the child's own assert_eq!(run_script("pwd", "/usr", &[], "").unwrap().0, "/usr\n"); // variables reach the child let (out, _) = run_script( "echo \"$GREETING, $NAME\"", "/", &[("GREETING", "hello"), ("NAME", "world")], "", ) .unwrap(); assert_eq!(out, "hello, world\n"); // nothing else is added or dropped: the child sees what a plain `sh` sees, plus the pairs let lines = |vars: &[(&str, &str)]| -> usize { run_script("env | wc -l", "/", vars, "") .unwrap() .0 .trim() .parse() .unwrap() }; let plain = Command::new("sh") .args(["-c", "env | wc -l"]) .current_dir("/") .output() .unwrap(); let plain: usize = String::from_utf8_lossy(&plain.stdout) .trim() .parse() .unwrap(); assert_eq!(lines(&[]), plain); assert_eq!(lines(&[("RBC_X", "1"), ("RBC_Y", "2")]), plain + 2); // the input reaches the child's stdin, and the pipe is closed so `cat` can finish assert_eq!( run_script("cat", "/", &[], "one\ntwo\n").unwrap(), ("one\ntwo\n".to_string(), Some(0)) ); assert_eq!( run_script("tr a-z A-Z", "/", &[], "shout\n").unwrap().0, "SHOUT\n" ); // a failing script is Ok with its code; a signal leaves no code assert_eq!( run_script("echo bye; exit 3", "/", &[], "").unwrap(), ("bye\n".to_string(), Some(3)) ); assert_eq!( run_script("kill -9 $$", "/", &[], "").unwrap(), (String::new(), None) ); // a directory that does not exist is an error let err = run_script("true", "/no/such/directory", &[], "").unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::NotFound);
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::io::{self, Write};use std::process::{self, Command, Stdio};use std::thread;use std::time::Duration;
/// Runs `script` with `sh -c` in the directory `dir`. The child inherits this process's/// environment, also sees every `(name, value)` pair in `vars`, and reads `input` as its/// standard input. Returns what the script printed on standard output and its exit code/// (`None` if a signal ended it). Standard error is left alone. A directory that does not/// exist is an `Err`; a script that exits non-zero is not.fn run_script( script: &str, dir: &str, vars: &[(&str, &str)], input: &str,) -> io::Result<(String, Option<i32>)> { let mut child = Command::new("sh") .args(["-c", script]) .current_dir(dir) .envs(vars.iter().copied()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; let mut stdin = child.stdin.take().expect("stdin was piped"); stdin.write_all(input.as_bytes())?; drop(stdin); // closing our end of the pipe is how the child sees the end of its input let out = child.wait_with_output()?; Ok(( String::from_utf8_lossy(&out.stdout).into_owned(), out.status.code(), ))}
fn main() { // A safety net for a wrong answer that waits for input that never ends. The checks // themselves use no clock. thread::spawn(|| { thread::sleep(Duration::from_secs(10)); eprintln!("stuck for 10 seconds: is the child's stdin closed after you wrote to it?"); process::exit(1); });
assert_eq!( run_script("echo hi", "/", &[], "").unwrap(), ("hi\n".to_string(), Some(0)) ); // the directory is the child's own assert_eq!(run_script("pwd", "/usr", &[], "").unwrap().0, "/usr\n"); // variables reach the child let (out, _) = run_script( "echo \"$GREETING, $NAME\"", "/", &[("GREETING", "hello"), ("NAME", "world")], "", ) .unwrap(); assert_eq!(out, "hello, world\n"); // nothing else is added or dropped: the child sees what a plain `sh` sees, plus the pairs let lines = |vars: &[(&str, &str)]| -> usize { run_script("env | wc -l", "/", vars, "") .unwrap() .0 .trim() .parse() .unwrap() }; let plain = Command::new("sh") .args(["-c", "env | wc -l"]) .current_dir("/") .output() .unwrap(); let plain: usize = String::from_utf8_lossy(&plain.stdout) .trim() .parse() .unwrap(); assert_eq!(lines(&[]), plain); assert_eq!(lines(&[("RBC_X", "1"), ("RBC_Y", "2")]), plain + 2); // the input reaches the child's stdin, and the pipe is closed so `cat` can finish assert_eq!( run_script("cat", "/", &[], "one\ntwo\n").unwrap(), ("one\ntwo\n".to_string(), Some(0)) ); assert_eq!( run_script("tr a-z A-Z", "/", &[], "shout\n").unwrap().0, "SHOUT\n" ); // a failing script is Ok with its code; a signal leaves no code assert_eq!( run_script("echo bye; exit 3", "/", &[], "").unwrap(), ("bye\n".to_string(), Some(3)) ); assert_eq!( run_script("kill -9 $$", "/", &[], "").unwrap(), (String::new(), None) ); // a directory that does not exist is an error let err = run_script("true", "/no/such/directory", &[], "").unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::NotFound);
println!("all checks passed");}all checks passed
How much time is left?
Write time_left (the part of a limit still unused, that it is used up, or that the clock went back) and average (of some durations, or None for none). The checks include a half second, which as_secs would lose, and a now before start, where duration_since and saturating_duration_since both give zero and tell the caller nothing.
use std::time::{Duration, Instant};
#[derive(Debug, PartialEq)]enum Left { Remaining(Duration), UsedUp, ClockWentBack,}
/// How much of `limit` is left at `now`, counting from `start`? `UsedUp` once the limit is used/// up (exactly used up counts as used up). A `now` before `start` cannot happen with a good/// clock, so report `ClockWentBack` rather than hiding it.fn time_left(start: Instant, limit: Duration, now: Instant) -> Left { todo!( "Instant has checked_duration_since (an Option) and saturating_duration_since (zero instead); Duration has checked_sub. Which one reports a clock that went back?" )}
/// The average of the samples, or `None` when there are none.fn average(samples: &[Duration]) -> Option<Duration> { todo!("a Duration divides by a u32, and dividing by zero panics")}
fn main() { // the instants are built by adding Durations, so no check depends on the real clock let start = Instant::now(); let limit = Duration::from_secs(10); let at = |ms: u64| start + Duration::from_millis(ms);
assert_eq!(time_left(start, limit, at(0)), Left::Remaining(limit)); assert_eq!( time_left(start, limit, at(3000)), Left::Remaining(Duration::from_secs(7)) ); assert_eq!( time_left(start, limit, at(9500)), Left::Remaining(Duration::from_millis(500)) ); assert_eq!(time_left(start, limit, at(10_000)), Left::UsedUp); assert_eq!(time_left(start, limit, at(11_000)), Left::UsedUp); // a clock reading from before the start: duration_since and saturating_duration_since // would both say "nothing used", and the caller would never learn of the mistake assert_eq!(time_left(at(1000), limit, start), Left::ClockWentBack);
assert_eq!(average(&[]), None); assert_eq!( average(&[Duration::from_secs(1), Duration::from_secs(2)]), Some(Duration::from_millis(1500)) ); assert_eq!( average(&[Duration::from_millis(250)]), Some(Duration::from_millis(250)) );
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::time::{Duration, Instant};
#[derive(Debug, PartialEq)]enum Left { Remaining(Duration), UsedUp, ClockWentBack,}
/// How much of `limit` is left at `now`, counting from `start`? `UsedUp` once the limit is used/// up (exactly used up counts as used up). A `now` before `start` cannot happen with a good/// clock, so report `ClockWentBack` rather than hiding it.fn time_left(start: Instant, limit: Duration, now: Instant) -> Left { let Some(used) = now.checked_duration_since(start) else { return Left::ClockWentBack; }; match limit.checked_sub(used) { Some(left) if !left.is_zero() => Left::Remaining(left), _ => Left::UsedUp, }}
/// The average of the samples, or `None` when there are none.fn average(samples: &[Duration]) -> Option<Duration> { if samples.is_empty() { return None; } let total: Duration = samples.iter().sum(); Some(total / samples.len() as u32)}
fn main() { // the instants are built by adding Durations, so no check depends on the real clock let start = Instant::now(); let limit = Duration::from_secs(10); let at = |ms: u64| start + Duration::from_millis(ms);
assert_eq!(time_left(start, limit, at(0)), Left::Remaining(limit)); assert_eq!( time_left(start, limit, at(3000)), Left::Remaining(Duration::from_secs(7)) ); assert_eq!( time_left(start, limit, at(9500)), Left::Remaining(Duration::from_millis(500)) ); assert_eq!(time_left(start, limit, at(10_000)), Left::UsedUp); assert_eq!(time_left(start, limit, at(11_000)), Left::UsedUp); // a clock reading from before the start: duration_since and saturating_duration_since // would both say "nothing used", and the caller would never learn of the mistake assert_eq!(time_left(at(1000), limit, start), Left::ClockWentBack);
assert_eq!(average(&[]), None); assert_eq!( average(&[Duration::from_secs(1), Duration::from_secs(2)]), Some(Duration::from_millis(1500)) ); assert_eq!( average(&[Duration::from_millis(250)]), Some(Duration::from_millis(250)) );
println!("all checks passed");}all checks passed
Say how it ended
Write describe: turn an ExitStatus into ok, failed with code N or killed by signal N. The checks end real scripts with a code and with signals 9 and 15.
use std::os::unix::process::ExitStatusExt;use std::process::{Command, ExitStatus};
/// "ok" for a success, "failed with code N" for any other exit code, and/// "killed by signal N" when a signal ended the program (it has no exit code then).fn describe(status: ExitStatus) -> String { todo!("status.success(), status.code() (an Option) and status.signal() (also an Option)")}
fn run(script: &str) -> ExitStatus { Command::new("sh") .args(["-c", script]) .status() .expect("sh should start")}
fn main() { assert_eq!(describe(run("exit 0")), "ok"); assert_eq!(describe(run("exit 7")), "failed with code 7"); assert_eq!(describe(run("exit 1")), "failed with code 1"); assert_eq!(describe(run("kill -9 $$")), "killed by signal 9"); assert_eq!(describe(run("kill -15 $$")), "killed by signal 15"); assert_eq!( describe(Command::new("false").status().expect("false should start")), "failed with code 1" );
println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::os::unix::process::ExitStatusExt;use std::process::{Command, ExitStatus};
/// "ok" for a success, "failed with code N" for any other exit code, and/// "killed by signal N" when a signal ended the program (it has no exit code then).fn describe(status: ExitStatus) -> String { if status.success() { "ok".to_string() } else if let Some(code) = status.code() { format!("failed with code {code}") } else if let Some(signal) = status.signal() { format!("killed by signal {signal}") } else { "ended in some other way".to_string() }}
fn run(script: &str) -> ExitStatus { Command::new("sh") .args(["-c", script]) .status() .expect("sh should start")}
fn main() { assert_eq!(describe(run("exit 0")), "ok"); assert_eq!(describe(run("exit 7")), "failed with code 7"); assert_eq!(describe(run("exit 1")), "failed with code 1"); assert_eq!(describe(run("kill -9 $$")), "killed by signal 9"); assert_eq!(describe(run("kill -15 $$")), "killed by signal 15"); assert_eq!( describe(Command::new("false").status().expect("false should start")), "failed with code 1" );
println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 12.5: Working with Environment Variables, inside the command-line project of chapter 12.
- The standard library:
std::env,std::time,Instant,Duration,std::process,Command,Child,Stdio,ExitCode,ExitStatusExt. - Rust by Example: Program arguments, Child processes and Pipes.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.