use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShellType {
Sh,
Bash,
Zsh,
Fish,
Ksh,
Tcsh,
Dash,
PowerShell,
Cmd,
Unknown,
}
impl ShellType {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Sh => "sh",
Self::Bash => "bash",
Self::Zsh => "zsh",
Self::Fish => "fish",
Self::Ksh => "ksh",
Self::Tcsh => "tcsh",
Self::Dash => "dash",
Self::PowerShell => "powershell",
Self::Cmd => "cmd",
Self::Unknown => "unknown",
}
}
#[must_use]
pub const fn supports_ansi(&self) -> bool {
!matches!(self, Self::Cmd)
}
#[must_use]
pub const fn prompt_pattern(&self) -> &'static str {
match self {
Self::Bash | Self::Sh | Self::Dash | Self::Ksh => r"[$#]\s*$",
Self::Zsh => r"[%#$]\s*$",
Self::Fish => r">\s*$",
Self::Tcsh => r"[%>]\s*$",
Self::PowerShell => r"PS[^>]*>\s*$",
Self::Cmd => r">\s*$",
Self::Unknown => r"[$#%>]\s*$",
}
}
#[must_use]
pub const fn exit_command(&self) -> &'static str {
match self {
Self::Cmd => "exit",
Self::PowerShell => "exit",
_ => "exit",
}
}
}
#[must_use]
pub fn detect_shell() -> ShellType {
if let Ok(shell) = std::env::var("SHELL") {
return detect_from_path(&shell);
}
#[cfg(windows)]
if let Ok(comspec) = std::env::var("COMSPEC") {
if comspec.to_lowercase().contains("powershell") {
return ShellType::PowerShell;
}
return ShellType::Cmd;
}
ShellType::Unknown
}
#[must_use]
pub fn detect_from_path(path: &str) -> ShellType {
let path_lower = path.to_lowercase();
let path_buf = PathBuf::from(&path_lower);
let name = path_buf
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&path_lower);
match name {
"sh" => ShellType::Sh,
"bash" => ShellType::Bash,
"zsh" => ShellType::Zsh,
"fish" => ShellType::Fish,
"ksh" | "ksh93" | "mksh" => ShellType::Ksh,
"tcsh" | "csh" => ShellType::Tcsh,
"dash" => ShellType::Dash,
"pwsh" | "powershell" | "powershell.exe" => ShellType::PowerShell,
"cmd" | "cmd.exe" => ShellType::Cmd,
_ => ShellType::Unknown,
}
}
#[must_use]
pub fn default_shell() -> String {
std::env::var("SHELL").unwrap_or_else(|_| {
#[cfg(unix)]
{
"/bin/sh".to_string()
}
#[cfg(windows)]
{
std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string())
}
#[cfg(not(any(unix, windows)))]
{
"sh".to_string()
}
})
}
#[derive(Debug, Clone)]
pub struct ShellConfig {
pub shell_type: ShellType,
pub path: String,
pub args: Vec<String>,
pub env: std::collections::HashMap<String, String>,
pub cwd: Option<PathBuf>,
}
impl Default for ShellConfig {
fn default() -> Self {
let path = default_shell();
let shell_type = detect_from_path(&path);
Self {
shell_type,
path,
args: Vec::new(),
env: std::collections::HashMap::new(),
cwd: None,
}
}
}
impl ShellConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = path.into();
self.shell_type = detect_from_path(&self.path);
self
}
#[must_use]
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
#[must_use]
pub fn cwd(mut self, dir: impl Into<PathBuf>) -> Self {
self.cwd = Some(dir.into());
self
}
#[must_use]
pub fn command(&self) -> (&str, &[String]) {
(&self.path, &self.args)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_bash() {
assert_eq!(detect_from_path("/bin/bash"), ShellType::Bash);
assert_eq!(detect_from_path("/usr/bin/bash"), ShellType::Bash);
}
#[test]
fn detect_zsh() {
assert_eq!(detect_from_path("/bin/zsh"), ShellType::Zsh);
}
#[test]
fn shell_type_name() {
assert_eq!(ShellType::Bash.name(), "bash");
assert_eq!(ShellType::Zsh.name(), "zsh");
}
#[test]
fn shell_config_default() {
let config = ShellConfig::new();
assert!(!config.path.is_empty());
}
}