diff --git a/src/env/Cargo.toml b/src/env/Cargo.toml index 0dc2de9bc..33419f8b5 100644 --- a/src/env/Cargo.toml +++ b/src/env/Cargo.toml @@ -11,6 +11,7 @@ path = "env.rs" [dependencies] libc = "0.2.42" uucore = { path="../uucore" } +rust-ini = "0.13.0" [[bin]] name = "env" diff --git a/src/env/env.rs b/src/env/env.rs index 931277bb6..1790cb75b 100644 --- a/src/env/env.rs +++ b/src/env/env.rs @@ -13,8 +13,11 @@ #[macro_use] extern crate uucore; +extern crate ini; + +use ini::Ini; use std::env; -use std::io::{stdout, Write}; +use std::io::{stdin, stdout, Write}; use std::process::Command; static NAME: &str = "env"; @@ -27,6 +30,7 @@ static LONG_HELP: &str = " struct Options { ignore_env: bool, null: bool, + files: Vec, unsets: Vec, sets: Vec<(String, String)>, program: Vec, @@ -40,6 +44,17 @@ fn print_env(null: bool) { } } +#[cfg(not(windows))] +fn build_command(mut args: Vec) -> (String, Vec) { + (args.remove(0), args) +} + +#[cfg(windows)] +fn build_command(mut args: Vec) -> (String, Vec) { + args.insert(0, "/d/c".to_string()); + (env::var("ComSpec").unwrap_or("cmd".to_string()), args) +} + pub fn uumain(args: Vec) -> i32 { let mut core_opts = new_coreopts!(SYNTAX, SUMMARY, LONG_HELP); core_opts @@ -49,12 +64,14 @@ pub fn uumain(args: Vec) -> i32 { "null", "end each output line with a 0 byte rather than newline (only valid when printing the environment)", ) + .optopt("f", "file", "read and sets variables from the file (prior to sets/unsets)", "FILE") .optopt("u", "unset", "remove variable from the environment", "NAME"); let mut opts = Box::new(Options { ignore_env: false, null: false, unsets: vec![], + files: vec![], sets: vec![], program: vec![], }); @@ -99,6 +116,14 @@ pub fn uumain(args: Vec) -> i32 { "--ignore-environment" => opts.ignore_env = true, "--null" => opts.null = true, + "--file" => { + let var = iter.next(); + + match var { + None => println!("{}: this option requires an argument: {}", NAME, opt), + Some(s) => opts.files.push(s.to_owned()), + } + } "--unset" => { let var = iter.next(); @@ -130,6 +155,14 @@ pub fn uumain(args: Vec) -> i32 { match c { 'i' => opts.ignore_env = true, '0' => opts.null = true, + 'f' => { + let var = iter.next(); + + match var { + None => println!("{}: this option requires an argument: {}", NAME, opt), + Some(s) => opts.files.push(s.to_owned()), + } + } 'u' => { let var = iter.next(); @@ -189,6 +222,28 @@ pub fn uumain(args: Vec) -> i32 { } } + for file in &opts.files { + let conf = if file == "-" { + let stdin = stdin(); + let mut stdin_locked = stdin.lock(); + Ini::read_from(&mut stdin_locked) + } else { + Ini::load_from_file(file) + }; + let conf = match conf { + Ok(config) => config, + Err(error) => { + eprintln!("env: error: \"{}\": {}", file, error); + return 1; + } + }; + for (_, prop) in &conf { + for (key, value) in prop { + env::set_var(key, value); + } + } + } + for name in &opts.unsets { env::remove_var(name); } @@ -198,8 +253,7 @@ pub fn uumain(args: Vec) -> i32 { } if !opts.program.is_empty() { - let prog = opts.program[0].clone(); - let args = &opts.program[1..]; + let (prog, args) = build_command(opts.program); match Command::new(prog).args(args).status() { Ok(exit) => { return if exit.success() { diff --git a/tests/common/util.rs b/tests/common/util.rs index 38b92fa24..1467e5135 100755 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -447,6 +447,7 @@ impl TestScenario { /// 2. it tracks arguments provided so that in test cases which may provide variations of an arg in loops /// the test failure can display the exact call which preceded an assertion failure. /// 3. it provides convenience construction arguments to set the Command working directory and/or clear its environment. +#[derive(Debug)] pub struct UCommand { pub raw: Command, comm_string: String, diff --git a/tests/fixtures/env/vars.conf.txt b/tests/fixtures/env/vars.conf.txt new file mode 100644 index 000000000..9364d6d12 --- /dev/null +++ b/tests/fixtures/env/vars.conf.txt @@ -0,0 +1,4 @@ +# comment +FOO=bar + +BAR="bamf this" diff --git a/tests/test_env.rs b/tests/test_env.rs index 6b81d1e48..44cc6d1e9 100644 --- a/tests/test_env.rs +++ b/tests/test_env.rs @@ -1,6 +1,5 @@ use common::util::*; - #[test] fn test_env_help() { assert!(new_ucmd!().arg("--help").succeeds().no_stderr().stdout.contains("Options:")); @@ -11,6 +10,54 @@ fn test_env_version() { assert!(new_ucmd!().arg("--version").succeeds().no_stderr().stdout.contains(util_name!())); } +#[test] +fn test_echo() { + // assert!(new_ucmd!().arg("printf").arg("FOO-bar").succeeds().no_stderr().stdout.contains("FOO-bar")); + let mut cmd = new_ucmd!(); + cmd.arg("echo").arg("FOO-bar"); + println!("cmd={:?}", cmd); + + let result = cmd.run(); + println!("success={:?}", result.success); + println!("stdout={:?}", result.stdout); + println!("stderr={:?}", result.stderr); + assert!(result.success); + + let out = result.stdout.trim_right(); + + assert_eq!(out, "FOO-bar"); +} + +#[test] +fn test_file_option() { + let out = new_ucmd!() + .arg("-f").arg("vars.conf.txt") + .run().stdout; + + assert_eq!(out.lines().filter(|&line| line == "FOO=bar" || line == "BAR=bamf this").count(), 2); +} + +#[test] +fn test_combined_file_set() { + let out = new_ucmd!() + .arg("-f").arg("vars.conf.txt") + .arg("FOO=bar.alt") + .run().stdout; + + assert_eq!(out.lines().filter(|&line| line == "FOO=bar.alt").count(), 1); +} + +#[test] +fn test_combined_file_set_unset() { + let out = new_ucmd!() + .arg("-u").arg("BAR") + .arg("-f").arg("vars.conf.txt") + .arg("FOO=bar.alt") + .run().stdout; + + assert_eq!(out.lines().filter(|&line| line == "FOO=bar.alt" || line.starts_with("BAR=")).count(), 1); +} + #[test] fn test_single_name_value_pair() { let out = new_ucmd!()