rust_expect/auto_config/
shell.rs1use std::path::PathBuf;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ShellType {
8 Sh,
10 Bash,
12 Zsh,
14 Fish,
16 Ksh,
18 Tcsh,
20 Dash,
22 PowerShell,
24 Cmd,
26 Unknown,
28}
29
30impl ShellType {
31 #[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 #[must_use]
50 pub const fn supports_ansi(&self) -> bool {
51 !matches!(self, Self::Cmd)
52 }
53
54 #[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 #[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#[must_use]
81pub fn detect_shell() -> ShellType {
82 if let Ok(shell) = std::env::var("SHELL") {
84 return detect_from_path(&shell);
85 }
86
87 #[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#[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#[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#[derive(Debug, Clone)]
144pub struct ShellConfig {
145 pub shell_type: ShellType,
147 pub path: String,
149 pub args: Vec<String>,
151 pub env: std::collections::HashMap<String, String>,
153 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 #[must_use]
174 pub fn new() -> Self {
175 Self::default()
176 }
177
178 #[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 #[must_use]
188 pub fn arg(mut self, arg: impl Into<String>) -> Self {
189 self.args.push(arg.into());
190 self
191 }
192
193 #[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 #[must_use]
202 pub fn cwd(mut self, dir: impl Into<PathBuf>) -> Self {
203 self.cwd = Some(dir.into());
204 self
205 }
206
207 #[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}