use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::{borrow::Cow, ffi::OsStr, path::Path, sync::LazyLock}; /// Shell configuration to open the terminal with.#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]#[serde(rename_all = "snake_case")]pub enum Shell { /// Use the system's default terminal configuration in /etc/passwd #[default] System, /// Use a specific program with no arguments. Program(String), /// Use a specific program with arguments. WithArguments { /// The program to run. program: String, /// The arguments to pass to the program. args: Vec<String>, /// An optional string to override the title of the terminal tab title_override: Option<String>, },} impl Shell { pub fn program(&self) -> String { match self { Shell::Program(program) => program.clone(), Shell::WithArguments { program, .. } => program.clone(), Shell::System => get_system_shell(), } } pub fn program_and_args(&self) -> (String, &[String]) { match self { Shell::Program(program) => (program.clone(), &[]), Shell::WithArguments { program, args, .. } => (program.clone(), args), Shell::System => (get_system_shell(), &[]), } } pub fn shell_kind(&self) -> Option<ShellKind> { match self { Shell::Program(program) => ShellKind::new(program), Shell::WithArguments { program, .. } => ShellKind::new(program), Shell::System => ShellKind::system(), } }} /// Specific POSIX-compatible shell variants.#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]pub enum PosixShell { #[default] Sh, Bash, Zsh, Dash, Ksh, Mksh, Ash,} impl PosixShell { pub fn name(&self) -> &'static str { match self { PosixShell::Sh => "sh", PosixShell::Bash => "bash", PosixShell::Zsh => "zsh", PosixShell::Dash => "dash", PosixShell::Ksh => "ksh", PosixShell::Mksh => "mksh", PosixShell::Ash => "ash", } }} #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]pub enum ShellKind { Posix(PosixShell), Csh, Tcsh, Rc, Fish, /// Pre-installed "legacy" powershell for windows PowerShell, /// PowerShell 7.x Pwsh, Nushell, Cmd, Xonsh, Elvish,} pub fn get_system_shell() -> String { if cfg!(windows) { get_windows_system_shell() } else { std::env::var("SHELL").unwrap_or("/bin/sh".to_string()) }} pub fn get_default_system_shell() -> String { if cfg!(windows) { get_windows_system_shell() } else { "/bin/sh".to_string() }} /// Get the default system shell, preferring bash on Windows.pub fn get_default_system_shell_preferring_bash() -> String { if cfg!(windows) { get_windows_bash().unwrap_or_else(|| get_windows_system_shell()) } else { "/bin/sh".to_string() }} pub fn get_windows_bash() -> Option<String> { use std::path::PathBuf; fn find_bash_in_scoop() -> Option<PathBuf> { let bash_exe = PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\bash.exe"); bash_exe.exists().then_some(bash_exe) } fn find_bash_in_git() -> Option<PathBuf> { // /path/to/git/cmd/git.exe/../../bin/bash.exe let git = which::which("git").ok()?; let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe"); git_bash.exists().then_some(git_bash) } static BASH: LazyLock<Option<String>> = LazyLock::new(|| { let bash = find_bash_in_scoop() .or_else(|| find_bash_in_git()) .map(|p| p.to_string_lossy().into_owned()); if let Some(ref path) = bash { log::info!("Found bash at {}", path); } bash }); (*BASH).clone()} pub fn get_windows_system_shell() -> String { use std::path::PathBuf; fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> { #[cfg(target_pointer_width = "64")] let env_var = if find_alternate { "ProgramFiles(x86)" } else { "ProgramFiles" }; #[cfg(target_pointer_width = "32")] let env_var = if find_alternate { "ProgramW6432" } else { "ProgramFiles" }; let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell"); install_base_dir .read_dir() .ok()? .filter_map(Result::ok) .filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir())) .filter_map(|entry| { let dir_name = entry.file_name(); let dir_name = dir_name.to_string_lossy(); let version = if find_preview { let dash_index = dir_name.find('-')?; if &dir_name[dash_index + 1..] != "preview" { return None; }; dir_name[..dash_index].parse::<u32>().ok()? } else { dir_name.parse::<u32>().ok()? }; let exe_path = entry.path().join("pwsh.exe"); if exe_path.exists() { Some((version, exe_path)) } else { None } }) .max_by_key(|(version, _)| *version) .map(|(_, path)| path) } fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> { let msix_app_dir = PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps"); if !msix_app_dir.exists() { return None; } let prefix = if find_preview { "Microsoft.PowerShellPreview_" } else { "Microsoft.PowerShell_" }; msix_app_dir .read_dir() .ok()? .filter_map(|entry| { let entry = entry.ok()?; if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) { return None; } if !entry.file_name().to_string_lossy().starts_with(prefix) { return None; } let exe_path = entry.path().join("pwsh.exe"); exe_path.exists().then_some(exe_path) }) .next() } fn find_pwsh_in_scoop() -> Option<PathBuf> { let pwsh_exe = PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe"); pwsh_exe.exists().then_some(pwsh_exe) } static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| { let locations = [ || find_pwsh_in_programfiles(false, false), || find_pwsh_in_programfiles(true, false), || find_pwsh_in_msix(false), || find_pwsh_in_programfiles(false, true), || find_pwsh_in_msix(true), || find_pwsh_in_programfiles(true, true), || find_pwsh_in_scoop(), || which::which_global("pwsh.exe").ok(), || which::which_global("powershell.exe").ok(), ]; locations .into_iter() .find_map(|f| f()) .map(|p| p.to_string_lossy().trim().to_owned()) .inspect(|shell| log::info!("Found powershell in: {}", shell)) .unwrap_or_else(|| { log::warn!("Powershell not found, falling back to `cmd`"); "cmd.exe".to_string() }) }); (*SYSTEM_SHELL).clone()} impl ShellKind { /// Returns the canonical shell kind for activation script lookups. /// /// This normalizes all POSIX shell variants to `Posix(Sh)` so that /// activation scripts stored with a single POSIX key can be found /// regardless of which specific POSIX shell the user has. pub fn activation_script_key(&self) -> ShellKind { match self { ShellKind::Posix(_) => ShellKind::Posix(PosixShell::Sh), other => *other, } } /// Creates a ShellKind from a program path, with a platform-aware fallback for unknown shells. /// /// Unlike `new()` which returns `None` for unrecognized shells, this method /// falls back to `PowerShell` when `is_windows` is true, or `Posix(Sh)` otherwise. /// This is useful for remote connections where we know the target platform but /// may not recognize the specific shell. pub fn new_with_fallback(program: impl AsRef<Path>, is_windows: bool) -> Self { Self::new(program).unwrap_or_else(|| { if is_windows { ShellKind::PowerShell } else { ShellKind::Posix(PosixShell::Sh) } }) } /// Returns the name of the shell. pub fn name(&self) -> &'static str { match self { ShellKind::Posix(posix) => posix.name(), ShellKind::Csh => "csh", ShellKind::Tcsh => "tcsh", ShellKind::Fish => "fish", ShellKind::PowerShell => "powershell", ShellKind::Pwsh => "pwsh", ShellKind::Nushell => "nu", ShellKind::Cmd => "cmd", ShellKind::Rc => "rc", ShellKind::Xonsh => "xonsh", ShellKind::Elvish => "elvish", } } pub fn system() -> Option<Self> { Self::new(&get_system_shell()) } /// Returns whether this shell's command chaining syntax can be parsed by brush-parser. /// /// This is used to determine if we can safely parse shell commands to extract sub-commands /// for security purposes (e.g., preventing shell injection in "always allow" patterns). /// /// The brush-parser handles `;` (sequential execution) and `|` (piping), which are /// supported by all common shells. It also handles `&&` and `||` for conditional /// execution, `$()` and backticks for command substitution, and process substitution. /// /// # Security Note /// /// Only explicitly recognized shells return `true`. Unknown shells (None) return `false` to /// prevent security bypasses - if we don't know a shell's syntax, we can't safely /// parse it for `always_allow` patterns. /// /// # Shell Notes /// /// - **Nushell**: Uses `;` for sequential execution. The `and`/`or` keywords are boolean /// operators on values (e.g., `$true and $false`), not command chaining operators. /// - **Elvish**: Uses `;` to separate pipelines, which brush-parser handles. Elvish does /// not have `&&` or `||` operators. Its `and`/`or` are special commands that operate /// on values, not command chaining (e.g., `and $true $false`). /// - **Rc (Plan 9)**: Uses `;` for sequential execution and `|` for piping. Does not /// have `&&`/`||` operators for conditional chaining. pub fn supports_posix_chaining(&self) -> bool { match self { ShellKind::Posix(_) | ShellKind::Fish | ShellKind::PowerShell | ShellKind::Pwsh | ShellKind::Cmd | ShellKind::Xonsh | ShellKind::Csh | ShellKind::Tcsh | ShellKind::Nushell | ShellKind::Elvish | ShellKind::Rc => true, } } pub fn new(program: impl AsRef<Path>) -> Option<Self> { match program.as_ref().file_stem().and_then(OsStr::to_str)? { "powershell" => Some(ShellKind::PowerShell), "pwsh" => Some(ShellKind::Pwsh), "cmd" => Some(ShellKind::Cmd), "nu" => Some(ShellKind::Nushell), "fish" => Some(ShellKind::Fish), "csh" => Some(ShellKind::Csh), "tcsh" => Some(ShellKind::Tcsh), "rc" => Some(ShellKind::Rc), "xonsh" => Some(ShellKind::Xonsh), "elvish" => Some(ShellKind::Elvish), "sh" => Some(ShellKind::Posix(PosixShell::Sh)), "bash" => Some(ShellKind::Posix(PosixShell::Bash)), "zsh" => Some(ShellKind::Posix(PosixShell::Zsh)), "dash" => Some(ShellKind::Posix(PosixShell::Dash)), "ksh" => Some(ShellKind::Posix(PosixShell::Ksh)), "mksh" => Some(ShellKind::Posix(PosixShell::Mksh)), "ash" => Some(ShellKind::Posix(PosixShell::Ash)), // Unrecognized shell - we cannot safely parse its syntax for // `always_allow` patterns, so they will be disabled. // Fall back to platform-specific behavior for non-security purposes. _ => None, } } pub fn to_shell_variable(&self, input: &str) -> String { match self { Self::PowerShell | Self::Pwsh => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix(_) => input.to_owned(), Self::Fish => input.to_owned(), Self::Csh => input.to_owned(), Self::Tcsh => input.to_owned(), Self::Rc => input.to_owned(), Self::Nushell => Self::to_nushell_variable(input), Self::Xonsh => input.to_owned(), Self::Elvish => input.to_owned(), } } fn to_cmd_variable(input: &str) -> String { if let Some(var_str) = input.strip_prefix("${") { if var_str.find(':').is_none() { // If the input starts with "${", remove the trailing "}" format!("%{}%", &var_str[..var_str.len() - 1]) } else { // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, // which will result in the task failing to run in such cases. input.into() } } else if let Some(var_str) = input.strip_prefix('$') { // If the input starts with "$", directly append to "$env:" format!("%{}%", var_str) } else { // If no prefix is found, return the input as is input.into() } } pub fn to_powershell_variable(input: &str) -> String { if let Some(var_str) = input.strip_prefix("${") { if var_str.find(':').is_none() { // If the input starts with "${", remove the trailing "}" format!("$env:{}", &var_str[..var_str.len() - 1]) } else { // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, // which will result in the task failing to run in such cases. input.into() } } else if let Some(var_str) = input.strip_prefix('$') { // If the input starts with "$", directly append to "$env:" format!("$env:{}", var_str) } else { // If no prefix is found, return the input as is input.into() } } fn to_nushell_variable(input: &str) -> String { let mut result = String::new(); let mut source = input; let mut is_start = true; loop { match source.chars().next() { None => return result, Some('$') => { source = Self::parse_nushell_var(&source[1..], &mut result, is_start); is_start = false; } Some(_) => { is_start = false; let chunk_end = source.find('$').unwrap_or(source.len()); let (chunk, rest) = source.split_at(chunk_end); result.push_str(chunk); source = rest; } } } } fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str { if source.starts_with("env.") { text.push('$'); return source; } match source.chars().next() { Some('{') => { let source = &source[1..]; if let Some(end) = source.find('}') { let var_name = &source[..end]; if !var_name.is_empty() { if !is_start { text.push_str("("); } text.push_str("$env."); text.push_str(var_name); if !is_start { text.push_str(")"); } &source[end + 1..] } else { text.push_str("${}"); &source[end + 1..] } } else { text.push_str("${"); source } } Some(c) if c.is_alphabetic() || c == '_' => { let end = source .find(|c: char| !c.is_alphanumeric() && c != '_') .unwrap_or(source.len()); let var_name = &source[..end]; if !is_start { text.push_str("("); } text.push_str("$env."); text.push_str(var_name); if !is_start { text.push_str(")"); } &source[end..] } _ => { text.push('$');Lines 1–500 of 1173. Load more
use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, ffi::OsStr, path::Path, sync::LazyLock}; /// Shell configuration to open the terminal with. #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] #[serde(rename_all = "snake_case")] pub enum Shell { /// Use the system's default terminal configuration in /etc/passwd #[default] System, /// Use a specific program with no arguments. Program(String), /// Use a specific program with arguments. WithArguments { /// The program to run. program: String, /// The arguments to pass to the program. args: Vec<String>, /// An optional string to override the title of the terminal tab title_override: Option<String>, }, } impl Shell { pub fn program(&self) -> String { match self { Shell::Program(program) => program.clone(), Shell::WithArguments { program, .. } => program.clone(), Shell::System => get_system_shell(), } } pub fn program_and_args(&self) -> (String, &[String]) { match self { Shell::Program(program) => (program.clone(), &[]), Shell::WithArguments { program, args, .. } => (program.clone(), args), Shell::System => (get_system_shell(), &[]), } } pub fn shell_kind(&self) -> Option<ShellKind> { match self { Shell::Program(program) => ShellKind::new(program), Shell::WithArguments { program, .. } => ShellKind::new(program), Shell::System => ShellKind::system(), } } } /// Specific POSIX-compatible shell variants. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum PosixShell { #[default] Sh, Bash, Zsh, Dash, Ksh, Mksh, Ash, } impl PosixShell { pub fn name(&self) -> &'static str { match self { PosixShell::Sh => "sh", PosixShell::Bash => "bash", PosixShell::Zsh => "zsh", PosixShell::Dash => "dash", PosixShell::Ksh => "ksh", PosixShell::Mksh => "mksh", PosixShell::Ash => "ash", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ShellKind { Posix(PosixShell), Csh, Tcsh, Rc, Fish, /// Pre-installed "legacy" powershell for windows PowerShell, /// PowerShell 7.x Pwsh, Nushell, Cmd, Xonsh, Elvish, } pub fn get_system_shell() -> String { if cfg!(windows) { get_windows_system_shell() } else { std::env::var("SHELL").unwrap_or("/bin/sh".to_string()) } } pub fn get_default_system_shell() -> String { if cfg!(windows) { get_windows_system_shell() } else { "/bin/sh".to_string() } } /// Get the default system shell, preferring bash on Windows. pub fn get_default_system_shell_preferring_bash() -> String { if cfg!(windows) { get_windows_bash().unwrap_or_else(|| get_windows_system_shell()) } else { "/bin/sh".to_string() } } pub fn get_windows_bash() -> Option<String> { use std::path::PathBuf; fn find_bash_in_scoop() -> Option<PathBuf> { let bash_exe = PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\bash.exe"); bash_exe.exists().then_some(bash_exe) } fn find_bash_in_git() -> Option<PathBuf> { // /path/to/git/cmd/git.exe/../../bin/bash.exe let git = which::which("git").ok()?; let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe"); git_bash.exists().then_some(git_bash) } static BASH: LazyLock<Option<String>> = LazyLock::new(|| { let bash = find_bash_in_scoop() .or_else(|| find_bash_in_git()) .map(|p| p.to_string_lossy().into_owned()); if let Some(ref path) = bash { log::info!("Found bash at {}", path); } bash }); (*BASH).clone() } pub fn get_windows_system_shell() -> String { use std::path::PathBuf; fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> { #[cfg(target_pointer_width = "64")] let env_var = if find_alternate { "ProgramFiles(x86)" } else { "ProgramFiles" }; #[cfg(target_pointer_width = "32")] let env_var = if find_alternate { "ProgramW6432" } else { "ProgramFiles" }; let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell"); install_base_dir .read_dir() .ok()? .filter_map(Result::ok) .filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir())) .filter_map(|entry| { let dir_name = entry.file_name(); let dir_name = dir_name.to_string_lossy(); let version = if find_preview { let dash_index = dir_name.find('-')?; if &dir_name[dash_index + 1..] != "preview" { return None; }; dir_name[..dash_index].parse::<u32>().ok()? } else { dir_name.parse::<u32>().ok()? }; let exe_path = entry.path().join("pwsh.exe"); if exe_path.exists() { Some((version, exe_path)) } else { None } }) .max_by_key(|(version, _)| *version) .map(|(_, path)| path) } fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> { let msix_app_dir = PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps"); if !msix_app_dir.exists() { return None; } let prefix = if find_preview { "Microsoft.PowerShellPreview_" } else { "Microsoft.PowerShell_" }; msix_app_dir .read_dir() .ok()? .filter_map(|entry| { let entry = entry.ok()?; if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) { return None; } if !entry.file_name().to_string_lossy().starts_with(prefix) { return None; } let exe_path = entry.path().join("pwsh.exe"); exe_path.exists().then_some(exe_path) }) .next() } fn find_pwsh_in_scoop() -> Option<PathBuf> { let pwsh_exe = PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe"); pwsh_exe.exists().then_some(pwsh_exe) } static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| { let locations = [ || find_pwsh_in_programfiles(false, false), || find_pwsh_in_programfiles(true, false), || find_pwsh_in_msix(false), || find_pwsh_in_programfiles(false, true), || find_pwsh_in_msix(true), || find_pwsh_in_programfiles(true, true), || find_pwsh_in_scoop(), || which::which_global("pwsh.exe").ok(), || which::which_global("powershell.exe").ok(), ]; locations .into_iter() .find_map(|f| f()) .map(|p| p.to_string_lossy().trim().to_owned()) .inspect(|shell| log::info!("Found powershell in: {}", shell)) .unwrap_or_else(|| { log::warn!("Powershell not found, falling back to `cmd`"); "cmd.exe".to_string() }) }); (*SYSTEM_SHELL).clone() } impl ShellKind { /// Returns the canonical shell kind for activation script lookups. /// /// This normalizes all POSIX shell variants to `Posix(Sh)` so that /// activation scripts stored with a single POSIX key can be found /// regardless of which specific POSIX shell the user has. pub fn activation_script_key(&self) -> ShellKind { match self { ShellKind::Posix(_) => ShellKind::Posix(PosixShell::Sh), other => *other, } } /// Creates a ShellKind from a program path, with a platform-aware fallback for unknown shells. /// /// Unlike `new()` which returns `None` for unrecognized shells, this method /// falls back to `PowerShell` when `is_windows` is true, or `Posix(Sh)` otherwise. /// This is useful for remote connections where we know the target platform but /// may not recognize the specific shell. pub fn new_with_fallback(program: impl AsRef<Path>, is_windows: bool) -> Self { Self::new(program).unwrap_or_else(|| { if is_windows { ShellKind::PowerShell } else { ShellKind::Posix(PosixShell::Sh) } }) } /// Returns the name of the shell. pub fn name(&self) -> &'static str { match self { ShellKind::Posix(posix) => posix.name(), ShellKind::Csh => "csh", ShellKind::Tcsh => "tcsh", ShellKind::Fish => "fish", ShellKind::PowerShell => "powershell", ShellKind::Pwsh => "pwsh", ShellKind::Nushell => "nu", ShellKind::Cmd => "cmd", ShellKind::Rc => "rc", ShellKind::Xonsh => "xonsh", ShellKind::Elvish => "elvish", } } pub fn system() -> Option<Self> { Self::new(&get_system_shell()) } /// Returns whether this shell's command chaining syntax can be parsed by brush-parser. /// /// This is used to determine if we can safely parse shell commands to extract sub-commands /// for security purposes (e.g., preventing shell injection in "always allow" patterns). /// /// The brush-parser handles `;` (sequential execution) and `|` (piping), which are /// supported by all common shells. It also handles `&&` and `||` for conditional /// execution, `$()` and backticks for command substitution, and process substitution. /// /// # Security Note /// /// Only explicitly recognized shells return `true`. Unknown shells (None) return `false` to /// prevent security bypasses - if we don't know a shell's syntax, we can't safely /// parse it for `always_allow` patterns. /// /// # Shell Notes /// /// - **Nushell**: Uses `;` for sequential execution. The `and`/`or` keywords are boolean /// operators on values (e.g., `$true and $false`), not command chaining operators. /// - **Elvish**: Uses `;` to separate pipelines, which brush-parser handles. Elvish does /// not have `&&` or `||` operators. Its `and`/`or` are special commands that operate /// on values, not command chaining (e.g., `and $true $false`). /// - **Rc (Plan 9)**: Uses `;` for sequential execution and `|` for piping. Does not /// have `&&`/`||` operators for conditional chaining. pub fn supports_posix_chaining(&self) -> bool { match self { ShellKind::Posix(_) | ShellKind::Fish | ShellKind::PowerShell | ShellKind::Pwsh | ShellKind::Cmd | ShellKind::Xonsh | ShellKind::Csh | ShellKind::Tcsh | ShellKind::Nushell | ShellKind::Elvish | ShellKind::Rc => true, } } pub fn new(program: impl AsRef<Path>) -> Option<Self> { match program.as_ref().file_stem().and_then(OsStr::to_str)? { "powershell" => Some(ShellKind::PowerShell), "pwsh" => Some(ShellKind::Pwsh), "cmd" => Some(ShellKind::Cmd), "nu" => Some(ShellKind::Nushell), "fish" => Some(ShellKind::Fish), "csh" => Some(ShellKind::Csh), "tcsh" => Some(ShellKind::Tcsh), "rc" => Some(ShellKind::Rc), "xonsh" => Some(ShellKind::Xonsh), "elvish" => Some(ShellKind::Elvish), "sh" => Some(ShellKind::Posix(PosixShell::Sh)), "bash" => Some(ShellKind::Posix(PosixShell::Bash)), "zsh" => Some(ShellKind::Posix(PosixShell::Zsh)), "dash" => Some(ShellKind::Posix(PosixShell::Dash)), "ksh" => Some(ShellKind::Posix(PosixShell::Ksh)), "mksh" => Some(ShellKind::Posix(PosixShell::Mksh)), "ash" => Some(ShellKind::Posix(PosixShell::Ash)), // Unrecognized shell - we cannot safely parse its syntax for // `always_allow` patterns, so they will be disabled. // Fall back to platform-specific behavior for non-security purposes. _ => None, } } pub fn to_shell_variable(&self, input: &str) -> String { match self { Self::PowerShell | Self::Pwsh => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix(_) => input.to_owned(), Self::Fish => input.to_owned(), Self::Csh => input.to_owned(), Self::Tcsh => input.to_owned(), Self::Rc => input.to_owned(), Self::Nushell => Self::to_nushell_variable(input), Self::Xonsh => input.to_owned(), Self::Elvish => input.to_owned(), } } fn to_cmd_variable(input: &str) -> String { if let Some(var_str) = input.strip_prefix("${") { if var_str.find(':').is_none() { // If the input starts with "${", remove the trailing "}" format!("%{}%", &var_str[..var_str.len() - 1]) } else { // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, // which will result in the task failing to run in such cases. input.into() } } else if let Some(var_str) = input.strip_prefix('$') { // If the input starts with "$", directly append to "$env:" format!("%{}%", var_str) } else { // If no prefix is found, return the input as is input.into() } } pub fn to_powershell_variable(input: &str) -> String { if let Some(var_str) = input.strip_prefix("${") { if var_str.find(':').is_none() { // If the input starts with "${", remove the trailing "}" format!("$env:{}", &var_str[..var_str.len() - 1]) } else { // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, // which will result in the task failing to run in such cases. input.into() } } else if let Some(var_str) = input.strip_prefix('$') { // If the input starts with "$", directly append to "$env:" format!("$env:{}", var_str) } else { // If no prefix is found, return the input as is input.into() } } fn to_nushell_variable(input: &str) -> String { let mut result = String::new(); let mut source = input; let mut is_start = true; loop { match source.chars().next() { None => return result, Some('$') => { source = Self::parse_nushell_var(&source[1..], &mut result, is_start); is_start = false; } Some(_) => { is_start = false; let chunk_end = source.find('$').unwrap_or(source.len()); let (chunk, rest) = source.split_at(chunk_end); result.push_str(chunk); source = rest; } } } } fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str { if source.starts_with("env.") { text.push('$'); return source; } match source.chars().next() { Some('{') => { let source = &source[1..]; if let Some(end) = source.find('}') { let var_name = &source[..end]; if !var_name.is_empty() { if !is_start { text.push_str("("); } text.push_str("$env."); text.push_str(var_name); if !is_start { text.push_str(")"); } &source[end + 1..] } else { text.push_str("${}"); &source[end + 1..] } } else { text.push_str("${"); source } } Some(c) if c.is_alphabetic() || c == '_' => { let end = source .find(|c: char| !c.is_alphanumeric() && c != '_') .unwrap_or(source.len()); let var_name = &source[..end]; if !is_start { text.push_str("("); } text.push_str("$env."); text.push_str(var_name); if !is_start { text.push_str(")"); } &source[end..] } _ => { text.push('$');