use std::path::Path; use gix_testtools::Result; #[test]fn extract_interpreter() -> gix_testtools::Result { let root = gix_testtools::scripted_fixture_read_only("win_path_lookup.sh")?; assert_eq!( gix_command::extract_interpreter(&root.join("b").join("exe")), Some(gix_command::shebang::Data { interpreter: Path::new("/b/exe").into(), args: vec![] }) ); Ok(())} mod shebang { mod parse { use gix_command::shebang; fn parse(input: &str) -> Option<shebang::Data> { shebang::parse(input.into()) } fn exe(name: &str) -> Option<shebang::Data> { shebang::Data { interpreter: name.into(), args: Vec::new(), } .into() } fn exe_arg(name: &str, arg: &str) -> Option<shebang::Data> { shebang::Data { interpreter: name.into(), args: shell_words::split(arg) .expect("can parse") .into_iter() .map(Into::into) .collect(), } .into() } #[test] fn valid() { assert_eq!(parse("#!/bin/sh"), exe("/bin/sh")); assert_eq!(parse("#!/bin/sh "), exe("/bin/sh"), "trim trailing whitespace"); assert_eq!( parse("#!/bin/sh\t\nother"), exe("/bin/sh"), "trimming works for tabs as well" ); assert_eq!( parse(r"#!\bin\sh"), exe(r"\bin\sh"), "backslashes are recognized as path separator" ); assert_eq!( parse("#!C:\\Program Files\\shell.exe\r\nsome stuff"), exe(r"C:\Program Files\shell.exe"), "absolute windows paths are fine" ); assert_eq!( parse("#!/bin/sh -i -o -u\nunrelated content"), exe_arg("/bin/sh", "-i -o -u"), "argument splitting happens as well" ); assert_eq!( parse("#!/bin/sh -o\nunrelated content"), exe_arg("/bin/sh", "-o"), "single arguments are OK too" ); assert_eq!( parse("#!/bin/exe anything goes\nunrelated content"), exe_arg("/bin/exe", "anything goes"), "any argument works" ); #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; use bstr::ByteSlice; assert_eq!( shebang::parse(b"#!/bin/sh -x \xC3\x28\x41 -y ".as_bstr()), Some(shebang::Data { interpreter: "/bin/sh".into(), args: vec![std::ffi::OsStr::from_bytes(b"-x \xC3\x28\x41 -y").to_owned()] }), "illformed UTF8 in the arguments leads to them not being split - useful in case it's just one path or so" ); assert_eq!( shebang::parse(b"#!/bin/\xC3\x28\x41 ".as_bstr()), Some(shebang::Data { interpreter: std::ffi::OsStr::from_bytes(b"/bin/\xC3\x28\x41").to_owned().into(), args: vec![] }), "illformed UTF8 in the executable path is fine as well" ); } } #[test] fn invalid() { assert_eq!(parse(""), None); assert_eq!(parse("missing shebang"), None); assert_eq!(parse("#!missing-slash"), None); assert_eq!( parse("/bin/sh"), None, "shebang missing, even though a valid path is given" ); } }} mod context { use gix_command::Context; fn winfix(expected: impl Into<String>) -> String { // Unclear why it's not debug-printing the env on windows. if cfg!(windows) { "\"\"".into() } else { expected.into() } } #[test] fn git_dir_sets_git_dir_env_and_cwd() { let ctx = Context { git_dir: Some(".".into()), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_DIR="." """#)); } #[test] fn worktree_dir_sets_env_only() { let ctx = Context { worktree_dir: Some(".".into()), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_WORK_TREE="." """#)); } #[test] fn no_replace_objects_sets_env_only() { for value in [false, true] { let expected = usize::from(value); let ctx = Context { no_replace_objects: Some(value), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_NO_REPLACE_OBJECTS="{expected}" """#)) ); } } #[test] fn ref_namespace_sets_env_only() { let ctx = Context { ref_namespace: Some("namespace".into()), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_NAMESPACE="namespace" """#)); } #[test] fn literal_pathspecs_sets_env_only() { for value in [false, true] { let expected = usize::from(value); let ctx = Context { literal_pathspecs: Some(value), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_LITERAL_PATHSPECS="{expected}" """#)) ); } } #[test] fn glob_pathspecs_sets_env_only() { for (value, expected) in [ (false, r#"GIT_NOGLOB_PATHSPECS="1""#), (true, r#"GIT_GLOB_PATHSPECS="1""#), ] { let ctx = Context { glob_pathspecs: Some(value), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!(format!("{cmd:?}"), winfix(format!(r#"{expected} """#))); } } #[test] fn icase_pathspecs_sets_env_only() { for value in [false, true] { let expected = usize::from(value); let ctx = Context { icase_pathspecs: Some(value), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_ICASE_PATHSPECS="{expected}" """#)) ); } }} mod prepare { use std::sync::LazyLock; static SH: LazyLock<&'static str> = LazyLock::new(|| { gix_path::env::shell() .to_str() .expect("`prepare` tests must be run where 'sh' path is valid Unicode") }); // The basename of the default shell, used as the `command_name` operand // after `-c <script>` and observable inside the shell as `$0`. The default // shell is `gix_path::env::shell()`, documented as `/bin/sh` on Unix and a // path ending in `sh.exe` on Windows. const SH_BASENAME: &str = if cfg!(windows) { "sh.exe" } else { "sh" }; fn quoted(input: &[&str]) -> String { input.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(" ") } #[test] fn empty() { let cmd = std::process::Command::from(gix_command::prepare("")); assert_eq!(format!("{cmd:?}"), "\"\""); } #[test] fn whitespace_only_without_shell() { let cmd = std::process::Command::from(gix_command::prepare(" ")); assert_eq!(format!("{cmd:?}"), "\" \""); } #[test] fn whitespace_only_commands_with_auto_split_fall_back_to_shell() { let cmd = std::process::Command::from( gix_command::prepare(" ").command_may_be_shell_script_allow_manual_argument_splitting(), ); assert_eq!(format!("{cmd:?}"), quoted(&[*SH, "-c", " ", SH_BASENAME])); } #[test] fn single_and_multiple_arguments() { let cmd = std::process::Command::from(gix_command::prepare("ls").arg("first").args(["second", "third"])); assert_eq!(format!("{cmd:?}"), quoted(&["ls", "first", "second", "third"])); } #[test] fn multiple_arguments_in_one_line_with_auto_split() { let cmd = std::process::Command::from( gix_command::prepare("echo first second third") .command_may_be_shell_script_allow_manual_argument_splitting(), ); assert_eq!( format!("{cmd:?}"), quoted(&["echo", "first", "second", "third"]), "we split by hand which works unless one tries to rely on shell-builtins (which we can't detect)" ); } #[test] fn single_and_multiple_arguments_as_part_of_command() { let cmd = std::process::Command::from(gix_command::prepare("ls first second third")); assert_eq!( format!("{cmd:?}"), quoted(&["ls first second third"]), "without shell, this is an invalid command" ); } #[test] fn single_and_multiple_arguments_as_part_of_command_with_shell() { let cmd = std::process::Command::from(gix_command::prepare("ls first second third").command_may_be_shell_script()); assert_eq!( format!("{cmd:?}"), if cfg!(windows) { quoted(&["ls", "first", "second", "third"]) } else { quoted(&[*SH, "-c", "ls first second third", SH_BASENAME]) }, "with shell, this works as it performs word splitting" ); } #[test] fn single_and_multiple_arguments_as_part_of_command_with_given_shell() { let cmd = std::process::Command::from( gix_command::prepare("ls first second third") .command_may_be_shell_script() .with_shell_program("/somepath/to/bash"), ); assert_eq!( format!("{cmd:?}"), if cfg!(windows) { quoted(&["ls", "first", "second", "third"]) } else { quoted(&["/somepath/to/bash", "-c", "ls first second third", "bash"]) }, "with shell, this works as it performs word splitting on Windows, but on linux (or without splitting) it uses the given shell" ); } #[test] fn single_and_complex_arguments_as_part_of_command_with_shell() { let cmd = std::process::Command::from( gix_command::prepare(r#"ls --foo "a b""#) .arg("additional") .command_may_be_shell_script(), ); assert_eq!( format!("{cmd:?}"), if cfg!(windows) { quoted(&["ls", "--foo", "a b", "additional"]) } else { let sh = *SH; format!(r#""{sh}" "-c" "ls --foo \"a b\" \"$@\"" "{SH_BASENAME}" "additional""#) }, "with shell, this works as it performs word splitting, on windows we can avoid the shell" ); } #[test] fn single_and_complex_arguments_with_auto_split() { let cmd = std::process::Command::from( gix_command::prepare(r#"ls --foo="a b""#).command_may_be_shell_script_allow_manual_argument_splitting(), ); assert_eq!( format!("{cmd:?}"), format!(r#""ls" "--foo=a b""#), "splitting can also handle quotes" ); } #[test] fn single_and_complex_arguments_without_auto_split() { let cmd = std::process::Command::from( gix_command::prepare(r#"ls --foo="a b""#).command_may_be_shell_script_disallow_manual_argument_splitting(), ); assert_eq!( format!("{cmd:?}"), quoted(&[*SH, "-c", r#"ls --foo=\"a b\""#, SH_BASENAME]) ); } #[test] fn single_and_simple_arguments_without_auto_split_with_shell() { let cmd = std::process::Command::from(gix_command::prepare("ls").arg("--foo=a b").with_shell()); assert_eq!( format!("{cmd:?}"), quoted(&[*SH, "-c", r#"ls \"$@\""#, SH_BASENAME, "--foo=a b"]) ); } #[test] fn quoted_command_without_argument_splitting() { let cmd = std::process::Command::from( gix_command::prepare("ls") .arg("--foo=a b") .with_shell() .with_quoted_command(), ); assert_eq!( format!("{cmd:?}"), quoted(&[*SH, "-c", r#"'ls' \"$@\""#, SH_BASENAME, "--foo=a b"]), "looks strange thanks to debug printing, but is the right amount of quotes actually" ); } #[test] fn quoted_windows_command_without_argument_splitting() { let cmd = std::process::Command::from( gix_command::prepare(r"C:\Users\O'Shaughnessy\with space.exe") .arg("--foo='a b'") .with_shell() .with_quoted_command(), ); assert_eq!( format!("{cmd:?}"), quoted(&[ *SH, "-c", r#"'C:\\Users\\O'\\''Shaughnessy\\with space.exe' \"$@\""#, SH_BASENAME, r"--foo='a b'" ]), "again, a lot of extra backslashes, but it's correct outside of the debug formatting" ); } #[test] fn single_and_complex_arguments_will_not_auto_split_on_special_characters() { let cmd = std::process::Command::from( gix_command::prepare("ls --foo=~/path").command_may_be_shell_script_allow_manual_argument_splitting(), ); let sh = *SH; assert_eq!( format!("{cmd:?}"), format!(r#""{sh}" "-c" "ls --foo=~/path" "{SH_BASENAME}""#), "splitting can also handle quotes" ); } #[test] fn tilde_path_and_multiple_arguments_as_part_of_command_with_shell() { let cmd = std::process::Command::from(gix_command::prepare(r#"~/bin/exe --foo "a b""#).command_may_be_shell_script()); let sh = *SH; assert_eq!( format!("{cmd:?}"), format!(r#""{sh}" "-c" "~/bin/exe --foo \"a b\"" "{SH_BASENAME}""#), "this always needs a shell as we need tilde expansion" ); } #[test] fn script_with_dollar_at() { let cmd = std::process::Command::from( gix_command::prepare(r#"echo "$@" >&2"#) .command_may_be_shell_script() .arg("store"), ); let sh = *SH; assert_eq!( format!("{cmd:?}"), format!(r#""{sh}" "-c" "echo \"$@\" >&2" "{SH_BASENAME}" "store""#), "this is how credential helpers have to work as for some reason they don't get '$@' added in Git.\ We deal with it by not doubling the '$@' argument, which seems more flexible." ); } #[test] fn script_with_dollar_at_has_no_quoting() { let cmd = std::process::Command::from( gix_command::prepare(r#"echo "$@" >&2"#) .command_may_be_shell_script() .with_quoted_command() .arg("store"), ); let sh = *SH; assert_eq!( format!("{cmd:?}"), format!(r#""{sh}" "-c" "echo \"$@\" >&2" "{SH_BASENAME}" "store""#) ); } #[test] fn shell_program_with_no_basename_uses_underscore_placeholder() { // Defensive fallback for degenerate input that should not occur in // practice. If a caller passes a shell path whose `file_name()` is // `None` (empty string, `/`, etc.), the `command_name` operand falls // back to `_`, the conventional placeholder for an unused `$0` used // in shell one-liners. Such a "shell" would not produce a runnable // command — the fallback only keeps the construction total in the // face of bad input, without making a false claim about the shell. let cmd = std::process::Command::from( gix_command::prepare("echo hi") .command_may_be_shell_script_disallow_manual_argument_splitting() .with_shell_program(""), ); assert_eq!( format!("{cmd:?}"), quoted(&["", "-c", "echo hi", "_"]), "with no basename available, the command_name operand is '_', not a guessed shell name" ); }} mod spawn { use bstr::ByteSlice; #[test] fn environment_variables_are_passed_one_by_one() -> crate::Result { let out = gix_command::prepare("echo $FIRST $SECOND") .env("FIRST", "first") .env("SECOND", "second") .command_may_be_shell_script() .spawn()? .wait_with_output()?; assert_eq!(out.stdout.as_bstr(), "first second\n"); Ok(())Lines 1–500 of 717. Load more
use std::path::Path; use gix_testtools::Result; #[test] fn extract_interpreter() -> gix_testtools::Result { let root = gix_testtools::scripted_fixture_read_only("win_path_lookup.sh")?; assert_eq!( gix_command::extract_interpreter(&root.join("b").join("exe")), Some(gix_command::shebang::Data { interpreter: Path::new("/b/exe").into(), args: vec![] }) ); Ok(()) } mod shebang { mod parse { use gix_command::shebang; fn parse(input: &str) -> Option<shebang::Data> { shebang::parse(input.into()) } fn exe(name: &str) -> Option<shebang::Data> { shebang::Data { interpreter: name.into(), args: Vec::new(), } .into() } fn exe_arg(name: &str, arg: &str) -> Option<shebang::Data> { shebang::Data { interpreter: name.into(), args: shell_words::split(arg) .expect("can parse") .into_iter() .map(Into::into) .collect(), } .into() } #[test] fn valid() { assert_eq!(parse("#!/bin/sh"), exe("/bin/sh")); assert_eq!(parse("#!/bin/sh "), exe("/bin/sh"), "trim trailing whitespace"); assert_eq!( parse("#!/bin/sh\t\nother"), exe("/bin/sh"), "trimming works for tabs as well" ); assert_eq!( parse(r"#!\bin\sh"), exe(r"\bin\sh"), "backslashes are recognized as path separator" ); assert_eq!( parse("#!C:\\Program Files\\shell.exe\r\nsome stuff"), exe(r"C:\Program Files\shell.exe"), "absolute windows paths are fine" ); assert_eq!( parse("#!/bin/sh -i -o -u\nunrelated content"), exe_arg("/bin/sh", "-i -o -u"), "argument splitting happens as well" ); assert_eq!( parse("#!/bin/sh -o\nunrelated content"), exe_arg("/bin/sh", "-o"), "single arguments are OK too" ); assert_eq!( parse("#!/bin/exe anything goes\nunrelated content"), exe_arg("/bin/exe", "anything goes"), "any argument works" ); #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; use bstr::ByteSlice; assert_eq!( shebang::parse(b"#!/bin/sh -x \xC3\x28\x41 -y ".as_bstr()), Some(shebang::Data { interpreter: "/bin/sh".into(), args: vec![std::ffi::OsStr::from_bytes(b"-x \xC3\x28\x41 -y").to_owned()] }), "illformed UTF8 in the arguments leads to them not being split - useful in case it's just one path or so" ); assert_eq!( shebang::parse(b"#!/bin/\xC3\x28\x41 ".as_bstr()), Some(shebang::Data { interpreter: std::ffi::OsStr::from_bytes(b"/bin/\xC3\x28\x41").to_owned().into(), args: vec![] }), "illformed UTF8 in the executable path is fine as well" ); } } #[test] fn invalid() { assert_eq!(parse(""), None); assert_eq!(parse("missing shebang"), None); assert_eq!(parse("#!missing-slash"), None); assert_eq!( parse("/bin/sh"), None, "shebang missing, even though a valid path is given" ); } } } mod context { use gix_command::Context; fn winfix(expected: impl Into<String>) -> String { // Unclear why it's not debug-printing the env on windows. if cfg!(windows) { "\"\"".into() } else { expected.into() } } #[test] fn git_dir_sets_git_dir_env_and_cwd() { let ctx = Context { git_dir: Some(".".into()), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_DIR="." """#)); } #[test] fn worktree_dir_sets_env_only() { let ctx = Context { worktree_dir: Some(".".into()), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_WORK_TREE="." """#)); } #[test] fn no_replace_objects_sets_env_only() { for value in [false, true] { let expected = usize::from(value); let ctx = Context { no_replace_objects: Some(value), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_NO_REPLACE_OBJECTS="{expected}" """#)) ); } } #[test] fn ref_namespace_sets_env_only() { let ctx = Context { ref_namespace: Some("namespace".into()), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_NAMESPACE="namespace" """#)); } #[test] fn literal_pathspecs_sets_env_only() { for value in [false, true] { let expected = usize::from(value); let ctx = Context { literal_pathspecs: Some(value), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_LITERAL_PATHSPECS="{expected}" """#)) ); } } #[test] fn glob_pathspecs_sets_env_only() { for (value, expected) in [ (false, r#"GIT_NOGLOB_PATHSPECS="1""#), (true, r#"GIT_GLOB_PATHSPECS="1""#), ] { let ctx = Context { glob_pathspecs: Some(value), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!(format!("{cmd:?}"), winfix(format!(r#"{expected} """#))); } } #[test] fn icase_pathspecs_sets_env_only() { for value in [false, true] { let expected = usize::from(value); let ctx = Context { icase_pathspecs: Some(value), ..Default::default() }; let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_ICASE_PATHSPECS="{expected}" """#)) ); } } } mod prepare { use std::sync::LazyLock; static SH: LazyLock<&'static str> = LazyLock::new(|| { gix_path::env::shell() .to_str() .expect("`prepare` tests must be run where 'sh' path is valid Unicode") }); // The basename of the default shell, used as the `command_name` operand // after `-c <script>` and observable inside the shell as `$0`. The default // shell is `gix_path::env::shell()`, documented as `/bin/sh` on Unix and a // path ending in `sh.exe` on Windows. const SH_BASENAME: &str = if cfg!(windows) { "sh.exe" } else { "sh" }; fn quoted(input: &[&str]) -> String { input.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(" ") } #[test] fn empty() { let cmd = std::process::Command::from(gix_command::prepare("")); assert_eq!(format!("{cmd:?}"), "\"\""); } #[test] fn whitespace_only_without_shell() { let cmd = std::process::Command::from(gix_command::prepare(" ")); assert_eq!(format!("{cmd:?}"), "\" \""); } #[test] fn whitespace_only_commands_with_auto_split_fall_back_to_shell() { let cmd = std::process::Command::from( gix_command::prepare(" ").command_may_be_shell_script_allow_manual_argument_splitting(), ); assert_eq!(format!("{cmd:?}"), quoted(&[*SH, "-c", " ", SH_BASENAME])); } #[test] fn single_and_multiple_arguments() { let cmd = std::process::Command::from(gix_command::prepare("ls").arg("first").args(["second", "third"])); assert_eq!(format!("{cmd:?}"), quoted(&["ls", "first", "second", "third"])); } #[test] fn multiple_arguments_in_one_line_with_auto_split() { let cmd = std::process::Command::from( gix_command::prepare("echo first second third") .command_may_be_shell_script_allow_manual_argument_splitting(), ); assert_eq!( format!("{cmd:?}"), quoted(&["echo", "first", "second", "third"]), "we split by hand which works unless one tries to rely on shell-builtins (which we can't detect)" ); } #[test] fn single_and_multiple_arguments_as_part_of_command() { let cmd = std::process::Command::from(gix_command::prepare("ls first second third")); assert_eq!( format!("{cmd:?}"), quoted(&["ls first second third"]), "without shell, this is an invalid command" ); } #[test] fn single_and_multiple_arguments_as_part_of_command_with_shell() { let cmd = std::process::Command::from(gix_command::prepare("ls first second third").command_may_be_shell_script()); assert_eq!( format!("{cmd:?}"), if cfg!(windows) { quoted(&["ls", "first", "second", "third"]) } else { quoted(&[*SH, "-c", "ls first second third", SH_BASENAME]) }, "with shell, this works as it performs word splitting" ); } #[test] fn single_and_multiple_arguments_as_part_of_command_with_given_shell() { let cmd = std::process::Command::from( gix_command::prepare("ls first second third") .command_may_be_shell_script() .with_shell_program("/somepath/to/bash"), ); assert_eq!( format!("{cmd:?}"), if cfg!(windows) { quoted(&["ls", "first", "second", "third"]) } else { quoted(&["/somepath/to/bash", "-c", "ls first second third", "bash"]) }, "with shell, this works as it performs word splitting on Windows, but on linux (or without splitting) it uses the given shell" ); } #[test] fn single_and_complex_arguments_as_part_of_command_with_shell() { let cmd = std::process::Command::from( gix_command::prepare(r#"ls --foo "a b""#) .arg("additional") .command_may_be_shell_script(), ); assert_eq!( format!("{cmd:?}"), if cfg!(windows) { quoted(&["ls", "--foo", "a b", "additional"]) } else { let sh = *SH; format!(r#""{sh}" "-c" "ls --foo \"a b\" \"$@\"" "{SH_BASENAME}" "additional""#) }, "with shell, this works as it performs word splitting, on windows we can avoid the shell" ); } #[test] fn single_and_complex_arguments_with_auto_split() { let cmd = std::process::Command::from( gix_command::prepare(r#"ls --foo="a b""#).command_may_be_shell_script_allow_manual_argument_splitting(), ); assert_eq!( format!("{cmd:?}"), format!(r#""ls" "--foo=a b""#), "splitting can also handle quotes" ); } #[test] fn single_and_complex_arguments_without_auto_split() { let cmd = std::process::Command::from( gix_command::prepare(r#"ls --foo="a b""#).command_may_be_shell_script_disallow_manual_argument_splitting(), ); assert_eq!( format!("{cmd:?}"), quoted(&[*SH, "-c", r#"ls --foo=\"a b\""#, SH_BASENAME]) ); } #[test] fn single_and_simple_arguments_without_auto_split_with_shell() { let cmd = std::process::Command::from(gix_command::prepare("ls").arg("--foo=a b").with_shell()); assert_eq!( format!("{cmd:?}"), quoted(&[*SH, "-c", r#"ls \"$@\""#, SH_BASENAME, "--foo=a b"]) ); } #[test] fn quoted_command_without_argument_splitting() { let cmd = std::process::Command::from( gix_command::prepare("ls") .arg("--foo=a b") .with_shell() .with_quoted_command(), ); assert_eq!( format!("{cmd:?}"), quoted(&[*SH, "-c", r#"'ls' \"$@\""#, SH_BASENAME, "--foo=a b"]), "looks strange thanks to debug printing, but is the right amount of quotes actually" ); } #[test] fn quoted_windows_command_without_argument_splitting() { let cmd = std::process::Command::from( gix_command::prepare(r"C:\Users\O'Shaughnessy\with space.exe") .arg("--foo='a b'") .with_shell() .with_quoted_command(), ); assert_eq!( format!("{cmd:?}"), quoted(&[ *SH, "-c", r#"'C:\\Users\\O'\\''Shaughnessy\\with space.exe' \"$@\""#, SH_BASENAME, r"--foo='a b'" ]), "again, a lot of extra backslashes, but it's correct outside of the debug formatting" ); } #[test] fn single_and_complex_arguments_will_not_auto_split_on_special_characters() { let cmd = std::process::Command::from( gix_command::prepare("ls --foo=~/path").command_may_be_shell_script_allow_manual_argument_splitting(), ); let sh = *SH; assert_eq!( format!("{cmd:?}"), format!(r#""{sh}" "-c" "ls --foo=~/path" "{SH_BASENAME}""#), "splitting can also handle quotes" ); } #[test] fn tilde_path_and_multiple_arguments_as_part_of_command_with_shell() { let cmd = std::process::Command::from(gix_command::prepare(r#"~/bin/exe --foo "a b""#).command_may_be_shell_script()); let sh = *SH; assert_eq!( format!("{cmd:?}"), format!(r#""{sh}" "-c" "~/bin/exe --foo \"a b\"" "{SH_BASENAME}""#), "this always needs a shell as we need tilde expansion" ); } #[test] fn script_with_dollar_at() { let cmd = std::process::Command::from( gix_command::prepare(r#"echo "$@" >&2"#) .command_may_be_shell_script() .arg("store"), ); let sh = *SH; assert_eq!( format!("{cmd:?}"), format!(r#""{sh}" "-c" "echo \"$@\" >&2" "{SH_BASENAME}" "store""#), "this is how credential helpers have to work as for some reason they don't get '$@' added in Git.\ We deal with it by not doubling the '$@' argument, which seems more flexible." ); } #[test] fn script_with_dollar_at_has_no_quoting() { let cmd = std::process::Command::from( gix_command::prepare(r#"echo "$@" >&2"#) .command_may_be_shell_script() .with_quoted_command() .arg("store"), ); let sh = *SH; assert_eq!( format!("{cmd:?}"), format!(r#""{sh}" "-c" "echo \"$@\" >&2" "{SH_BASENAME}" "store""#) ); } #[test] fn shell_program_with_no_basename_uses_underscore_placeholder() { // Defensive fallback for degenerate input that should not occur in // practice. If a caller passes a shell path whose `file_name()` is // `None` (empty string, `/`, etc.), the `command_name` operand falls // back to `_`, the conventional placeholder for an unused `$0` used // in shell one-liners. Such a "shell" would not produce a runnable // command — the fallback only keeps the construction total in the // face of bad input, without making a false claim about the shell. let cmd = std::process::Command::from( gix_command::prepare("echo hi") .command_may_be_shell_script_disallow_manual_argument_splitting() .with_shell_program(""), ); assert_eq!( format!("{cmd:?}"), quoted(&["", "-c", "echo hi", "_"]), "with no basename available, the command_name operand is '_', not a guessed shell name" ); } } mod spawn { use bstr::ByteSlice; #[test] fn environment_variables_are_passed_one_by_one() -> crate::Result { let out = gix_command::prepare("echo $FIRST $SECOND") .env("FIRST", "first") .env("SECOND", "second") .command_may_be_shell_script() .spawn()? .wait_with_output()?; assert_eq!(out.stdout.as_bstr(), "first second\n"); Ok(())