// This file is part of the uutils coreutils package. // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel Secho sighandler #![allow(clippy::missing_errors_doc)] #[cfg(unix)] use nix::libc; #[cfg(unix)] use nix::sys::signal::Signal; #[cfg(feature = "echo")] use regex::Regex; use std::env; use std::path::Path; #[cfg(unix)] use std::process::Command; use tempfile::tempdir; use uutests::new_ucmd; #[cfg(unix)] use uutests::util::PATH; #[cfg(unix)] use uutests::util::TerminalSimulation; use uutests::util::TestScenario; #[cfg(unix)] use uutests::util::UChild; use uutests::util_name; #[cfg(unix)] struct Target { child: UChild, } #[cfg(unix)] impl Target { fn new(signals: &[&str]) -> Self { let mut cmd = new_ucmd!(); if signals.is_empty() { cmd.arg("--ignore-signal"); } else { cmd.arg(format!("--ignore-signal={}", signals.join(","))); } let mut child = cmd.args(&["sleep", "1000"]).run_no_wait(); child.delay(500); Self { child } } fn send_signal(&mut self, signal: Signal) { let _ = Command::new("kill") .args(&[ format!("-{}", signal as i32), format!("{}", self.child.id()), ]) .spawn() .expect("failed to send signal") .wait(); // macOS CI needs a longer delay for process teardown after signal delivery #[cfg(target_os = "macos")] self.child.delay(500); #[cfg(not(target_os = "macos"))] self.child.delay(100); } fn is_alive(&mut self) -> bool { self.child.is_alive() } } #[cfg(unix)] impl Drop for Target { fn drop(&mut self) { self.child.kill(); } } #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(125); } #[test] #[cfg(not(target_os = "windows"))] fn test_flags_after_command() { new_ucmd!() // This would cause an error if -u=v were processed because it's malformed .args(&["echo", "-u=v"]) .succeeds() .no_stderr() .stdout_is("-u=v\n"); new_ucmd!() // Ensure the string isn't split // cSpell:disable .args(&["printf", "%s-%s", "-Sfoo bar"]) .succeeds() .no_stderr() .stdout_is("-Sfoo bar-"); // cSpell:enable new_ucmd!() // Ensure -- is recognized .args(&["-i", "--", "-u=v"]) .succeeds() .no_stderr() .stdout_is("-u=v\n"); new_ucmd!() // Recognize echo as the command after a flag that takes a value .args(&["-C", "..", "echo", "-u=v"]) .succeeds() .no_stderr() .stdout_is("-u=v\n"); new_ucmd!() // Recognize echo as the command after a flag that takes an inline value .args(&["-C..", "echo", "-u=v"]) .succeeds() .no_stderr() .stdout_is("-u=v\n"); new_ucmd!() // Recognize echo as the command after a flag that takes a value after another flag .args(&["-iC", "..", "echo", "-u=v"]) .succeeds() .no_stderr() .stdout_is("-u=v\n"); new_ucmd!() // Similar to the last two combined .args(&["-iC..", "echo", "-u=v"]) .succeeds() .no_stderr() .stdout_is("-u=v\n"); } #[test] fn test_env_help() { new_ucmd!() .arg("--help") .succeeds() .no_stderr() .stdout_contains("Options:"); } #[test] fn test_env_version() { new_ucmd!() .arg("--version") .succeeds() .no_stderr() .stdout_is(format!("env {}\n", uucore::crate_version!())); } #[test] #[cfg(unix)] fn test_env_permissions() { // Try to execute `empty` in test fixture, that does not have exec permission. new_ucmd!() .arg("./empty") .fails_with_code(126) .stderr_is("env: './empty': Permission denied\n"); } #[test] fn test_echo() { #[cfg(target_os = "windows")] let args = ["cmd", "/d/c", "echo"]; #[cfg(not(target_os = "windows"))] let args = ["echo"]; let result = new_ucmd!().args(&args).arg("FOO-bar").succeeds(); assert_eq!(result.stdout_str().trim(), "FOO-bar"); } #[cfg(target_os = "windows")] #[test] fn test_if_windows_batch_files_can_be_executed() { let result = new_ucmd!().arg("./runBat.bat").succeeds(); assert!(result.stdout_str().contains("Hello Windows World!")); } #[cfg(feature = "echo")] #[test] fn test_debug_1() { let ts = TestScenario::new(util_name!()); let result = ts .ucmd() .arg("-v") .arg(&ts.bin_path) .args(&["echo", "hello"]) .succeeds(); result.stderr_matches( &Regex::new(concat!( r"executing: [^\n]+(\/|\\)coreutils(\.exe)?\n", r" arg\[0\]= '[^\n]+(\/|\\)coreutils(\.exe)?'\n", r" arg\[1\]= 'echo'\n", r" arg\[2\]= 'hello'" )) .unwrap(), ); } #[cfg(feature = "echo")] #[test] fn test_debug_2() { let ts = TestScenario::new(util_name!()); let result = ts .ucmd() .arg("-vv") .arg(&ts.bin_path) .args(&["echo", "hello2"]) .succeeds(); result.stderr_matches( &Regex::new(concat!( r"input args:\n", r"arg\[0\]: 'env'\n", r"arg\[1\]: '-vv'\n", r"arg\[2\]: '[^\n]+(\/|\\)coreutils(.exe)?'\n", r"arg\[3\]: 'echo'\n", r"arg\[4\]: 'hello2'\n", r"executing: [^\n]+(\/|\\)coreutils(.exe)?\n", r" arg\[0\]= '[^\n]+(\/|\\)coreutils(.exe)?'\n", r" arg\[1\]= 'echo'\n", r" arg\[2\]= 'hello2'" )) .unwrap(), ); } #[cfg(feature = "echo")] #[test] fn test_debug1_part_of_string_arg() { let ts = TestScenario::new(util_name!()); let result = ts .ucmd() .arg("-vS FOO=BAR") .arg(&ts.bin_path) .args(&["echo", "hello1"]) .succeeds(); result.stderr_matches( &Regex::new(concat!( r"executing: [^\n]+(\/|\\)coreutils(\.exe)?\n", r" arg\[0\]= '[^\n]+(\/|\\)coreutils(\.exe)?'\n", r" arg\[1\]= 'echo'\n", r" arg\[2\]= 'hello1'" )) .unwrap(), ); } #[cfg(feature = "echo")] #[test] fn test_debug2_part_of_string_arg() { let ts = TestScenario::new(util_name!()); let result = ts .ucmd() .arg("-vvS FOO=BAR") .arg(&ts.bin_path) .args(&["echo", "hello2"]) .succeeds(); result.stderr_matches( &Regex::new(concat!( r"input args:\n", r"arg\[0\]: 'env'\n", r"arg\[1\]: '-vvS FOO=BAR'\n", r"arg\[2\]: '[^\n]+(\/|\\)coreutils(.exe)?'\n", r"arg\[3\]: 'echo'\n", r"arg\[4\]: 'hello2'\n", r"executing: [^\n]+(\/|\\)coreutils(.exe)?\n", r" arg\[0\]= '[^\n]+(\/|\\)coreutils(.exe)?'\n", r" arg\[1\]= 'echo'\n", r" arg\[2\]= 'hello2'" )) .unwrap(), ); } #[test] fn test_file_option() { let out = new_ucmd!() .arg("-f") .arg("vars.conf.txt") .succeeds() .stdout_move_str(); 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") .succeeds() .stdout_move_str(); 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") .succeeds() .stdout_move_str(); assert_eq!( out.lines() .filter(|&line| line == "FOO=bar.alt" || line.starts_with("BAR=")) .count(), 1 ); } #[test] fn test_unset_invalid_variables() { use uucore::display::Quotable; // Cannot test input with \0 in it, since output will also contain \0. rlimit::prlimit fails // with this error: Error { kind: InvalidInput, message: "nul byte found in provided data" } for var in ["", "a=b"] { new_ucmd!().arg("-u").arg(var).fails().stderr_only(format!( "env: cannot unset {}: Invalid argument\n", var.quote() )); } } #[test] fn test_single_name_value_pair() { new_ucmd!() .arg("FOO=bar") .succeeds() .stdout_str() .lines() .any(|line| line == "FOO=bar"); } #[test] fn test_multiple_name_value_pairs() { let out = new_ucmd!().arg("FOO=bar").arg("ABC=xyz").succeeds(); assert_eq!( out.stdout_str() .lines() .filter(|&line| line == "FOO=bar" || line == "ABC=xyz") .count(), 2 ); } #[test] fn test_ignore_environment() { let scene = TestScenario::new(util_name!()); scene.ucmd().arg("-i").succeeds().no_stdout(); scene.ucmd().arg("-").succeeds().no_stdout(); } #[test] fn test_empty_name() { new_ucmd!() .arg("-i") .arg("=xyz") .succeeds() .stderr_only("env: warning: no name specified for value 'xyz'\n"); } #[test] fn test_null_delimiter() { let out = new_ucmd!() .arg("-i") .arg("--null") .arg("FOO=bar") .arg("ABC=xyz") .succeeds() .stdout_move_str(); let mut vars: Vec<_> = out.split('\0').collect(); assert_eq!(vars.len(), 3); vars.sort_unstable(); assert_eq!(vars[0], ""); assert_eq!(vars[1], "ABC=xyz"); assert_eq!(vars[2], "FOO=bar"); } #[test] fn test_unset_variable() { let out = TestScenario::new(util_name!()) .ucmd() .env("HOME", "FOO") .arg("-u") .arg("HOME") .succeeds() .stdout_move_str(); assert!(!out.lines().any(|line| line.starts_with("HOME="))); } #[test] fn test_fail_null_with_program() { new_ucmd!() .arg("--null") .arg("cd") .fails() .stderr_contains("cannot specify --null (-0) with command"); } #[cfg(not(windows))] #[test] fn test_change_directory() { let scene = TestScenario::new(util_name!()); let temporary_directory = tempdir().unwrap(); let temporary_path = std::fs::canonicalize(temporary_directory.path()).unwrap(); assert_ne!(env::current_dir().unwrap(), temporary_path); // command to print out current working directory let pwd = "pwd"; let out = scene .ucmd() .arg("--chdir") .arg(&temporary_path) .arg(pwd) .succeeds() .stdout_move_str(); assert_eq!(out.trim(), temporary_path.as_os_str()); } #[cfg(windows)] #[test] fn test_change_directory() { let scene = TestScenario::new(util_name!()); let temporary_directory = tempdir().unwrap(); let temporary_path = temporary_directory.path(); let temporary_path = temporary_path .strip_prefix(r"\\?\") .unwrap_or(temporary_path); let env_cd = env::current_dir().unwrap(); let env_cd = env_cd.strip_prefix(r"\\?\").unwrap_or(&env_cd); assert_ne!(env_cd, temporary_path); // COMSPEC is a variable that contains the full path to cmd.exe let cmd_path = env::var("COMSPEC").unwrap(); // command to print out current working directory let pwd = [&*cmd_path, "/C", "cd"]; let out = scene .ucmd() .arg("--chdir") .arg(temporary_path) .args(&pwd) .succeeds() .stdout_move_str(); assert_eq!(out.trim(), temporary_path.as_os_str()); } #[test] fn test_fail_change_directory() { let scene = TestScenario::new(util_name!()); let some_non_existing_path = "some_nonexistent_path"; assert!(!Path::new(some_non_existing_path).is_dir()); let out = scene .ucmd() .arg("--chdir") .arg(some_non_existing_path) .arg("pwd") .fails() .stderr_move_str(); assert!(out.contains("env: cannot change directory to ")); } #[cfg(not(target_os = "windows"))] // windows has no executable "echo", its only supported as part of a batch-file #[test] fn test_split_string_into_args_one_argument_no_quotes() { let scene = TestScenario::new(util_name!()); let out = scene .ucmd() .arg("-S echo hello world") .succeeds() .stdout_move_str(); assert_eq!(out, "hello world\n"); } #[cfg(not(target_os = "windows"))] // windows has no executable "echo", its only supported as part of a batch-file #[test] fn test_split_string_into_args_one_argument() { let scene = TestScenario::new(util_name!()); let out = scene .ucmd() .arg("-S echo \"hello world\"") .succeeds() .stdout_move_str(); assert_eq!(out, "hello world\n"); } #[cfg(not(target_os = "windows"))] // windows has no executable "echo", its only supported as part of a batch-file #[test] fn test_split_string_into_args_s_escaping_challenge() { let scene = TestScenario::new(util_name!()); let out = scene .ucmd() .args(&[r#"-S echo "hello \"great\" world""#]) .succeeds() .stdout_move_str(); assert_eq!(out, "hello \"great\" world\n"); } #[test] fn test_split_string_into_args_s_escaped_c_not_allowed() { let scene = TestScenario::new(util_name!()); let out = scene.ucmd().args(&[r#"-S"\c""#]).fails().stderr_move_str(); assert_eq!( out, "env: '\\c' must not appear in double-quoted -S string at position 2\n" ); } #[cfg(not(target_os = "windows"))] // no printf available #[test] fn test_split_string_into_args_s_whitespace_handling() { let scene = TestScenario::new(util_name!()); let out = scene .ucmd() .args(&["-Sprintf x%sx\\n A \t B \x0B\x0C\r\n"]) .succeeds() .stdout_move_str(); assert_eq!(out, "xAx\nxBx\n"); } #[cfg(not(target_os = "windows"))] // no printf available #[test] fn test_split_string_into_args_long_option_whitespace_handling() { let scene = TestScenario::new(util_name!()); let out = scene .ucmd() .args(&["--split-string printf x%sx\\n A \t B \x0B\x0C\r\n"]) .succeeds() .stdout_move_str(); assert_eq!(out, "xAx\nxBx\n"); } #[cfg(not(target_os = "windows"))] // no printf available #[test] fn test_split_string_into_args_debug_output_whitespace_handling() { let scene = TestScenario::new(util_name!()); let out = scene .ucmd() .args(&["-vvS printf x%sx\\n A \t B \x0B\x0C\r\n"]) .succeeds(); assert_eq!(out.stdout_str(), "xAx\nxBx\n"); assert_eq!( out.stderr_str(), "input args:\narg[0]: 'env'\narg[1]: $\ '-vvS printf x%sx\\\\n A \\t B \\x0B\\x0C\\r\\n'\nexecuting: printf\ \n arg[0]= 'printf'\n arg[1]= $'x%sx\\n'\n arg[2]= 'A'\n arg[3]= 'B'\n" ); } // FixMe: This test fails on MACOS: // thread 'test_env::test_gnu_e20' panicked at 'assertion failed: `(left == right)` // left: `"A=B C=D\n__CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0\n"`, // right: `"A=B C=D\n"`', tests/by-util/test_env.rs:369:5 #[cfg(not(target_os = "macos"))] #[test] fn test_gnu_e20() { let scene = TestScenario::new(util_name!()); let env_bin = String::from(uutests::util::get_tests_binary()) + " " + util_name!(); let input = [ String::from("-i"), String::from(r#"-SA="B\_C=D" "#) + env_bin.escape_default().to_string().as_str() + "", ]; let mut output = "A=B C=D\n".to_string(); // Workaround for the test to pass when coverage is being run. // If enabled, the binary called by env_bin will most probably be // instrumented for coverage, and thus will set the // __LLVM_PROFILE_RT_INIT_ONCE if env::var("__LLVM_PROFILE_RT_INIT_ONCE").is_ok() { output.push_str("__LLVM_PROFILE_RT_INIT_ONCE=__LLVM_PROFILE_RT_INIT_ONCE\n"); } let out = scene.ucmd().args(&input).succeeds(); assert_eq!(out.stdout_str(), output); } #[test] #[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign fn test_env_parsing_errors() { let ts = TestScenario::new(util_name!()); ts.ucmd() .arg("-S\\|echo hallo") // no quotes, invalid escape sequence | .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\|' in -S at position 1\n"); ts.ucmd() .arg("-S\\a") // no quotes, invalid escape sequence a .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\a' in -S at position 1\n"); ts.ucmd() .arg("-S\"\\a\"") // double quotes, invalid escape sequence a .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\a' in -S at position 2\n"); ts.ucmd() .arg(r#"-S"\a""#) // same as before, just using r#""# .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\a' in -S at position 2\n"); ts.ucmd() .arg("-S'\\a'") // single quotes, invalid escape sequence a .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\a' in -S at position 2\n"); ts.ucmd() .arg(r"-S\|\&\;") // no quotes, invalid escape sequence | .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\|' in -S at position 1\n"); ts.ucmd() .arg(r"-S\<\&\;") // no quotes, invalid escape sequence < .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\<' in -S at position 1\n"); ts.ucmd() .arg(r"-S\>\&\;") // no quotes, invalid escape sequence > .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\>' in -S at position 1\n"); ts.ucmd() .arg(r"-S\`\&\;") // no quotes, invalid escape sequence ` .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\`' in -S at position 1\n"); ts.ucmd() .arg(r#"-S"\`\&\;""#) // double quotes, invalid escape sequence ` .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\`' in -S at position 2\n"); ts.ucmd() .arg(r"-S'\`\&\;'") // single quotes, invalid escape sequence ` .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\`' in -S at position 2\n"); ts.ucmd() .arg(r"-S\`") // ` escaped without quotes .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\`' in -S at position 1\n"); ts.ucmd() .arg(r#"-S"\`""#) // ` escaped in double quotes .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\`' in -S at position 2\n"); ts.ucmd() .arg(r"-S'\`'") // ` escaped in single quotes .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\`' in -S at position 2\n"); ts.ucmd() .args(&[r"-S\🦉"]) // ` escaped in single quotes .fails_with_code(125) .no_stdout() .stderr_is("env: invalid sequence '\\\u{FFFD}' in -S at position 1\n"); // gnu doesn't show the owl. Instead a invalid unicode ? } #[test] fn test_env_with_empty_executable_single_quotes() { let ts = TestScenario::new(util_name!()); ts.ucmd() .args(&["-S''"]) // empty single quotes, considered as program name .fails_with_code(127) .no_stdout() .stderr_is("env: '': No such file or directory\n"); // gnu version again adds escaping here } #[test] fn test_env_with_empty_executable_double_quotes() { let ts = TestScenario::new(util_name!()); ts.ucmd() .args(&["-S\"\""]) // empty double quotes, considered as program name .fails_with_code(127) .no_stdout() .stderr_is("env: '': No such file or directory\n"); } // Do not assume that coreutils uses argv0 #[test] #[cfg(unix)] fn test_env_overwrite_arg0() { let ts = TestScenario::new(util_name!()); ts.ucmd() .args(&["--argv0", "hijacked", "sh", "-c", "echo $0"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); } // Do not assume that coreutils uses argv0 #[test] #[cfg(unix)] fn test_env_arg_argv0_overwrite() { let ts = TestScenario::new(util_name!()); // overwrite --argv0 by --argv0 ts.ucmd() .args(&["--argv0", "dirname"]) .args(&["--argv0", "hijacked", "sh", "-c", "echo $0"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); // overwrite -a by -a ts.ucmd() .args(&["-a", "dirname"]) .args(&["-a", "hijacked", "sh", "-c", "echo $0"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); // overwrite --argv0 by -a ts.ucmd() .args(&["--argv0", "dirname"]) .args(&["-a", "hijacked", "sh", "-c", "echo $0"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); // overwrite -a by --argv0 ts.ucmd() .args(&["-a", "dirname"]) .args(&["--argv0", "hijacked", "sh", "-c", "echo $0"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); } // Do not assume that coreutils uses argv0 #[test] #[cfg(unix)] fn test_env_arg_argv0_overwrite_mixed_with_string_args() { let ts = TestScenario::new(util_name!()); // string arg following normal ts.ucmd() .args(&["-S--argv0 dirname"]) .args(&["--argv0", "hijacked", "sh", "-c", "echo $0"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); // normal following string arg ts.ucmd() .args(&["-a", "dirname"]) .args(&["-S-a hijacked sh -c 'echo $0'"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); // one large string arg ts.ucmd() .args(&["-S--argv0 dirname -a hijacked sh -c 'echo $0'"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); // two string args ts.ucmd() .args(&["-S-a dirname"]) .args(&["-S--argv0 hijacked sh -c 'echo $0'"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); // three args: normal, string, normal ts.ucmd() .args(&["-a", "sleep"]) .args(&["-S-a dirname"]) .args(&["-a", "hijacked", "sh", "-c", "echo $0"]) .succeeds() .stdout_is("hijacked\n") .stderr_is(""); } #[test] #[cfg(unix)] fn test_env_arg_ignore_signal_invalid_signals() { let ts = TestScenario::new(util_name!()); ts.ucmd() .args(&["--ignore-signal=banana"]) .fails_with_code(125) .stderr_contains("env: 'banana': invalid signal"); ts.ucmd() .args(&["--ignore-signal=SIGbanana"]) .fails_with_code(125) .stderr_contains("env: 'SIGbanana': invalid signal"); ts.ucmd() .args(&["--ignore-signal=exit"]) .fails_with_code(125) .stderr_contains("env: 'exit': invalid signal"); ts.ucmd() .args(&["--ignore-signal=SIGexit"]) .fails_with_code(125) .stderr_contains("env: 'SIGexit': invalid signal"); } #[test] #[cfg(unix)] fn test_env_arg_ignore_signal_special_signals() { let ts = TestScenario::new(util_name!()); let signal_stop = nix::sys::signal::SIGSTOP; let signal_kill = nix::sys::signal::SIGKILL; ts.ucmd() .args(&["--ignore-signal=stop", "echo", "hello"]) .fails_with_code(125) .stderr_contains(format!( "env: failed to set signal action for signal {}: Invalid argument", signal_stop as i32 )); ts.ucmd() .args(&["--ignore-signal=kill", "echo", "hello"]) .fails_with_code(125) .stderr_contains(format!( "env: failed to set signal action for signal {}: Invalid argument", signal_kill as i32 )); ts.ucmd() .args(&["--ignore-signal=SToP", "echo", "hello"]) .fails_with_code(125) .stderr_contains(format!( "env: failed to set signal action for signal {}: Invalid argument", signal_stop as i32 )); ts.ucmd() .args(&["--ignore-signal=SIGKILL", "echo", "hello"]) .fails_with_code(125) .stderr_contains(format!( "env: failed to set signal action for signal {}: Invalid argument", signal_kill as i32 )); } #[test] #[cfg(unix)] fn test_env_arg_ignore_signal_valid_signals() { { let mut target = Target::new(&["int"]); target.send_signal(Signal::SIGINT); assert!(target.is_alive()); } { let mut target = Target::new(&["usr2"]); target.send_signal(Signal::SIGUSR2); assert!(target.is_alive()); } { let mut target = Target::new(&["int", "usr2"]); target.send_signal(Signal::SIGUSR1); assert!(!target.is_alive()); } } #[test] #[cfg(unix)] fn test_env_arg_ignore_signal_empty() { let ts = TestScenario::new(util_name!()); ts.ucmd() .args(&["--ignore-signal=", "echo", "hello"]) .succeeds() .no_stderr() .stdout_contains("hello"); } #[test] #[cfg(unix)] fn test_env_arg_ignore_signal_all_signals() { let mut target = Target::new(&[]); target.send_signal(Signal::SIGINT); assert!(target.is_alive()); } #[test] #[cfg(unix)] fn test_env_default_signal_pipe() { let ts = TestScenario::new(util_name!()); run_sigpipe_script(&ts, &["--default-signal=PIPE"]); } #[test] #[cfg(unix)] fn test_env_default_signal_all_signals() { let ts = TestScenario::new(util_name!()); run_sigpipe_script(&ts, &["--default-signal"]); } #[test] #[cfg(unix)] fn test_env_block_signal_flag() { new_ucmd!() .env("PATH", PATH) .args(&["--block-signal", "true"]) .succeeds() .no_stderr(); } #[test] #[cfg(unix)] fn test_env_list_signal_handling_reports_ignore() { let result = new_ucmd!() .env("PATH", PATH) .args(&["--ignore-signal=INT", "--list-signal-handling", "true"]) .succeeds(); let stderr = result.stderr_str(); assert!( stderr.contains("INT") && stderr.contains("IGNORE"), "unexpected signal listing: {stderr}" ); } #[cfg(unix)] fn run_sigpipe_script(ts: &TestScenario, extra_args: &[&str]) { let shell = env::var("SHELL").unwrap_or_else(|_| String::from("sh")); let _guard = SigpipeGuard::new(); let mut cmd = ts.ucmd(); cmd.env("PATH", PATH); cmd.args(extra_args); cmd.arg(shell); cmd.arg("-c"); cmd.arg("trap - PIPE; seq 999999 2>err | head -n1 > out"); cmd.succeeds(); assert_eq!(ts.fixtures.read("out"), "1\n"); assert_eq!(ts.fixtures.read("err"), ""); } #[cfg(unix)] struct SigpipeGuard { previous: libc::sighandler_t, } #[cfg(unix)] impl SigpipeGuard { fn new() -> Self { let previous = unsafe { libc::signal(libc::SIGPIPE, libc::SIG_IGN) }; Self { previous } } } #[cfg(unix)] impl Drop for SigpipeGuard { fn drop(&mut self) { unsafe { libc::signal(libc::SIGPIPE, self.previous); } } } #[test] fn disallow_equals_sign_on_short_unset_option() { let ts = TestScenario::new(util_name!()); ts.ucmd() .arg("-u=") .fails_with_code(125) .stderr_contains("env: cannot unset '=': Invalid argument"); ts.ucmd() .arg("-u=A1B2C3") .fails_with_code(125) .stderr_contains("env: cannot unset '=A1B2C3': Invalid argument"); ts.ucmd().arg("--split-string=A1B=2C3=").succeeds(); ts.ucmd() .arg("--unset=") .fails_with_code(125) .stderr_contains("env: cannot unset '': Invalid argument"); } #[cfg(test)] mod tests_split_iterator { enum EscapeStyle { /// No escaping. None, /// Wrap in single quotes. SingleQuoted, /// Single quotes combined with backslash. Mixed, } /// Determines escaping style to use. fn escape_style(s: &str) -> EscapeStyle { if s.is_empty() { return EscapeStyle::SingleQuoted; } let mut special = false; let mut newline = false; let mut single_quote = false; for c in s.chars() { match c { '\n' => { newline = true; special = true; } '\'' => { single_quote = true; special = true; } '|' | '&' | ';' | '<' | '>' | '(' | ')' | '$' | '`' | '\\' | '"' | ' ' | '\t' | '*' | '?' | '[' | '#' | '˜' | '=' | '%' => { special = true; } _ => (), } } if !special { EscapeStyle::None } else if newline && !single_quote { EscapeStyle::SingleQuoted } else { EscapeStyle::Mixed } } /// Escapes special characters in a string, so that it will retain its literal /// meaning when used as a part of command in Unix shell. /// /// It tries to avoid introducing any unnecessary quotes or escape characters, /// but specifics regarding quoting style are left unspecified. pub fn quote(s: &str) -> std::borrow::Cow<'_, str> { // We are going somewhat out of the way to provide // minimal amount of quoting in typical cases. match escape_style(s) { EscapeStyle::None => s.into(), EscapeStyle::SingleQuoted => format!("'{s}'").into(), EscapeStyle::Mixed => { let mut quoted = String::new(); quoted.push('\''); for c in s.chars() { if c == '\'' { quoted.push_str("'\\''"); } else { quoted.push(c); } } quoted.push('\''); quoted.into() } } } /// Joins arguments into a single command line suitable for execution in Unix /// shell. /// /// Each argument is quoted using [`quote`] to preserve its literal meaning when /// parsed by Unix shell. /// /// Note: This function is essentially an inverse of [`split`]. /// /// # Examples /// /// Logging executed commands in format that can be easily copied and pasted /// into an actual shell: /// /// ```rust,no_run /// fn execute(args: &[&str]) { /// use std::process::Command; /// println!("Executing: {}", shell_words::join(args)); /// Command::new(&args[0]) /// .args(&args[1..]) /// .spawn() /// .expect("failed to start subprocess") /// .wait() /// .expect("failed to wait for subprocess"); /// } /// /// execute(&["python", "-c", "print('Hello world!')"]); /// ``` /// /// [`quote`]: fn.quote.html /// [`split`]: fn.split.html pub fn join(words: I) -> String where I: IntoIterator, S: AsRef, { let mut line = words.into_iter().fold(String::new(), |mut line, word| { let quoted = quote(word.as_ref()); line.push_str(quoted.as_ref()); line.push(' '); line }); line.pop(); line } use std::ffi::OsString; use env::{ EnvError, native_int_str::{Convert, NCvt, from_native_int_representation_owned}, }; fn split(input: &str) -> Result, EnvError> { ::env::split_iterator::split(&NCvt::convert(input)).map(|vec| { vec.into_iter() .map(from_native_int_representation_owned) .collect() }) } fn split_ok(cases: &[(&str, &[&str])]) { for (i, &(input, expected)) in cases.iter().enumerate() { match split(input) { Err(actual) => { panic!( "[{i}] calling split({input:?}):\nexpected: Ok({expected:?})\n actual: Err({actual:?})\n" ); } Ok(actual) => { assert_eq!( expected, actual.as_slice(), "[{i}] After split({input:?}).unwrap()\nexpected: {expected:?}\n actual: {actual:?}\n" ); } } } } #[test] fn split_empty() { split_ok(&[("", &[])]); } #[test] fn split_initial_whitespace_is_removed() { split_ok(&[ (" a", &["a"]), ("\t\t\t\tbar", &["bar"]), ("\t \nc", &["c"]), ]); } #[test] fn split_trailing_whitespace_is_removed() { split_ok(&[ ("a ", &["a"]), ("b\t", &["b"]), ("c\t \n \n \n", &["c"]), ("d\n\n", &["d"]), ]); } #[test] fn split_carriage_return() { split_ok(&[("c\ra\r'\r'\r", &["c", "a", "\r"])]); } #[test] fn split_() { split_ok(&[("\\'\\'", &["''"])]); } #[test] fn split_single_quotes() { split_ok(&[ (r"''", &[r""]), (r"'a'", &[r"a"]), (r"'\\'", &[r"\"]), (r"' \\ '", &[r" \ "]), (r"'#'", &[r"#"]), ]); } #[test] fn split_double_quotes() { split_ok(&[ (r#""""#, &[""]), (r#""""""#, &[""]), (r#""a b c' d""#, &["a b c' d"]), (r#""\$""#, &["$"]), (r#""`""#, &["`"]), (r#""\"""#, &["\""]), (r#""\\""#, &["\\"]), ("\"\n\"", &["\n"]), ("\"\\\n\"", &[""]), ]); } #[test] fn split_unquoted() { split_ok(&[ (r"\\|\\&\\;", &[r"\|\&\;"]), (r"\\<\\>", &[r"\<\>"]), (r"\\(\\)", &[r"\(\)"]), (r"\$", &[r"$"]), (r#"\""#, &[r#"""#]), (r"\'", &[r"'"]), ("\\\n", &[]), (" \\\n \n", &[]), ("a\nb\nc", &["a", "b", "c"]), ("a\\\nb\\\nc", &["abc"]), ("foo bar baz", &["foo", "bar", "baz"]), ]); } #[test] fn split_trailing_backslash() { assert_eq!( split("\\"), Err(EnvError::EnvInvalidBackslashAtEndOfStringInMinusS( 1, "Delimiter".into() )) ); assert_eq!( split(" \\"), Err(EnvError::EnvInvalidBackslashAtEndOfStringInMinusS( 2, "Delimiter".into() )) ); assert_eq!( split("a\\"), Err(EnvError::EnvInvalidBackslashAtEndOfStringInMinusS( 2, "Unquoted".into() )) ); } #[test] fn split_errors() { assert_eq!( split("'abc"), Err(EnvError::EnvMissingClosingQuote(4, '\'')) ); assert_eq!(split("\""), Err(EnvError::EnvMissingClosingQuote(1, '"'))); assert_eq!(split("'\\"), Err(EnvError::EnvMissingClosingQuote(2, '\''))); assert_eq!(split("'\\"), Err(EnvError::EnvMissingClosingQuote(2, '\''))); assert_eq!( split(r#""$""#), Err(EnvError::EnvParsingOfMissingVariable(2)), ); } #[test] fn split_error_fail_with_unknown_escape_sequences() { assert_eq!( split("\\a"), Err(EnvError::EnvInvalidSequenceBackslashXInMinusS(1, 'a')) ); assert_eq!( split("\"\\a\""), Err(EnvError::EnvInvalidSequenceBackslashXInMinusS(2, 'a')) ); assert_eq!( split("'\\a'"), Err(EnvError::EnvInvalidSequenceBackslashXInMinusS(2, 'a')) ); assert_eq!( split(r#""\a""#), Err(EnvError::EnvInvalidSequenceBackslashXInMinusS(2, 'a')) ); assert_eq!( split(r"\🦉"), Err(EnvError::EnvInvalidSequenceBackslashXInMinusS( 1, '\u{FFFD}' )) ); } #[test] fn split_comments() { split_ok(&[ (r" x # comment ", &["x"]), (r" w1#w2 ", &["w1#w2"]), (r"'not really a # comment'", &["not really a # comment"]), (" a # very long comment \n b # another comment", &["a", "b"]), ]); } #[test] fn test_quote() { assert_eq!(quote(""), "''"); assert_eq!(quote("'"), "''\\'''"); assert_eq!(quote("abc"), "abc"); assert_eq!(quote("a \n b"), "'a \n b'"); assert_eq!(quote("X'\nY"), "'X'\\''\nY'"); } #[test] fn test_join() { assert_eq!(join(["a", "b", "c"]), "a b c"); assert_eq!(join([" ", "$", "\n"]), "' ' '$' '\n'"); } #[test] fn join_followed_by_split_is_identity() { let cases: Vec<&[&str]> = vec![ &["a"], &["python", "-c", "print('Hello world!')"], &["echo", " arg with spaces ", "arg \' with \" quotes"], &["even newlines are quoted correctly\n", "\n", "\n\n\t "], &["$", "`test`"], &["cat", "~user/log*"], &["test", "'a \"b", "\"X'"], &["empty", "", "", ""], ]; for argv in cases { let args = join(argv); assert_eq!(split(&args).unwrap(), argv); } } } mod test_raw_string_parser { use std::{ borrow::Cow, ffi::{OsStr, OsString}, }; use env::{ native_int_str::{ NativeStr, from_native_int_representation, from_native_int_representation_owned, to_native_int_representation, }, string_expander::StringExpander, string_parser, }; const LEN_OWL: usize = if cfg!(target_os = "windows") { 2 } else { 4 }; #[test] fn test_ascii_only_take_one_look_at_correct_data_and_end_behavior() { let input = "hello"; let cow = to_native_int_representation(OsStr::new(input)); let mut uut = StringExpander::new(&cow); for c in input.chars() { assert_eq!(c, uut.get_parser().peek().unwrap()); uut.take_one().unwrap(); } assert_eq!( uut.get_parser().peek(), Err(string_parser::Error { peek_position: 5, err_type: string_parser::ErrorType::EndOfInput }) ); uut.take_one().unwrap_err(); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), input ); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), "" ); } #[test] fn test_multi_byte_codes_take_one_look_at_correct_data_and_end_behavior() { let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); let cow = to_native_int_representation(input.as_os_str()); let mut uut = StringExpander::new(&cow); for _i in 0..3 { assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); uut.take_one().unwrap(); assert_eq!(uut.get_parser().peek().unwrap(), 'x'); uut.take_one().unwrap(); } assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); uut.take_one().unwrap(); assert_eq!( uut.get_parser().peek(), Err(string_parser::Error { peek_position: 10 * LEN_OWL + 3, err_type: string_parser::ErrorType::EndOfInput }) ); uut.take_one().unwrap_err(); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), input ); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), "" ); } #[test] fn test_multi_byte_codes_put_one_ascii_start_middle_end_try_invalid_ascii() { let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); let cow = to_native_int_representation(input.as_os_str()); let owl: char = '🦉'; let mut uut = StringExpander::new(&cow); uut.put_one_char('a'); for _i in 0..3 { assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); uut.take_one().unwrap(); uut.put_one_char('a'); assert_eq!(uut.get_parser().peek().unwrap(), 'x'); uut.take_one().unwrap(); uut.put_one_char('a'); } assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); uut.take_one().unwrap(); uut.put_one_char(owl); uut.put_one_char('a'); assert_eq!( uut.get_parser().peek(), Err(string_parser::Error { peek_position: LEN_OWL * 10 + 3, err_type: string_parser::ErrorType::EndOfInput }) ); uut.take_one().unwrap_err(); uut.put_one_char('a'); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), "a🦉🦉🦉axa🦉🦉axa🦉axa🦉🦉🦉🦉🦉aa" ); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), "" ); } #[test] fn test_multi_byte_codes_skip_one_take_one_skip_until_ascii_char_or_end() { let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); let cow = to_native_int_representation(input.as_os_str()); let mut uut = StringExpander::new(&cow); uut.skip_one().unwrap(); // skip 🦉🦉🦉 let p = LEN_OWL * 3; assert_eq!(uut.get_peek_position(), p); uut.skip_one().unwrap(); // skip x assert_eq!(uut.get_peek_position(), p + 1); uut.take_one().unwrap(); // take 🦉🦉 let p = p + 1 + LEN_OWL * 2; assert_eq!(uut.get_peek_position(), p); uut.skip_one().unwrap(); // skip x assert_eq!(uut.get_peek_position(), p + 1); uut.get_parser_mut().skip_until_char_or_end('x'); // skip 🦉 let p = p + 1 + LEN_OWL; assert_eq!(uut.get_peek_position(), p); uut.take_one().unwrap(); // take x uut.get_parser_mut().skip_until_char_or_end('x'); // skip 🦉🦉🦉🦉 till end let p = p + 1 + LEN_OWL * 4; assert_eq!(uut.get_peek_position(), p); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), "🦉🦉x" ); } #[test] fn test_multi_byte_codes_skip_multiple_ascii_bounded_good_and_bad() { let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); let cow = to_native_int_representation(input.as_os_str()); let mut uut = StringExpander::new(&cow); uut.get_parser_mut().skip_multiple(0); assert_eq!(uut.get_peek_position(), 0); let p = LEN_OWL * 3; uut.get_parser_mut().skip_multiple(p); // skips 🦉🦉🦉 assert_eq!(uut.get_peek_position(), p); uut.take_one().unwrap(); // take x assert_eq!(uut.get_peek_position(), p + 1); let step = LEN_OWL * 3 + 1; uut.get_parser_mut().skip_multiple(step); // skips 🦉🦉x🦉 let p = p + 1 + step; assert_eq!(uut.get_peek_position(), p); uut.take_one().unwrap(); // take x assert_eq!(uut.get_peek_position(), p + 1); let step = 4 * LEN_OWL; uut.get_parser_mut().skip_multiple(step); // skips 🦉🦉🦉🦉 let p = p + 1 + step; assert_eq!(uut.get_peek_position(), p); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), "xx" ); } #[test] fn test_multi_byte_codes_put_string_utf8_start_middle_end() { let input = OsString::from("🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"); let cow = to_native_int_representation(input.as_os_str()); let mut uut = StringExpander::new(&cow); uut.put_string("🦔oo"); uut.take_one().unwrap(); // takes 🦉🦉🦉 uut.put_string("oo🦔"); uut.take_one().unwrap(); // take x uut.get_parser_mut().skip_until_char_or_end('\n'); // skips till end uut.put_string("o🦔o"); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), "🦔oo🦉🦉🦉oo🦔xo🦔o" ); } #[test] fn test_multi_byte_codes_look_at_remaining_start_middle_end() { let input = "🦉🦉🦉x🦉🦉x🦉x🦉🦉🦉🦉"; let cow = to_native_int_representation(OsStr::new(input)); let mut uut = StringExpander::new(&cow); assert_eq!(uut.get_parser().peek_remaining(), OsStr::new(input)); uut.take_one().unwrap(); // takes 🦉🦉🦉 assert_eq!(uut.get_parser().peek_remaining(), OsStr::new(&input[12..])); uut.get_parser_mut().skip_until_char_or_end('\n'); // skips till end assert_eq!(uut.get_parser().peek_remaining(), OsStr::new("")); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), "🦉🦉🦉" ); } #[test] fn test_deal_with_invalid_encoding() { let owl_invalid_part; let (brace_1, brace_2); #[cfg(target_os = "windows")] { let mut buffer = [0u16; 2]; let owl = '🦉'.encode_utf16(&mut buffer); owl_invalid_part = owl[0]; brace_1 = '<'.encode_utf16(&mut buffer).to_vec(); brace_2 = '>'.encode_utf16(&mut buffer).to_vec(); } #[cfg(not(target_os = "windows"))] { let mut buffer = [0u8; 4]; let owl = '🦉'.encode_utf8(&mut buffer); owl_invalid_part = owl.bytes().next().unwrap(); brace_1 = [b'<'].to_vec(); brace_2 = [b'>'].to_vec(); } let mut input_ux = brace_1; input_ux.push(owl_invalid_part); input_ux.extend(brace_2); let input_str = from_native_int_representation(Cow::Borrowed(&input_ux)); let mut uut = StringExpander::new(&input_ux); assert_eq!(uut.get_parser().peek_remaining(), input_str); assert_eq!(uut.get_parser().peek().unwrap(), '<'); uut.take_one().unwrap(); // takes "<" assert_eq!( uut.get_parser().peek_remaining(), NativeStr::new(&input_str).split_at(1).1 ); assert_eq!(uut.get_parser().peek().unwrap(), '\u{FFFD}'); uut.take_one().unwrap(); // takes owl_b assert_eq!( uut.get_parser().peek_remaining(), NativeStr::new(&input_str).split_at(2).1 ); assert_eq!(uut.get_parser().peek().unwrap(), '>'); uut.get_parser_mut().skip_until_char_or_end('\n'); assert_eq!(uut.get_parser().peek_remaining(), OsStr::new("")); uut.take_one().unwrap_err(); assert_eq!( from_native_int_representation_owned(uut.take_collected_output()), NativeStr::new(&input_str).split_at(2).0 ); } } #[cfg(unix)] #[test] fn test_simulation_of_terminal_false() { let scene = TestScenario::new("util"); let out = scene.ccmd("env").arg("sh").arg("is_a_tty.sh").succeeds(); std::assert_eq!( String::from_utf8_lossy(out.stdout()), "stdin is not a tty\nstdout is not a tty\nstderr is not a tty\n" ); std::assert_eq!( String::from_utf8_lossy(out.stderr()), "This is an error message.\n" ); } #[cfg(unix)] #[test] fn test_simulation_of_terminal_true() { let scene = TestScenario::new("util"); let out = scene .ccmd("env") .arg("sh") .arg("is_a_tty.sh") .terminal_simulation(true) .succeeds(); std::assert_eq!( String::from_utf8_lossy(out.stdout()), "stdin is a tty\r\nterminal size: 30 80\r\nstdout is a tty\r\nstderr is a tty\r\n" ); std::assert_eq!( String::from_utf8_lossy(out.stderr()), "This is an error message.\r\n" ); } #[cfg(unix)] #[test] fn test_simulation_of_terminal_for_stdin_only() { let scene = TestScenario::new("util"); let out = scene .ccmd("env") .arg("sh") .arg("is_a_tty.sh") .terminal_sim_stdio(TerminalSimulation { stdin: true, stdout: false, stderr: false, ..Default::default() }) .succeeds(); std::assert_eq!( String::from_utf8_lossy(out.stdout()), "stdin is a tty\nterminal size: 30 80\nstdout is not a tty\nstderr is not a tty\n" ); std::assert_eq!( String::from_utf8_lossy(out.stderr()), "This is an error message.\n" ); } #[cfg(unix)] #[test] fn test_simulation_of_terminal_for_stdout_only() { let scene = TestScenario::new("util"); let out = scene .ccmd("env") .arg("sh") .arg("is_a_tty.sh") .terminal_sim_stdio(TerminalSimulation { stdin: false, stdout: true, stderr: false, ..Default::default() }) .succeeds(); std::assert_eq!( String::from_utf8_lossy(out.stdout()), "stdin is not a tty\r\nstdout is a tty\r\nstderr is not a tty\r\n" ); std::assert_eq!( String::from_utf8_lossy(out.stderr()), "This is an error message.\n" ); } #[cfg(unix)] #[test] fn test_simulation_of_terminal_for_stderr_only() { let scene = TestScenario::new("util"); let out = scene .ccmd("env") .arg("sh") .arg("is_a_tty.sh") .terminal_sim_stdio(TerminalSimulation { stdin: false, stdout: false, stderr: true, ..Default::default() }) .succeeds(); std::assert_eq!( String::from_utf8_lossy(out.stdout()), "stdin is not a tty\nstdout is not a tty\nstderr is a tty\n" ); std::assert_eq!( String::from_utf8_lossy(out.stderr()), "This is an error message.\r\n" ); } #[cfg(unix)] #[test] fn test_simulation_of_terminal_size_information() { let scene = TestScenario::new("util"); let out = scene .ccmd("env") .arg("sh") .arg("is_a_tty.sh") .terminal_sim_stdio(TerminalSimulation { size: Some(libc::winsize { ws_col: 40, ws_row: 10, ws_xpixel: 40 * 8, ws_ypixel: 10 * 10, }), stdout: true, stdin: true, stderr: true, }) .succeeds(); std::assert_eq!( String::from_utf8_lossy(out.stdout()), "stdin is a tty\r\nterminal size: 10 40\r\nstdout is a tty\r\nstderr is a tty\r\n" ); std::assert_eq!( String::from_utf8_lossy(out.stderr()), "This is an error message.\r\n" ); } #[cfg(unix)] #[test] fn test_simulation_of_terminal_pty_sends_eot_automatically() { let scene = TestScenario::new("util"); let mut cmd = scene.ccmd("env"); cmd.timeout(std::time::Duration::from_secs(10)); cmd.args(&["cat", "-"]); cmd.terminal_simulation(true); let child = cmd.run_no_wait(); let out = child.wait().unwrap(); // cat would block if there is no eot std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); std::assert_eq!(String::from_utf8_lossy(out.stdout()), "\r\n"); } #[cfg(unix)] #[test] fn test_simulation_of_terminal_pty_pipes_into_data_and_sends_eot_automatically() { let scene = TestScenario::new("util"); let message = "Hello stdin forwarding!"; let mut cmd = scene.ccmd("env"); cmd.args(&["cat", "-"]); cmd.terminal_simulation(true); cmd.pipe_in(message); let child = cmd.run_no_wait(); let out = child.wait().unwrap(); std::assert_eq!( String::from_utf8_lossy(out.stdout()), format!("{message}\r\n") ); std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); } #[test] #[cfg(not(windows))] fn test_emoji_env_vars() { new_ucmd!() .arg("🎯_VAR=Hello 🌍") .arg("printenv") .arg("🎯_VAR") .succeeds() .stdout_contains("Hello 🌍"); } #[cfg(unix)] #[test] fn test_simulation_of_terminal_pty_write_in_data_and_sends_eot_automatically() { let scene = TestScenario::new("util"); let mut cmd = scene.ccmd("env"); cmd.args(&["cat", "-"]); cmd.terminal_simulation(true); let mut child = cmd.run_no_wait(); child.write_in("Hello stdin forwarding via write_in!"); let out = child.wait().unwrap(); std::assert_eq!( String::from_utf8_lossy(out.stdout()), "Hello stdin forwarding via write_in!\r\n" ); std::assert_eq!(String::from_utf8_lossy(out.stderr()), ""); } #[test] fn test_env_french() { new_ucmd!() .arg("--verbo") .env("LANG", "fr_FR") .fails() .stderr_contains("erreur : argument inattendu"); } #[test] fn test_shebang_error() { new_ucmd!() .arg("\'-v \'") .fails() .stderr_contains("use -[v]S to pass options in shebang lines"); } #[test] #[cfg(not(target_os = "windows"))] fn test_braced_variable_with_default_value() { new_ucmd!() .arg("-Secho ${UNSET_VAR_UNLIKELY_12345:fallback}") .succeeds() .stdout_is("fallback\n"); } #[test] #[cfg(not(target_os = "windows"))] fn test_braced_variable_with_default_when_set() { new_ucmd!() .env("TEST_VAR_12345", "actual") .arg("-Secho ${TEST_VAR_12345:fallback}") .succeeds() .stdout_is("actual\n"); } #[test] #[cfg(not(target_os = "windows"))] fn test_simple_braced_variable() { new_ucmd!() .env("TEST_VAR_12345", "value") .arg("-Secho ${TEST_VAR_12345}") .succeeds() .stdout_is("value\n"); } #[test] fn test_braced_variable_error_missing_closing_brace() { new_ucmd!() .arg("-Secho ${FOO") .fails_with_code(125) .stderr_contains("Missing closing brace"); } #[test] fn test_braced_variable_error_missing_closing_brace_after_default() { new_ucmd!() .arg("-Secho ${FOO:-value") .fails_with_code(125) .stderr_contains("Missing closing brace after default value"); } #[test] fn test_braced_variable_error_starts_with_digit() { new_ucmd!() .arg("-Secho ${1FOO}") .fails_with_code(125) .stderr_contains("Unexpected character: '1'"); } #[test] fn test_braced_variable_error_unexpected_character() { new_ucmd!() .arg("-Secho ${FOO?}") .fails_with_code(125) .stderr_contains("Unexpected character: '?'"); } #[test] #[cfg(unix)] fn test_non_utf8_env_vars() { use std::ffi::OsString; use std::os::unix::ffi::OsStringExt; let non_utf8_value = OsString::from_vec(b"hello\x80world".to_vec()); new_ucmd!() .env("NON_UTF8_VAR", &non_utf8_value) .succeeds() .stdout_contains_bytes(b"NON_UTF8_VAR=hello\x80world"); } #[test] #[cfg(unix)] fn test_ignore_signal_pipe_broken_pipe_regression() { // Test that --ignore-signal=PIPE properly ignores SIGPIPE in child processes. // When SIGPIPE is ignored, processes should handle broken pipes gracefully // instead of being terminated by the signal. // // Regression test for: https://github.com/uutils/coreutils/issues/9617 use std::io::{BufRead, BufReader}; use std::process::{Command, Stdio}; let scene = TestScenario::new(util_name!()); // Helper function to simulate a broken pipe scenario (like "seq 1000000 | head -n1") let test_sigpipe_behavior = |use_ignore_signal: bool| -> i32 { let mut cmd = Command::new(&scene.bin_path); cmd.arg("env"); if use_ignore_signal { cmd.arg("--ignore-signal=PIPE"); } // Use seq instead of yes - writes bounded output but enough to trigger SIGPIPE cmd.arg("seq") .arg("1") .arg("1000000") .stdout(Stdio::piped()) .stderr(Stdio::null()); let mut child = cmd.spawn().expect("Failed to spawn env process"); // Read exactly one line then close the pipe to trigger SIGPIPE if let Some(stdout) = child.stdout.take() { let mut reader = BufReader::new(stdout); let mut line = String::new(); let _ = reader.read_line(&mut line); // Pipe closes when reader is dropped, sending SIGPIPE to writing process } // seq should exit quickly (either from SIGPIPE or after handling EPIPE) match child.wait() { Ok(status) => status.code().unwrap_or(141), // 128 + 13 Err(_) => 141, } }; // Test without signal ignoring - should be killed by SIGPIPE let normal_exit_code = test_sigpipe_behavior(false); println!("Normal 'env seq' exit code: {normal_exit_code}"); // Test with --ignore-signal=PIPE - should handle broken pipe gracefully let ignore_signal_exit_code = test_sigpipe_behavior(true); println!("With --ignore-signal=PIPE exit code: {ignore_signal_exit_code}"); // Verify the --ignore-signal=PIPE flag changes the behavior assert!( ignore_signal_exit_code != 141, "--ignore-signal=PIPE had no effect! Process was still killed by SIGPIPE (exit code 141). Normal: {normal_exit_code}, --ignore-signal: {ignore_signal_exit_code}" ); // Expected behavior: assert_eq!( normal_exit_code, 141, "Without --ignore-signal, process should be killed by SIGPIPE" ); assert_ne!( ignore_signal_exit_code, 141, "With --ignore-signal=PIPE, process should NOT be killed by SIGPIPE" ); // Process should exit gracefully when SIGPIPE is ignored assert!( ignore_signal_exit_code == 0 || ignore_signal_exit_code == 1, "With --ignore-signal=PIPE, process should exit gracefully (0 or 1), got: {ignore_signal_exit_code}" ); } #[test] #[cfg(unix)] fn test_env_disallow_double_underscore_all() { new_ucmd!() .args(&["--ignore-signal=__ALL__", "true"]) .fails() .stderr_contains("invalid signal"); new_ucmd!() .args(&["--default-signal=__ALL__", "true"]) .fails() .stderr_contains("invalid signal"); new_ucmd!() .args(&["--block-signal=__ALL__", "true"]) .fails() .stderr_contains("invalid signal"); }