#[cfg(not(unix))]
fn main() {
eprintln!("env: only available on Unix");
std::process::exit(1);
}
#[cfg(unix)]
use std::process;
#[cfg(unix)]
const TOOL_NAME: &str = "env";
#[cfg(unix)]
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(unix)]
fn main() {
coreutils_rs::common::reset_sigpipe();
let raw_args: Vec<String> = std::env::args().skip(1).collect();
let mut args: Vec<String> = Vec::new();
{
let mut j = 0;
while j < raw_args.len() {
if raw_args[j] == "-S" || raw_args[j] == "--split-string" {
j += 1;
if j < raw_args.len() {
args.extend(split_string(&raw_args[j]));
} else {
eprintln!("{}: option requires an argument -- 'S'", TOOL_NAME);
process::exit(125);
}
} else if raw_args[j].starts_with("-S") && !raw_args[j].starts_with("--") {
args.extend(split_string(&raw_args[j][2..]));
} else if let Some(val) = raw_args[j].strip_prefix("--split-string=") {
args.extend(split_string(val));
} else {
args.push(raw_args[j].clone());
}
j += 1;
}
}
let mut ignore_env = false;
let mut unsets: Vec<String> = Vec::new();
let mut sets: Vec<(String, String)> = Vec::new();
let mut null_terminated = false;
let mut chdir: Option<String> = None;
let mut command_start: Option<usize> = None;
let mut options_done = false;
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if command_start.is_some() {
break;
}
if options_done {
if arg.contains('=') {
if let Some(pos) = arg.find('=') {
let name = &arg[..pos];
let value = &arg[pos + 1..];
sets.push((name.to_string(), value.to_string()));
}
} else {
command_start = Some(i);
break;
}
i += 1;
continue;
}
match arg.as_str() {
"--help" => {
println!(
"Usage: {} [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]",
TOOL_NAME
);
println!("Set each NAME to VALUE in the environment and run COMMAND.");
println!();
println!(" -i, --ignore-environment start with an empty environment");
println!(" -0, --null end each output line with NUL, not newline");
println!(" -u, --unset=NAME remove variable from the environment");
println!(" -C, --chdir=DIR change working directory to DIR");
println!(" -S, --split-string=S process and split S into separate arguments");
println!(" --help display this help and exit");
println!(" --version output version information and exit");
println!();
println!("A mere - implies -i. If no COMMAND, print the resulting environment.");
return;
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
return;
}
"-i" | "--ignore-environment" => ignore_env = true,
"-" => ignore_env = true,
"-0" | "--null" => null_terminated = true,
"--" => {
i += 1;
while i < args.len() {
if args[i].contains('=') {
if let Some(pos) = args[i].find('=') {
let name = &args[i][..pos];
let value = &args[i][pos + 1..];
sets.push((name.to_string(), value.to_string()));
}
} else {
command_start = Some(i);
break;
}
i += 1;
}
break;
}
s if s.starts_with("--unset=") => {
let name = &s["--unset=".len()..];
unsets.push(name.to_string());
}
s if s.starts_with("--chdir=") => {
let dir = &s["--chdir=".len()..];
chdir = Some(dir.to_string());
}
"-u" | "--unset" => {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'u'", TOOL_NAME);
process::exit(125);
}
unsets.push(args[i].clone());
}
"-C" | "--chdir" => {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'C'", TOOL_NAME);
process::exit(125);
}
chdir = Some(args[i].clone());
}
s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => {
let chars: Vec<char> = s[1..].chars().collect();
let mut j = 0;
while j < chars.len() {
match chars[j] {
'i' => ignore_env = true,
'0' => null_terminated = true,
'u' => {
if j + 1 < chars.len() {
let name: String = chars[j + 1..].iter().collect();
unsets.push(name);
j = chars.len(); continue;
} else {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'u'", TOOL_NAME);
process::exit(125);
}
unsets.push(args[i].clone());
}
}
'C' => {
if j + 1 < chars.len() {
let dir: String = chars[j + 1..].iter().collect();
chdir = Some(dir);
j = chars.len();
continue;
} else {
i += 1;
if i >= args.len() {
eprintln!("{}: option requires an argument -- 'C'", TOOL_NAME);
process::exit(125);
}
chdir = Some(args[i].clone());
}
}
_ => {
eprintln!("{}: invalid option -- '{}'", TOOL_NAME, chars[j]);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(125);
}
}
j += 1;
}
}
s if s.starts_with("--") => {
eprintln!("{}: unrecognized option '{}'", TOOL_NAME, s);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(125);
}
s if s.contains('=') => {
if let Some(pos) = s.find('=') {
let name = &s[..pos];
let value = &s[pos + 1..];
sets.push((name.to_string(), value.to_string()));
}
options_done = true;
}
_ => {
command_start = Some(i);
break;
}
}
i += 1;
}
if null_terminated && command_start.is_some() {
eprintln!("{}: cannot specify --null (-0) with command", TOOL_NAME);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(125);
}
if ignore_env {
let keys: Vec<String> = std::env::vars().map(|(k, _)| k).collect();
for k in keys {
unsafe { std::env::remove_var(&k) };
}
}
for name in &unsets {
if name.is_empty() || name.contains('=') {
eprintln!(
"{}: cannot unset \u{2018}{}\u{2019}: Invalid argument",
TOOL_NAME, name
);
process::exit(125);
}
}
for name in &unsets {
unsafe { std::env::remove_var(name) };
}
for (name, value) in &sets {
unsafe { std::env::set_var(name, value) };
}
if let Some(ref dir) = chdir {
if let Err(e) = std::env::set_current_dir(dir) {
eprintln!(
"{}: cannot change directory to \u{2018}{}\u{2019}: {}",
TOOL_NAME,
dir,
coreutils_rs::common::io_error_msg(&e)
);
process::exit(125);
}
if let Ok(cwd) = std::env::current_dir() {
unsafe {
std::env::set_var("PWD", cwd);
}
}
}
if let Some(start) = command_start {
let command = &args[start];
let command_args = &args[start + 1..];
let err = std::process::Command::new(command)
.args(command_args)
.exec();
let code = if err.kind() == std::io::ErrorKind::NotFound {
127
} else {
126
};
eprintln!(
"{}: \u{2018}{}\u{2019}: {}",
TOOL_NAME,
command,
coreutils_rs::common::io_error_msg(&err)
);
process::exit(code);
} else {
let terminator = if null_terminated { '\0' } else { '\n' };
for (key, value) in std::env::vars() {
print!("{}={}{}", key, value, terminator);
}
}
}
#[cfg(unix)]
fn split_string(s: &str) -> Vec<String> {
let mut result = Vec::new();
let mut current = String::new();
let mut chars = s.chars().peekable();
let mut in_word = false;
while let Some(&ch) = chars.peek() {
match ch {
' ' | '\t' | '\n' if !in_word => {
chars.next();
}
' ' | '\t' | '\n' => {
if !current.is_empty() {
result.push(std::mem::take(&mut current));
}
in_word = false;
chars.next();
}
'#' if !in_word => {
break;
}
'\'' => {
chars.next();
in_word = true;
loop {
match chars.next() {
Some('\'') => break,
Some(c) => current.push(c),
None => break,
}
}
}
'"' => {
chars.next();
in_word = true;
loop {
match chars.next() {
Some('"') => break,
Some('\\') => match chars.next() {
Some('$') => current.push('$'),
Some('`') => current.push('`'),
Some('"') => current.push('"'),
Some('\\') => current.push('\\'),
Some('n') => current.push('\n'),
Some(c) => {
current.push('\\');
current.push(c);
}
None => current.push('\\'),
},
Some(c) => current.push(c),
None => break,
}
}
}
'\\' => {
chars.next();
in_word = true;
match chars.next() {
Some('#') => current.push('#'),
Some('n') => current.push('\n'),
Some('t') => current.push('\t'),
Some('\\') => current.push('\\'),
Some('_') => current.push(' '),
Some('$') => current.push('$'),
Some('"') => current.push('"'),
Some('\'') => current.push('\''),
Some(c) => {
current.push('\\');
current.push(c);
}
None => current.push('\\'),
}
}
_ => {
in_word = true;
current.push(ch);
chars.next();
}
}
}
if !current.is_empty() {
result.push(current);
}
result
}
#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(all(test, unix))]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fenv");
Command::new(path)
}
#[test]
fn test_print_env() {
let output = cmd().output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("PATH="), "Should print env vars");
}
#[test]
fn test_ignore_environment() {
let output = cmd().args(["-i", "TEST_VAR=hello"]).output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("TEST_VAR=hello"));
assert!(!stdout.contains("PATH="), "PATH should be cleared with -i");
}
#[test]
fn test_dash_alias_for_i() {
let output = cmd().args(["-", "MY_VAR=world"]).output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("MY_VAR=world"));
assert!(!stdout.contains("PATH="), "PATH should be cleared with -");
}
#[test]
fn test_unset() {
let output = cmd().args(["-u", "PATH"]).output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
let has_path = stdout.lines().any(|line| line.starts_with("PATH="));
assert!(!has_path, "PATH should be unset");
}
#[test]
fn test_set_var() {
let output = cmd().args(["MY_TEST_VAR=12345"]).output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("MY_TEST_VAR=12345"));
}
#[test]
fn test_run_command() {
let output = cmd().args(["echo", "hello"]).output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "hello");
}
#[test]
fn test_run_command_with_var() {
let output = cmd()
.args(["MY_VAR=test", "sh", "-c", "echo $MY_VAR"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "test");
}
#[test]
fn test_null_terminator() {
let output = cmd().args(["-i", "-0", "A=1", "B=2"]).output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = &output.stdout;
assert!(stdout.contains(&0u8), "Should contain NUL bytes");
let s = String::from_utf8_lossy(stdout);
assert!(s.contains("A=1\0"));
assert!(s.contains("B=2\0"));
}
#[test]
fn test_chdir() {
let output = cmd().args(["-C", "/tmp", "pwd"]).output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
let expected =
std::fs::canonicalize("/tmp").unwrap_or_else(|_| std::path::PathBuf::from("/tmp"));
assert_eq!(stdout.trim(), expected.to_str().unwrap());
}
#[test]
fn test_matches_gnu_run_command() {
let gnu = Command::new("env").args(["echo", "test"]).output();
if let Ok(gnu) = gnu {
let ours = cmd().args(["echo", "test"]).output().unwrap();
assert_eq!(ours.stdout, gnu.stdout, "STDOUT mismatch");
assert_eq!(ours.status.code(), gnu.status.code(), "Exit code mismatch");
}
}
#[test]
fn test_nonexistent_command() {
let output = cmd().arg("nonexistent_cmd_999").output().unwrap();
assert_eq!(output.status.code(), Some(127));
}
}