2023-08-21 10:49:27 +02:00
// 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.
2025-12-27 08:16:36 +09:00
// spell-checker:ignore (words) bamf chdir rlimit prlimit COMSPEC cout cerr FFFD winsize xpixel ypixel Secho sighandler
2024-06-30 16:27:08 +02:00
#![allow(clippy::missing_errors_doc)]
2020-08-03 11:32:31 -04:00
2025-12-27 08:16:36 +09:00
#[cfg(unix)]
use nix ::libc ;
2024-05-23 23:01:39 +03:00
#[cfg(unix)]
use nix ::sys ::signal ::Signal ;
2024-11-27 09:29:22 +01:00
#[cfg(feature = "echo" )]
2024-03-30 21:19:35 +01:00
use regex ::Regex ;
2020-08-03 11:32:31 -04:00
use std ::env ;
2021-01-19 08:15:53 +01:00
use std ::path ::Path ;
2024-05-23 23:01:39 +03:00
#[cfg(unix)]
use std ::process ::Command ;
2021-01-19 08:15:53 +01:00
use tempfile ::tempdir ;
2025-03-28 09:51:51 +01:00
use uutests ::new_ucmd ;
#[cfg(unix)]
2025-12-27 08:16:36 +09:00
use uutests ::util ::PATH ;
#[cfg(unix)]
2025-03-28 09:51:51 +01:00
use uutests ::util ::TerminalSimulation ;
use uutests ::util ::TestScenario ;
#[cfg(unix)]
use uutests ::util ::UChild ;
use uutests ::util_name ;
2015-11-16 00:25:01 -05:00
2024-05-23 23:01:39 +03:00
#[cfg(unix)]
struct Target {
child : UChild ,
}
#[cfg(unix)]
impl Target {
fn new ( signals : & [ & str ]) -> Self {
2025-12-27 08:16:36 +09:00
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 ();
2024-05-23 23:01:39 +03:00
child . delay ( 500 );
Self { child }
}
fn send_signal ( & mut self , signal : Signal ) {
2024-11-28 21:27:49 +01:00
let _ = Command ::new ( "kill" )
2024-05-23 23:01:39 +03:00
. args ( & [
format! ( "- {} " , signal as i32 ),
format! ( " {} " , self . child . id ()),
])
. spawn ()
2024-11-28 21:27:49 +01:00
. expect ( "failed to send signal" )
. wait ();
2024-05-23 23:01:39 +03:00
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 ();
}
}
2022-09-10 18:38:14 +02:00
#[test]
fn test_invalid_arg () {
2025-03-01 15:29:11 +01:00
new_ucmd! (). arg ( "--definitely-invalid" ). fails_with_code ( 125 );
2022-09-10 18:38:14 +02:00
}
2025-04-18 12:29:50 -07:00
#[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 " );
}
2017-10-28 17:32:50 +03:00
#[test]
fn test_env_help () {
2021-04-05 23:03:43 +03:00
new_ucmd! ()
2020-04-13 20:36:03 +02:00
. arg ( "--help" )
. succeeds ()
. no_stderr ()
2022-09-29 17:48:27 +02:00
. stdout_contains ( "Options:" );
2017-10-28 17:32:50 +03:00
}
#[test]
fn test_env_version () {
2021-04-05 23:03:43 +03:00
new_ucmd! ()
2020-04-13 20:36:03 +02:00
. arg ( "--version" )
. succeeds ()
. no_stderr ()
2021-04-05 23:03:43 +03:00
. stdout_contains ( util_name! ());
2017-10-28 17:32:50 +03:00
}
2025-01-07 23:53:52 +01:00
#[test]
2025-03-23 14:47:05 +01:00
#[cfg(unix)]
2025-01-07 23:53:52 +01:00
fn test_env_permissions () {
2025-03-23 14:47:05 +01:00
// Try to execute `empty` in test fixture, that does not have exec permission.
2025-01-07 23:53:52 +01:00
new_ucmd! ()
2025-03-23 14:47:05 +01:00
. arg ( "./empty" )
2025-03-01 15:29:11 +01:00
. fails_with_code ( 126 )
2025-03-23 14:47:05 +01:00
. stderr_is ( "env: './empty': Permission denied \n " );
2025-01-07 23:53:52 +01:00
}
2018-11-19 02:22:13 -06:00
#[test]
fn test_echo () {
2024-03-14 19:38:28 +01:00
#[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 ();
2018-11-19 02:22:13 -06:00
2021-04-05 23:03:43 +03:00
assert_eq! ( result . stdout_str (). trim (), "FOO-bar" );
2018-11-19 02:22:13 -06:00
}
2024-03-14 19:38:28 +01:00
#[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!" ));
}
2024-11-27 09:29:22 +01:00
#[cfg(feature = "echo" )]
2024-03-30 21:19:35 +01:00
#[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 (),
);
}
2024-11-27 09:29:22 +01:00
#[cfg(feature = "echo" )]
2024-03-30 21:19:35 +01:00
#[test]
fn test_debug_2 () {
let ts = TestScenario ::new ( util_name! ());
let result = ts
. ucmd ()
. arg ( "-vv" )
2025-02-01 23:31:49 +05:30
. arg ( & ts . bin_path )
2024-03-30 21:19:35 +01:00
. 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 (),
);
}
2024-11-27 09:29:22 +01:00
#[cfg(feature = "echo" )]
2024-03-30 21:19:35 +01:00
#[test]
fn test_debug1_part_of_string_arg () {
let ts = TestScenario ::new ( util_name! ());
let result = ts
. ucmd ()
. arg ( "-vS FOO=BAR" )
2025-02-01 23:31:49 +05:30
. arg ( & ts . bin_path )
2024-03-30 21:19:35 +01:00
. 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 (),
);
}
2024-11-27 09:29:22 +01:00
#[cfg(feature = "echo" )]
2024-03-30 21:19:35 +01:00
#[test]
fn test_debug2_part_of_string_arg () {
let ts = TestScenario ::new ( util_name! ());
let result = ts
. ucmd ()
. arg ( "-vvS FOO=BAR" )
2025-02-01 23:31:49 +05:30
. arg ( & ts . bin_path )
2024-03-30 21:19:35 +01:00
. 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 (),
);
}
2019-04-28 10:19:14 -05:00
#[test]
fn test_file_option () {
2021-04-17 11:22:49 +02:00
let out = new_ucmd! ()
. arg ( "-f" )
. arg ( "vars.conf.txt" )
2025-03-09 16:53:56 +01:00
. succeeds ()
2021-04-17 11:22:49 +02:00
. stdout_move_str ();
2019-04-28 10:19:14 -05:00
2020-04-13 20:36:03 +02:00
assert_eq! (
out . lines ()
. filter ( |& line | line == "FOO=bar" || line == "BAR=bamf this" )
. count (),
2
);
2019-04-28 10:19:14 -05:00
}
#[test]
fn test_combined_file_set () {
let out = new_ucmd! ()
2020-04-13 20:36:03 +02:00
. arg ( "-f" )
. arg ( "vars.conf.txt" )
2019-04-28 10:19:14 -05:00
. arg ( "FOO=bar.alt" )
2025-03-09 16:53:56 +01:00
. succeeds ()
2021-04-05 23:03:43 +03:00
. stdout_move_str ();
2019-04-28 10:19:14 -05:00
assert_eq! ( out . lines (). filter ( |& line | line == "FOO=bar.alt" ). count (), 1 );
}
#[test]
fn test_combined_file_set_unset () {
let out = new_ucmd! ()
2020-04-13 20:36:03 +02:00
. arg ( "-u" )
. arg ( "BAR" )
. arg ( "-f" )
. arg ( "vars.conf.txt" )
2019-04-28 10:19:14 -05:00
. arg ( "FOO=bar.alt" )
2021-04-05 23:03:43 +03:00
. succeeds ()
. stdout_move_str ();
2019-04-28 10:19:14 -05:00
2020-04-13 20:36:03 +02:00
assert_eq! (
out . lines ()
. filter ( |& line | line == "FOO=bar.alt" || line . starts_with ( "BAR=" ))
. count (),
1
);
2019-04-28 10:19:14 -05:00
}
2021-11-02 19:32:41 -03:00
#[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" }
2022-04-02 10:47:37 +02:00
for var in [ "" , "a=b" ] {
2025-03-09 16:53:56 +01:00
new_ucmd! (). arg ( "-u" ). arg ( var ). fails (). stderr_only ( format! (
2023-01-05 21:09:15 +01:00
"env: cannot unset {} : Invalid argument \n " ,
2021-11-02 19:32:41 -03:00
var . quote ()
));
}
}
2015-11-16 00:25:01 -05:00
#[test]
fn test_single_name_value_pair () {
2025-03-09 16:53:56 +01:00
new_ucmd! ()
. arg ( "FOO=bar" )
. succeeds ()
. stdout_str ()
. lines ()
. any ( | line | line == "FOO=bar" );
2015-11-16 00:25:01 -05:00
}
#[test]
fn test_multiple_name_value_pairs () {
2025-03-09 16:53:56 +01:00
let out = new_ucmd! (). arg ( "FOO=bar" ). arg ( "ABC=xyz" ). succeeds ();
2015-11-16 00:25:01 -05:00
2020-04-13 20:36:03 +02:00
assert_eq! (
2021-04-17 11:22:49 +02:00
out . stdout_str ()
. lines ()
2020-04-13 20:36:03 +02:00
. filter ( |& line | line == "FOO=bar" || line == "ABC=xyz" )
. count (),
2
);
2015-11-16 00:25:01 -05:00
}
#[test]
fn test_ignore_environment () {
2016-08-23 07:52:43 -04:00
let scene = TestScenario ::new ( util_name! ());
2015-11-16 00:25:01 -05:00
2022-10-13 19:59:10 +02:00
scene . ucmd (). arg ( "-i" ). succeeds (). no_stdout ();
scene . ucmd (). arg ( "-" ). succeeds (). no_stdout ();
2015-11-16 00:25:01 -05:00
}
2021-10-25 14:45:29 -03:00
#[test]
fn test_empty_name () {
new_ucmd! ()
. arg ( "-i" )
. arg ( "=xyz" )
2025-03-09 16:53:56 +01:00
. succeeds ()
2023-01-05 21:09:15 +01:00
. stderr_only ( "env: warning: no name specified for value 'xyz' \n " );
2021-10-25 14:45:29 -03:00
}
2015-11-16 00:25:01 -05:00
#[test]
fn test_null_delimiter () {
2016-08-23 07:52:43 -04:00
let out = new_ucmd! ()
2020-04-13 20:36:03 +02:00
. arg ( "-i" )
. arg ( "--null" )
. arg ( "FOO=bar" )
. arg ( "ABC=xyz" )
2021-04-05 23:03:43 +03:00
. succeeds ()
. stdout_move_str ();
2015-11-16 00:25:01 -05:00
2020-04-13 20:36:03 +02:00
let mut vars : Vec < _ > = out . split ( '\0' ). collect ();
2015-12-22 12:44:05 +01:00
assert_eq! ( vars . len (), 3 );
2021-05-29 14:32:35 +02:00
vars . sort_unstable ();
2015-12-22 12:44:05 +01:00
assert_eq! ( vars [ 0 ], "" );
assert_eq! ( vars [ 1 ], "ABC=xyz" );
assert_eq! ( vars [ 2 ], "FOO=bar" );
2015-11-16 00:25:01 -05:00
}
#[test]
fn test_unset_variable () {
2016-08-23 07:52:43 -04:00
let out = TestScenario ::new ( util_name! ())
2023-01-25 03:40:39 +01:00
. ucmd ()
2023-03-24 10:52:02 +08:00
. env ( "HOME" , "FOO" )
2020-04-13 20:36:03 +02:00
. arg ( "-u" )
. arg ( "HOME" )
2021-04-05 23:03:43 +03:00
. succeeds ()
. stdout_move_str ();
2015-11-16 00:25:01 -05:00
2021-05-29 14:32:35 +02:00
assert! ( ! out . lines (). any ( | line | line . starts_with ( "HOME=" )));
2015-11-16 00:25:01 -05:00
}
2019-04-28 11:12:37 +02:00
#[test]
fn test_fail_null_with_program () {
2021-04-22 22:37:44 +02:00
new_ucmd! ()
. arg ( "--null" )
. arg ( "cd" )
. fails ()
. stderr_contains ( "cannot specify --null (-0) with command" );
2019-04-28 11:12:37 +02:00
}
2020-08-03 11:32:31 -04:00
#[cfg(not(windows))]
#[test]
fn test_change_directory () {
let scene = TestScenario ::new ( util_name! ());
let temporary_directory = tempdir (). unwrap ();
2021-11-02 20:06:59 -03:00
let temporary_path = std ::fs ::canonicalize ( temporary_directory . path ()). unwrap ();
2020-08-03 11:32:31 -04:00
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 )
2021-04-05 23:03:43 +03:00
. succeeds ()
. stdout_move_str ();
2022-01-30 13:55:03 +01:00
assert_eq! ( out . trim (), temporary_path . as_os_str ());
2020-08-03 11:32:31 -04:00
}
#[cfg(windows)]
#[test]
fn test_change_directory () {
let scene = TestScenario ::new ( util_name! ());
let temporary_directory = tempdir (). unwrap ();
2021-11-02 20:06:59 -03:00
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" ];
2020-08-03 11:32:31 -04:00
let out = scene
. ucmd ()
. arg ( "--chdir" )
2022-11-04 11:10:35 +01:00
. arg ( temporary_path )
2021-11-02 20:06:59 -03:00
. args ( & pwd )
2021-04-05 23:03:43 +03:00
. succeeds ()
. stdout_move_str ();
2022-01-30 21:25:09 +01:00
assert_eq! ( out . trim (), temporary_path . as_os_str ());
2020-08-03 11:32:31 -04:00
}
#[test]
fn test_fail_change_directory () {
let scene = TestScenario ::new ( util_name! ());
let some_non_existing_path = "some_nonexistent_path" ;
2021-05-29 14:32:35 +02:00
assert! ( ! Path ::new ( some_non_existing_path ). is_dir ());
2020-08-03 11:32:31 -04:00
let out = scene
. ucmd ()
. arg ( "--chdir" )
. arg ( some_non_existing_path )
. arg ( "pwd" )
. fails ()
2021-04-05 23:03:43 +03:00
. stderr_move_str ();
2020-08-03 11:32:31 -04:00
assert! ( out . contains ( "env: cannot change directory to " ));
}
2024-03-14 19:38:28 +01:00
#[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 ,
2025-06-17 21:50:45 +02:00
"env: ' \\ c' must not appear in double-quoted -S string at position 2 \n "
2024-03-14 19:38:28 +01:00
);
}
#[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 \n xBx \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 \n xBx \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 ()
2024-03-30 21:19:35 +01:00
. args ( & [ "-vvS printf x%sx \\ n A \t B \x0B\x0C\r\n " ])
2024-03-14 19:38:28 +01:00
. succeeds ();
assert_eq! ( out . stdout_str (), "xAx \n xBx \n " );
2024-03-30 21:19:35 +01:00
assert_eq! (
out . stderr_str (),
"input args: \n arg[0]: 'env' \n arg[1]: $ \
'-vvS printf x%sx \\\\ n A \\ t B \\ x0B \\ x0C \\ r \\ n' \n executing: printf \
\n arg[0]= 'printf' \n arg[1]= $'x%sx \\ n' \n arg[2]= 'A' \n arg[3]= 'B' \n "
);
2024-03-14 19:38:28 +01:00
}
// 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! ());
2025-03-28 09:51:51 +01:00
let env_bin = String ::from ( uutests ::util ::get_tests_binary ()) + " " + util_name! ();
2025-04-03 15:41:54 +02:00
let input = [
String ::from ( "-i" ),
String ::from ( r #"-SA="B\_C=D" "# ) + env_bin . escape_default (). to_string (). as_str () + "" ,
];
2024-03-14 19:38:28 +01:00
2025-04-03 15:41:54 +02:00
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 " );
}
2024-03-14 19:38:28 +01:00
let out = scene . ucmd (). args ( & input ). succeeds ();
assert_eq! ( out . stdout_str (), output );
}
#[test]
2024-03-23 16:49:35 +08:00
#[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign
2024-04-22 14:55:14 +02:00
fn test_env_parsing_errors () {
2024-03-14 19:38:28 +01:00
let ts = TestScenario ::new ( util_name! ());
2024-04-22 14:55:14 +02:00
ts . ucmd ()
. arg ( "-S \\ |echo hallo" ) // no quotes, invalid escape sequence |
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ |' in -S at position 1 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
. arg ( "-S \\ a" ) // no quotes, invalid escape sequence a
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ a' in -S at position 1 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
. arg ( "-S \"\\ a \" " ) // double quotes, invalid escape sequence a
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ a' in -S at position 2 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
. arg ( r #"-S"\a""# ) // same as before, just using r#""#
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ a' in -S at position 2 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
. arg ( "-S' \\ a'" ) // single quotes, invalid escape sequence a
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ a' in -S at position 2 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
2024-06-30 16:27:08 +02:00
. arg ( r "-S\|\&\;" ) // no quotes, invalid escape sequence |
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ |' in -S at position 1 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
2024-06-30 16:27:08 +02:00
. arg ( r "-S\<\&\;" ) // no quotes, invalid escape sequence <
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ <' in -S at position 1 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
2024-06-30 16:27:08 +02:00
. arg ( r "-S\>\&\;" ) // no quotes, invalid escape sequence >
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ >' in -S at position 1 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
2024-06-30 16:27:08 +02:00
. arg ( r "-S\`\&\;" ) // no quotes, invalid escape sequence `
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ `' in -S at position 1 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
. arg ( r #"-S"\`\&\;""# ) // double quotes, invalid escape sequence `
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ `' in -S at position 2 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
2024-06-30 16:27:08 +02:00
. arg ( r "-S'\`\&\;'" ) // single quotes, invalid escape sequence `
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ `' in -S at position 2 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
2024-06-30 16:27:08 +02:00
. arg ( r "-S\`" ) // ` escaped without quotes
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ `' in -S at position 1 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
. arg ( r #"-S"\`""# ) // ` escaped in double quotes
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ `' in -S at position 2 \n " );
2024-03-14 19:38:28 +01:00
2024-04-22 14:55:14 +02:00
ts . ucmd ()
2024-06-30 16:27:08 +02:00
. arg ( r "-S'\`'" ) // ` escaped in single quotes
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\ `' in -S at position 2 \n " );
2024-03-14 19:38:28 +01:00
ts . ucmd ()
2024-06-30 16:27:08 +02:00
. args ( & [ r "-S\🦉" ]) // ` escaped in single quotes
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
2025-06-17 21:50:45 +02:00
. stderr_is ( "env: invalid sequence ' \\\u{FFFD} ' in -S at position 1 \n " ); // gnu doesn't show the owl. Instead a invalid unicode ?
2024-03-14 19:38:28 +01:00
}
#[test]
2024-04-22 14:55:14 +02:00
fn test_env_with_empty_executable_single_quotes () {
2024-03-14 19:38:28 +01:00
let ts = TestScenario ::new ( util_name! ());
ts . ucmd ()
. args ( & [ "-S''" ]) // empty single quotes, considered as program name
2025-03-01 15:29:11 +01:00
. fails_with_code ( 127 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
. stderr_is ( "env: '': No such file or directory \n " ); // gnu version again adds escaping here
}
#[test]
2024-04-22 14:55:14 +02:00
fn test_env_with_empty_executable_double_quotes () {
2024-03-14 19:38:28 +01:00
let ts = TestScenario ::new ( util_name! ());
2024-04-22 14:55:14 +02:00
ts . ucmd ()
. args ( & [ "-S \"\" " ]) // empty double quotes, considered as program name
2025-03-01 15:29:11 +01:00
. fails_with_code ( 127 )
2024-03-14 19:38:28 +01:00
. no_stdout ()
. stderr_is ( "env: '': No such file or directory \n " );
}
2026-02-10 01:11:37 +09:00
// Do not assume that coreutils uses argv0
2024-03-30 21:19:35 +01:00
#[test]
2026-02-10 01:11:37 +09:00
#[cfg(unix)]
2024-03-30 21:19:35 +01:00
fn test_env_overwrite_arg0 () {
let ts = TestScenario ::new ( util_name! ());
ts . ucmd ()
2026-02-10 01:11:37 +09:00
. args ( & [ "--argv0" , "hijacked" , "sh" , "-c" , "echo $0" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
}
2026-02-10 01:11:37 +09:00
// Do not assume that coreutils uses argv0
2024-03-30 21:19:35 +01:00
#[test]
2026-02-10 01:11:37 +09:00
#[cfg(unix)]
2024-03-30 21:19:35 +01:00
fn test_env_arg_argv0_overwrite () {
let ts = TestScenario ::new ( util_name! ());
// overwrite --argv0 by --argv0
ts . ucmd ()
. args ( & [ "--argv0" , "dirname" ])
2026-02-10 01:11:37 +09:00
. args ( & [ "--argv0" , "hijacked" , "sh" , "-c" , "echo $0" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
// overwrite -a by -a
ts . ucmd ()
. args ( & [ "-a" , "dirname" ])
2026-02-10 01:11:37 +09:00
. args ( & [ "-a" , "hijacked" , "sh" , "-c" , "echo $0" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
// overwrite --argv0 by -a
ts . ucmd ()
. args ( & [ "--argv0" , "dirname" ])
2026-02-10 01:11:37 +09:00
. args ( & [ "-a" , "hijacked" , "sh" , "-c" , "echo $0" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
// overwrite -a by --argv0
ts . ucmd ()
. args ( & [ "-a" , "dirname" ])
2026-02-10 01:11:37 +09:00
. args ( & [ "--argv0" , "hijacked" , "sh" , "-c" , "echo $0" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
}
2026-02-10 01:11:37 +09:00
// Do not assume that coreutils uses argv0
2024-03-30 21:19:35 +01:00
#[test]
2026-02-10 01:11:37 +09:00
#[cfg(unix)]
2024-03-30 21:19:35 +01:00
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" ])
2026-02-10 01:11:37 +09:00
. args ( & [ "--argv0" , "hijacked" , "sh" , "-c" , "echo $0" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
// normal following string arg
ts . ucmd ()
. args ( & [ "-a" , "dirname" ])
2026-02-10 01:11:37 +09:00
. args ( & [ "-S-a hijacked sh -c 'echo $0'" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
// one large string arg
ts . ucmd ()
2026-02-10 01:11:37 +09:00
. args ( & [ "-S--argv0 dirname -a hijacked sh -c 'echo $0'" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
// two string args
ts . ucmd ()
. args ( & [ "-S-a dirname" ])
2026-02-10 01:11:37 +09:00
. args ( & [ "-S--argv0 hijacked sh -c 'echo $0'" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
// three args: normal, string, normal
ts . ucmd ()
. args ( & [ "-a" , "sleep" ])
. args ( & [ "-S-a dirname" ])
2026-02-10 01:11:37 +09:00
. args ( & [ "-a" , "hijacked" , "sh" , "-c" , "echo $0" ])
2024-03-30 21:19:35 +01:00
. succeeds ()
2026-02-10 01:11:37 +09:00
. stdout_is ( "hijacked \n " )
2024-03-30 21:19:35 +01:00
. stderr_is ( "" );
}
2024-05-23 23:01:39 +03:00
#[test]
#[cfg(unix)]
fn test_env_arg_ignore_signal_invalid_signals () {
let ts = TestScenario ::new ( util_name! ());
ts . ucmd ()
. args ( & [ "--ignore-signal=banana" ])
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-05-23 23:01:39 +03:00
. stderr_contains ( "env: 'banana': invalid signal" );
ts . ucmd ()
. args ( & [ "--ignore-signal=SIGbanana" ])
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-05-23 23:01:39 +03:00
. stderr_contains ( "env: 'SIGbanana': invalid signal" );
ts . ucmd ()
. args ( & [ "--ignore-signal=exit" ])
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-05-23 23:01:39 +03:00
. stderr_contains ( "env: 'exit': invalid signal" );
ts . ucmd ()
. args ( & [ "--ignore-signal=SIGexit" ])
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-05-23 23:01:39 +03:00
. 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" ])
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-05-23 23:01:39 +03:00
. 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" ])
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-05-23 23:01:39 +03:00
. 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" ])
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-05-23 23:01:39 +03:00
. 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" ])
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-05-23 23:01:39 +03:00
. 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" );
}
2024-12-31 01:16:14 -07:00
2025-12-27 08:16:36 +09:00
#[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 );
}
}
}
2024-12-31 01:16:14 -07:00
#[test]
fn disallow_equals_sign_on_short_unset_option () {
let ts = TestScenario ::new ( util_name! ());
ts . ucmd ()
. arg ( "-u=" )
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-12-31 01:16:14 -07:00
. stderr_contains ( "env: cannot unset '=': Invalid argument" );
ts . ucmd ()
. arg ( "-u=A1B2C3" )
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-12-31 01:16:14 -07:00
. stderr_contains ( "env: cannot unset '=A1B2C3': Invalid argument" );
ts . ucmd (). arg ( "--split-string=A1B=2C3=" ). succeeds ();
ts . ucmd ()
. arg ( "--unset=" )
2025-03-01 15:29:11 +01:00
. fails_with_code ( 125 )
2024-12-31 01:16:14 -07:00
. stderr_contains ( "env: cannot unset '': Invalid argument" );
}
2024-03-14 19:38:28 +01:00
#[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 ;
}
2025-01-23 21:49:13 +00:00
_ => (),
2024-03-14 19:38:28 +01:00
}
}
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.
2025-08-07 14:44:10 +02:00
pub fn quote ( s : & str ) -> std ::borrow ::Cow < '_ , str > {
2024-03-14 19:38:28 +01:00
// 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 (),
2024-09-19 17:56:27 -04:00
EscapeStyle ::SingleQuoted => format! ( "' {s} '" ). into (),
2024-03-14 19:38:28 +01:00
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 < I , S > ( words : I ) -> String
where
I : IntoIterator < Item = S > ,
S : AsRef < str > ,
{
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 ;
2025-03-30 11:21:57 +02:00
use env ::{
EnvError ,
native_int_str ::{ Convert , NCvt , from_native_int_representation_owned },
};
2024-03-14 19:38:28 +01:00
2025-03-30 11:21:57 +02:00
fn split ( input : & str ) -> Result < Vec < OsString > , EnvError > {
2024-03-14 19:38:28 +01:00
::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! (
2024-09-19 17:56:27 -04:00
"[ {i} ] calling split( {input:?} ): \n expected: Ok( {expected:?} ) \n actual: Err( {actual:?} ) \n "
2024-03-14 19:38:28 +01:00
);
}
Ok ( actual ) => {
2025-04-08 13:25:12 -04:00
assert_eq! (
expected ,
actual . as_slice (),
2024-09-19 17:56:27 -04:00
"[{i}] After split({input:?}).unwrap() \n expected: {expected:?} \n actual: {actual:?} \n "
2024-03-14 19:38:28 +01:00
);
}
}
}
}
#[test]
fn split_empty () {
split_ok ( & [( "" , & [])]);
}
#[test]
fn split_initial_whitespace_is_removed () {
split_ok ( & [
( " a" , & [ "a" ]),
( " \t\t\t\t bar" , & [ "bar" ]),
( " \t \n c" , & [ "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 \r a \r ' \r ' \r " , & [ "c" , "a" , " \r " ])]);
}
#[test]
fn split_ () {
split_ok ( & [( " \\ ' \\ '" , & [ "''" ])]);
}
#[test]
fn split_single_quotes () {
split_ok ( & [
2024-06-30 16:27:08 +02:00
( r "''" , & [ r "" ]),
( r "'a'" , & [ r "a" ]),
( r "'\\'" , & [ r "\" ]),
( r "' \\ '" , & [ r " \ " ]),
( r "'#'" , & [ r "#" ]),
2024-03-14 19:38:28 +01:00
]);
}
#[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 ( & [
2024-06-30 16:27:08 +02:00
( r "\\|\\&\\;" , & [ r "\|\&\;" ]),
( r "\\<\\>" , & [ r "\<\>" ]),
( r "\\(\\)" , & [ r "\(\)" ]),
( r "\$" , & [ r "$" ]),
2024-03-14 19:38:28 +01:00
( r #"\""# , & [ r #"""# ]),
2024-06-30 16:27:08 +02:00
( r "\'" , & [ r "'" ]),
2024-03-14 19:38:28 +01:00
( " \\\n " , & []),
( " \\\n \n " , & []),
( "a \n b \n c" , & [ "a" , "b" , "c" ]),
( "a \\\n b \\\n c" , & [ "abc" ]),
( "foo bar baz" , & [ "foo" , "bar" , "baz" ]),
]);
}
#[test]
fn split_trailing_backslash () {
assert_eq! (
split ( " \\ " ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvInvalidBackslashAtEndOfStringInMinusS (
1 ,
"Delimiter" . into ()
))
2024-03-14 19:38:28 +01:00
);
assert_eq! (
split ( " \\ " ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvInvalidBackslashAtEndOfStringInMinusS (
2 ,
"Delimiter" . into ()
))
2024-03-14 19:38:28 +01:00
);
assert_eq! (
split ( "a \\ " ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvInvalidBackslashAtEndOfStringInMinusS (
2 ,
"Unquoted" . into ()
))
2024-03-14 19:38:28 +01:00
);
}
#[test]
fn split_errors () {
assert_eq! (
split ( "'abc" ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvMissingClosingQuote ( 4 , '\'' ))
2024-03-14 19:38:28 +01:00
);
2025-03-30 11:21:57 +02:00
assert_eq! ( split ( " \" " ), Err ( EnvError ::EnvMissingClosingQuote ( 1 , '"' )));
assert_eq! ( split ( "' \\ " ), Err ( EnvError ::EnvMissingClosingQuote ( 2 , '\'' )));
assert_eq! ( split ( "' \\ " ), Err ( EnvError ::EnvMissingClosingQuote ( 2 , '\'' )));
2024-03-14 19:38:28 +01:00
assert_eq! (
split ( r #""$""# ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvParsingOfMissingVariable ( 2 )),
2024-03-14 19:38:28 +01:00
);
}
#[test]
fn split_error_fail_with_unknown_escape_sequences () {
assert_eq! (
split ( " \\ a" ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvInvalidSequenceBackslashXInMinusS ( 1 , 'a' ))
2024-03-14 19:38:28 +01:00
);
assert_eq! (
split ( " \"\\ a \" " ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvInvalidSequenceBackslashXInMinusS ( 2 , 'a' ))
2024-03-14 19:38:28 +01:00
);
assert_eq! (
split ( "' \\ a'" ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvInvalidSequenceBackslashXInMinusS ( 2 , 'a' ))
2024-03-14 19:38:28 +01:00
);
assert_eq! (
split ( r #""\a""# ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvInvalidSequenceBackslashXInMinusS ( 2 , 'a' ))
2024-03-14 19:38:28 +01:00
);
assert_eq! (
2024-06-30 16:27:08 +02:00
split ( r "\🦉" ),
2025-03-30 11:21:57 +02:00
Err ( EnvError ::EnvInvalidSequenceBackslashXInMinusS (
1 , '\u{FFFD}'
))
2024-03-14 19:38:28 +01:00
);
}
#[test]
fn split_comments () {
split_ok ( & [
2024-06-30 16:27:08 +02:00
( r " x # comment " , & [ "x" ]),
( r " w1#w2 " , & [ "w1#w2" ]),
( r "'not really a # comment'" , & [ "not really a # comment" ]),
2024-03-14 19:38:28 +01:00
( " 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' \n Y" ), "'X' \\ '' \n Y'" );
}
#[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 ::{
2025-03-24 21:04:32 +01:00
NativeStr , from_native_int_representation , from_native_int_representation_owned ,
to_native_int_representation ,
2024-03-14 19:38:28 +01:00
},
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 = [ 0 u16 ; 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 = [ 0 u8 ; 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
);
}
}
2025-03-28 09:51:51 +01:00
#[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 \n stdout is not a tty \n stderr 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\n terminal size: 30 80 \r\n stdout is a tty \r\n stderr 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 \n terminal size: 30 80 \n stdout is not a tty \n stderr 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\n stdout is a tty \r\n stderr 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 \n stdout is not a tty \n stderr 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\n terminal size: 10 40 \r\n stdout is a tty \r\n stderr 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 ()), "" );
}
2025-09-03 22:00:28 +02:00
#[test]
#[cfg(not(windows))]
fn test_emoji_env_vars () {
new_ucmd! ()
. arg ( "🎯_VAR=Hello 🌍" )
. arg ( "printenv" )
. arg ( "🎯_VAR" )
. succeeds ()
. stdout_contains ( "Hello 🌍" );
}
2025-03-28 09:51:51 +01:00
#[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 ()), "" );
}
2025-08-24 20:52:49 +02:00
#[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" );
}
2025-11-23 15:21:53 -05:00
#[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: '?'" );
}
2025-12-19 18:24:01 +00:00
#[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 \x80 world" . to_vec ());
new_ucmd! ()
. env ( "NON_UTF8_VAR" , & non_utf8_value )
. succeeds ()
. stdout_contains_bytes ( b "NON_UTF8_VAR=hello \x80 world" );
}
2026-02-10 00:33:43 +01:00
#[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}"
);
}