rust_expect/auto_config/
shell.rs

1//! Shell detection and configuration.
2
3use std::path::PathBuf;
4
5/// Known shell types.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ShellType {
8    /// Bourne shell.
9    Sh,
10    /// Bash shell.
11    Bash,
12    /// Zsh shell.
13    Zsh,
14    /// Fish shell.
15    Fish,
16    /// Ksh shell.
17    Ksh,
18    /// Tcsh shell.
19    Tcsh,
20    /// Dash shell.
21    Dash,
22    /// `PowerShell`.
23    PowerShell,
24    /// Windows Command Prompt.
25    Cmd,
26    /// Unknown shell.
27    Unknown,
28}
29
30impl ShellType {
31    /// Get shell name.
32    #[must_use]
33    pub const fn name(&self) -> &'static str {
34        match self {
35            Self::Sh => "sh",
36            Self::Bash => "bash",
37            Self::Zsh => "zsh",
38            Self::Fish => "fish",
39            Self::Ksh => "ksh",
40            Self::Tcsh => "tcsh",
41            Self::Dash => "dash",
42            Self::PowerShell => "powershell",
43            Self::Cmd => "cmd",
44            Self::Unknown => "unknown",
45        }
46    }
47
48    /// Check if shell supports ANSI sequences.
49    #[must_use]
50    pub const fn supports_ansi(&self) -> bool {
51        !matches!(self, Self::Cmd)
52    }
53
54    /// Get typical prompt pattern.
55    #[must_use]
56    pub const fn prompt_pattern(&self) -> &'static str {
57        match self {
58            Self::Bash | Self::Sh | Self::Dash | Self::Ksh => r"[$#]\s*$",
59            Self::Zsh => r"[%#$]\s*$",
60            Self::Fish => r">\s*$",
61            Self::Tcsh => r"[%>]\s*$",
62            Self::PowerShell => r"PS[^>]*>\s*$",
63            Self::Cmd => r">\s*$",
64            Self::Unknown => r"[$#%>]\s*$",
65        }
66    }
67
68    /// Get exit command.
69    #[must_use]
70    pub const fn exit_command(&self) -> &'static str {
71        match self {
72            Self::Cmd => "exit",
73            Self::PowerShell => "exit",
74            _ => "exit",
75        }
76    }
77}
78
79/// Detect shell type from environment.
80#[must_use]
81pub fn detect_shell() -> ShellType {
82    // Check SHELL environment variable
83    if let Ok(shell) = std::env::var("SHELL") {
84        return detect_from_path(&shell);
85    }
86
87    // Windows: check COMSPEC
88    #[cfg(windows)]
89    if let Ok(comspec) = std::env::var("COMSPEC") {
90        if comspec.to_lowercase().contains("powershell") {
91            return ShellType::PowerShell;
92        }
93        return ShellType::Cmd;
94    }
95
96    ShellType::Unknown
97}
98
99/// Detect shell type from path.
100#[must_use]
101pub fn detect_from_path(path: &str) -> ShellType {
102    let path_lower = path.to_lowercase();
103    let path_buf = PathBuf::from(&path_lower);
104    let name = path_buf
105        .file_name()
106        .and_then(|n| n.to_str())
107        .unwrap_or(&path_lower);
108
109    match name {
110        "sh" => ShellType::Sh,
111        "bash" => ShellType::Bash,
112        "zsh" => ShellType::Zsh,
113        "fish" => ShellType::Fish,
114        "ksh" | "ksh93" | "mksh" => ShellType::Ksh,
115        "tcsh" | "csh" => ShellType::Tcsh,
116        "dash" => ShellType::Dash,
117        "pwsh" | "powershell" | "powershell.exe" => ShellType::PowerShell,
118        "cmd" | "cmd.exe" => ShellType::Cmd,
119        _ => ShellType::Unknown,
120    }
121}
122
123/// Get default shell path.
124#[must_use]
125pub fn default_shell() -> String {
126    std::env::var("SHELL").unwrap_or_else(|_| {
127        #[cfg(unix)]
128        {
129            "/bin/sh".to_string()
130        }
131        #[cfg(windows)]
132        {
133            std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string())
134        }
135        #[cfg(not(any(unix, windows)))]
136        {
137            "sh".to_string()
138        }
139    })
140}
141
142/// Shell configuration options.
143#[derive(Debug, Clone)]
144pub struct ShellConfig {
145    /// Shell type.
146    pub shell_type: ShellType,
147    /// Shell path.
148    pub path: String,
149    /// Additional arguments.
150    pub args: Vec<String>,
151    /// Environment variables.
152    pub env: std::collections::HashMap<String, String>,
153    /// Working directory.
154    pub cwd: Option<PathBuf>,
155}
156
157impl Default for ShellConfig {
158    fn default() -> Self {
159        let path = default_shell();
160        let shell_type = detect_from_path(&path);
161        Self {
162            shell_type,
163            path,
164            args: Vec::new(),
165            env: std::collections::HashMap::new(),
166            cwd: None,
167        }
168    }
169}
170
171impl ShellConfig {
172    /// Create a new shell config.
173    #[must_use]
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    /// Set shell path.
179    #[must_use]
180    pub fn with_path(mut self, path: impl Into<String>) -> Self {
181        self.path = path.into();
182        self.shell_type = detect_from_path(&self.path);
183        self
184    }
185
186    /// Add an argument.
187    #[must_use]
188    pub fn arg(mut self, arg: impl Into<String>) -> Self {
189        self.args.push(arg.into());
190        self
191    }
192
193    /// Set an environment variable.
194    #[must_use]
195    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
196        self.env.insert(key.into(), value.into());
197        self
198    }
199
200    /// Set working directory.
201    #[must_use]
202    pub fn cwd(mut self, dir: impl Into<PathBuf>) -> Self {
203        self.cwd = Some(dir.into());
204        self
205    }
206
207    /// Get command and args for spawning.
208    #[must_use]
209    pub fn command(&self) -> (&str, &[String]) {
210        (&self.path, &self.args)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn detect_bash() {
220        assert_eq!(detect_from_path("/bin/bash"), ShellType::Bash);
221        assert_eq!(detect_from_path("/usr/bin/bash"), ShellType::Bash);
222    }
223
224    #[test]
225    fn detect_zsh() {
226        assert_eq!(detect_from_path("/bin/zsh"), ShellType::Zsh);
227    }
228
229    #[test]
230    fn shell_type_name() {
231        assert_eq!(ShellType::Bash.name(), "bash");
232        assert_eq!(ShellType::Zsh.name(), "zsh");
233    }
234
235    #[test]
236    fn shell_config_default() {
237        let config = ShellConfig::new();
238        assert!(!config.path.is_empty());
239    }
240}