use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[allow(clippy::enum_variant_names)] pub enum Shell {
#[default]
Sh,
Bash,
Zsh,
Fish,
Cmd,
#[serde(alias = "pwsh")]
PowerShell,
}
impl Shell {
#[cfg(unix)]
pub fn default_for_platform() -> Self {
Shell::Sh
}
#[cfg(windows)]
pub fn default_for_platform() -> Self {
Shell::Cmd
}
pub fn program(&self) -> &'static str {
match self {
Shell::Sh => "sh",
Shell::Bash => "bash",
Shell::Zsh => "zsh",
Shell::Fish => "fish",
Shell::Cmd => "cmd",
Shell::PowerShell => {
#[cfg(windows)]
{
"powershell"
}
#[cfg(not(windows))]
{
"pwsh"
}
}
}
}
pub fn exec_args(&self, command: &str) -> Vec<String> {
match self {
Shell::Sh | Shell::Bash | Shell::Zsh => {
vec!["-c".to_string(), command.to_string()]
}
Shell::Fish => {
vec!["-c".to_string(), command.to_string()]
}
Shell::Cmd => {
vec!["/C".to_string(), command.to_string()]
}
Shell::PowerShell => {
vec!["-Command".to_string(), command.to_string()]
}
}
}
pub fn command(&self, cmd: &str) -> tokio::process::Command {
let mut command = tokio::process::Command::new(self.program());
command.shell_script(self.program(), &self.exec_options(), cmd);
command
}
#[allow(dead_code)] pub fn std_command(&self, cmd: &str) -> std::process::Command {
let mut command = std::process::Command::new(self.program());
command.shell_script(self.program(), &self.exec_options(), cmd);
command
}
fn exec_options(&self) -> Vec<String> {
let mut args = self.exec_args("");
args.pop();
args
}
}
pub(crate) trait HideConsoleWindow {
fn hide_console_window(&mut self) -> &mut Self;
}
#[cfg(windows)]
fn child_would_get_its_own_console() -> bool {
let console = unsafe { windows_sys::Win32::System::Console::GetConsoleWindow() };
console.is_null()
}
#[cfg(windows)]
impl HideConsoleWindow for std::process::Command {
fn hide_console_window(&mut self) -> &mut Self {
use std::os::windows::process::CommandExt;
if child_would_get_its_own_console() {
self.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW)
} else {
self
}
}
}
#[cfg(windows)]
impl HideConsoleWindow for tokio::process::Command {
fn hide_console_window(&mut self) -> &mut Self {
if child_would_get_its_own_console() {
self.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW)
} else {
self
}
}
}
#[cfg(not(windows))]
impl HideConsoleWindow for std::process::Command {
fn hide_console_window(&mut self) -> &mut Self {
self
}
}
#[cfg(not(windows))]
impl HideConsoleWindow for tokio::process::Command {
fn hide_console_window(&mut self) -> &mut Self {
self
}
}
pub(crate) trait ShellScript {
fn shell_script(&mut self, program: &str, options: &[String], script: &str) -> &mut Self;
}
#[cfg(any(windows, test))]
fn cmd_raw_script<'a>(
program: &str,
options: &'a [String],
script: &str,
) -> Option<(&'a [String], String)> {
let name = program.rsplit(['/', '\\']).next().unwrap_or(program);
let stem = name.rsplit_once('.').map_or(name, |(stem, _)| stem);
let is_cmd = stem.eq_ignore_ascii_case("cmd");
let (flag, leading) = options.split_last()?;
if !is_cmd || !flag.eq_ignore_ascii_case("/c") {
return None;
}
let strip = if leading.iter().any(|o| o.eq_ignore_ascii_case("/s")) {
""
} else {
"/S "
};
Some((leading, format!("{strip}{flag} \"{script}\"")))
}
#[cfg(windows)]
impl ShellScript for std::process::Command {
fn shell_script(&mut self, program: &str, options: &[String], script: &str) -> &mut Self {
use std::os::windows::process::CommandExt;
match cmd_raw_script(program, options, script) {
Some((leading, raw)) => self.args(leading).raw_arg(raw),
None => self.args(options).arg(script),
}
}
}
#[cfg(windows)]
impl ShellScript for tokio::process::Command {
fn shell_script(&mut self, program: &str, options: &[String], script: &str) -> &mut Self {
match cmd_raw_script(program, options, script) {
Some((leading, raw)) => self.args(leading).raw_arg(raw),
None => self.args(options).arg(script),
}
}
}
#[cfg(not(windows))]
impl ShellScript for std::process::Command {
fn shell_script(&mut self, _program: &str, options: &[String], script: &str) -> &mut Self {
self.args(options).arg(script)
}
}
#[cfg(not(windows))]
impl ShellScript for tokio::process::Command {
fn shell_script(&mut self, _program: &str, options: &[String], script: &str) -> &mut Self {
self.args(options).arg(script)
}
}
impl std::fmt::Display for Shell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Shell::Sh => write!(f, "sh"),
Shell::Bash => write!(f, "bash"),
Shell::Zsh => write!(f, "zsh"),
Shell::Fish => write!(f, "fish"),
Shell::Cmd => write!(f, "cmd"),
Shell::PowerShell => write!(f, "powershell"),
}
}
}
impl std::str::FromStr for Shell {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"sh" => Ok(Shell::Sh),
"bash" => Ok(Shell::Bash),
"zsh" => Ok(Shell::Zsh),
"fish" => Ok(Shell::Fish),
"cmd" => Ok(Shell::Cmd),
"powershell" | "pwsh" => Ok(Shell::PowerShell),
_ => Err(format!("unknown shell: {s}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shell_program() {
assert_eq!(Shell::Sh.program(), "sh");
assert_eq!(Shell::Bash.program(), "bash");
assert_eq!(Shell::Zsh.program(), "zsh");
assert_eq!(Shell::Fish.program(), "fish");
assert_eq!(Shell::Cmd.program(), "cmd");
}
#[test]
fn test_shell_exec_args() {
assert_eq!(Shell::Sh.exec_args("echo hello"), vec!["-c", "echo hello"]);
assert_eq!(
Shell::Bash.exec_args("echo hello"),
vec!["-c", "echo hello"]
);
assert_eq!(Shell::Cmd.exec_args("echo hello"), vec!["/C", "echo hello"]);
assert_eq!(
Shell::PowerShell.exec_args("echo hello"),
vec!["-Command", "echo hello"]
);
}
#[test]
fn test_shell_from_str() {
assert_eq!("sh".parse::<Shell>().unwrap(), Shell::Sh);
assert_eq!("bash".parse::<Shell>().unwrap(), Shell::Bash);
assert_eq!("BASH".parse::<Shell>().unwrap(), Shell::Bash);
assert_eq!("powershell".parse::<Shell>().unwrap(), Shell::PowerShell);
assert_eq!("pwsh".parse::<Shell>().unwrap(), Shell::PowerShell);
assert!("unknown".parse::<Shell>().is_err());
}
#[test]
fn test_shell_display() {
assert_eq!(Shell::Sh.to_string(), "sh");
assert_eq!(Shell::Bash.to_string(), "bash");
assert_eq!(Shell::Cmd.to_string(), "cmd");
}
#[test]
fn test_default_shell() {
let default = Shell::default_for_platform();
#[cfg(unix)]
assert_eq!(default, Shell::Sh);
#[cfg(windows)]
assert_eq!(default, Shell::Cmd);
}
#[test]
fn test_hide_console_window() {
let program = if cfg!(windows) { "cmd" } else { "echo" };
let args: Vec<&str> = if cfg!(windows) {
vec!["/C", "echo hi"]
} else {
vec!["hi"]
};
let output = std::process::Command::new(program)
.args(&args)
.hide_console_window()
.output()
.expect("spawning the child should succeed");
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "hi");
let mut async_command = tokio::process::Command::new(program);
async_command.args(&args).hide_console_window();
}
fn strings(words: &[&str]) -> Vec<String> {
words.iter().map(|w| w.to_string()).collect()
}
#[test]
fn test_cmd_raw_script_wraps_the_script_for_cmd() {
let options = strings(&["/C"]);
let (leading, raw) = cmd_raw_script("cmd", &options, r#"echo "a b""#).unwrap();
assert!(leading.is_empty());
assert_eq!(raw, r#"/S /C "echo "a b"""#);
let options = strings(&["/d", "/s", "/c"]);
let (leading, raw) =
cmd_raw_script(r"C:\Windows\System32\CMD.EXE", &options, "echo hi").unwrap();
assert_eq!(leading, &options[..2]);
assert_eq!(raw, r#"/c "echo hi""#);
}
#[test]
fn test_cmd_raw_script_leaves_other_shells_alone() {
assert_eq!(cmd_raw_script("sh", &strings(&["-c"]), "echo hi"), None);
assert_eq!(
cmd_raw_script("pwsh", &strings(&["-Command"]), "echo hi"),
None
);
assert_eq!(cmd_raw_script("cmd", &strings(&["/K"]), "echo hi"), None);
assert_eq!(cmd_raw_script("cmd", &[], "echo hi"), None);
}
#[cfg(windows)]
#[test]
fn test_cmd_runs_a_script_with_quotes() {
let dir = tempfile::tempdir().unwrap();
let script_dir = dir.path().join("with space");
std::fs::create_dir_all(&script_dir).unwrap();
let script = script_dir.join("say.cmd");
std::fs::write(&script, "@echo [%~1]\r\n").unwrap();
let run = format!(r#""{}" "a b""#, script.display());
let output = std::process::Command::new("cmd")
.shell_script("cmd", &strings(&["/C"]), &run)
.output()
.unwrap();
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "[a b]");
let output = Shell::Cmd.std_command(r#"echo "a b""#).output().unwrap();
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), r#""a b""#);
}
}