what_the_path/
shell.rs

1use std::env;
2use std::ffi::OsStr;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use dirs::{config_dir, home_dir};
7
8use crate::error::ShellError;
9
10#[derive(Debug)]
11/// Represents different types of Unix shells supported by this library.
12///
13/// This enum provides variants for common Unix shells (POSIX, Zsh, Bash, Fish)
14/// along with their specific configuration handling.
15///
16/// # Examples
17///
18/// ```
19/// use what_the_path::Shell;
20///
21/// // Detect current shell
22/// if let Some(shell) = Shell::detect() {
23///     match shell {
24///         Shell::Zsh(_) => println!("Using Zsh"),
25///         Shell::Bash(_) => println!("Using Bash"),
26///         Shell::Fish(_) => println!("Using Fish"),
27///         Shell::POSIX(_) => println!("Using POSIX shell"),
28///     }
29/// }
30/// ```
31///
32/// # Variants
33///
34/// * `POSIX` - Default POSIX-compliant shell (like sh)
35/// * `Zsh` - Z shell
36/// * `Bash` - Bourne Again Shell
37/// * `Fish` - Friendly Interactive Shell
38///
39pub enum Shell {
40    POSIX(POSIX),
41    Zsh(Zsh),
42    Bash(Bash),
43    Fish(Fish),
44}
45
46impl Shell {
47    /// Detects the current shell by examining the `SHELL` environment variable.
48    ///
49    /// This function attempts to identify the shell type based on the `SHELL` environment variable.
50    /// It will return `None` on Windows systems as the `SHELL` variable is not typically used.
51    ///
52    /// # Returns
53    /// - `Some(Shell)` containing the detected shell type if:
54    ///   - Running on a non-Windows system
55    ///   - The `SHELL` environment variable exists and contains a recognized shell name
56    /// - `None` if:
57    ///   - Running on Windows
58    ///   - The `SHELL` environment variable does not exist
59    ///
60    /// # Shell Detection
61    /// The following shells are recognized (in order):
62    /// - Zsh
63    /// - Bash
64    /// - Fish
65    /// - Any other shell is assumed to be POSIX-compliant
66    pub fn detect_by_shell_var() -> Result<Shell, ShellError> {
67        if cfg!(windows) {
68            return Err(ShellError::UnsupportedPlatform);
69        }
70
71        let shell = env::var("SHELL").map_err(|_| ShellError::NoShellVar)?;
72
73        match shell.as_str() {
74            path if path.contains("zsh") => Ok(Shell::Zsh(Zsh)),
75            path if path.contains("bash") => Ok(Shell::Bash(Bash)),
76            path if path.contains("fish") => Ok(Shell::Fish(Fish)),
77            _ => Ok(Shell::POSIX(POSIX)),
78        }
79    }
80
81    pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
82        match self {
83            Shell::Fish(fish) => fish.get_rcfiles(),
84            Shell::Zsh(zsh) => zsh.get_rcfiles(),
85            Shell::Bash(bash) => bash.get_rcfiles(),
86            Shell::POSIX(posix) => posix.get_rcfiles(),
87        }
88    }
89}
90
91#[derive(Debug)]
92pub struct POSIX;
93
94impl POSIX {
95    pub fn does_exist(&self) -> bool {
96        true
97    }
98    pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
99        let dir = home_dir().ok_or(ShellError::NoHomeDir)?;
100        Ok(vec![dir.join(".profile")])
101    }
102    pub fn get_rcfiles_from_base(base_dir: impl AsRef<Path>) -> Vec<PathBuf> {
103        vec![base_dir.as_ref().join(".profile")]
104    }
105}
106
107#[derive(Debug)]
108pub struct Zsh;
109
110impl Zsh {
111    pub fn does_exist(&self) -> bool {
112        matches!(env::var("SHELL"), Ok(v) if v.contains("zsh"))
113            || Command::new("zsh").output().is_ok()
114    }
115
116    pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
117        let mut rc_files = Vec::new();
118
119        // Try ZDOTDIR
120        if let Ok(output) = std::process::Command::new("zsh")
121            .args(["-c", "echo -n $ZDOTDIR"])
122            .output()
123        {
124            if !output.stdout.is_empty() {
125                if let Ok(zdotdir) = String::from_utf8(output.stdout) {
126                    let path = PathBuf::from(zdotdir.trim()).join(".zshenv");
127                    if path.exists() {
128                        rc_files.push(path);
129                    }
130                }
131            }
132        }
133
134        // Try HOME
135        if let Ok(home) = std::env::var("HOME") {
136            let path = PathBuf::from(home).join(".zshenv");
137            if path.exists() {
138                rc_files.push(path);
139            }
140        }
141
142        if rc_files.is_empty() {
143            Err(ShellError::EmptyHomeAndZdotdir)
144        } else {
145            Ok(rc_files)
146        }
147    }
148    pub fn get_rcfiles_from_base(base_dir: impl AsRef<Path>) -> Vec<PathBuf> {
149        vec![base_dir.as_ref().join(".zshenv")]
150    }
151}
152
153#[derive(Debug)]
154pub struct Bash;
155
156impl Bash {
157    pub fn does_exist(&self) -> bool {
158        matches!(env::var("SHELL"), Ok(v) if v.contains("bash"))
159            || Command::new("bash").output().is_ok()
160    }
161
162    pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
163        let dir = home_dir().ok_or(ShellError::NoHomeDir)?;
164        let rcfiles = [".bash_profile", ".bash_login", ".bashrc"]
165            .iter()
166            .map(|rc| dir.join(rc))
167            .collect();
168        Ok(rcfiles)
169    }
170
171    pub fn get_rcfiles_from_base(base_dir: impl AsRef<Path>) -> Vec<PathBuf> {
172        [".bash_profile", ".bash_login", ".bashrc"]
173            .iter()
174            .map(|rc| base_dir.as_ref().join(rc))
175            .collect()
176    }
177}
178
179#[derive(Debug)]
180pub struct Fish;
181
182impl Fish {
183    pub fn does_exist(&self) -> bool {
184        matches!(env::var("SHELL"), Ok(v) if v.contains("fish"))
185            || Command::new("fish").output().is_ok()
186    }
187
188    /// Returns the configuration directory path for Fish shell
189    ///
190    /// This function attempts to locate the Fish shell's configuration directory
191    /// by joining the user's home directory with the Fish config path.
192    ///
193    /// # Returns
194    /// - `Some(Vec<PathBuf>)` containing the path to Fish's conf.d directory
195    /// - `None` if the home directory cannot be determined
196    ///
197    /// # Important
198    /// Note that this function returns a directory path (`conf.d`), not individual
199    /// file paths. You'll need to enumerate the directory contents to access
200    /// specific configuration files.
201    ///
202    /// # Example
203    /// ```
204    /// if let Some(paths) = get_rcfiles() {
205    ///     // paths[0] points to ~/.config/fish/conf.d directory
206    ///     // not to specific .fish files
207    /// }
208    /// ```
209    pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
210        let mut paths = vec![];
211
212        if let Some(path) = config_dir() {
213            paths.push(path.join("fish/conf.d"));
214        }
215
216        Ok(paths)
217    }
218
219    pub fn get_rcfiles_from_base(base_dir: impl AsRef<Path>) -> Vec<PathBuf> {
220        vec![base_dir.as_ref().join(".config/fish/conf.d")]
221    }
222}
223
224pub fn exists_in_path(path: impl AsRef<Path>) -> bool {
225    matches!(env::var("PATH"), Ok(paths) if paths.contains(path.as_ref().to_str().unwrap()))
226}
227
228pub fn append_to_rcfile(rcfile: PathBuf, line: &str) -> Result<(), ShellError> {
229    use std::fs::OpenOptions;
230    use std::io::Write;
231
232    if !rcfile.exists() {
233        return Err(ShellError::RCFileNotFound(
234            rcfile
235                .file_name()
236                .unwrap_or_else(|| OsStr::new("unknown"))
237                .to_string_lossy()
238                .into_owned(),
239        ));
240    }
241
242    let mut file = OpenOptions::new().append(true).open(rcfile).unwrap();
243    writeln!(file, "{}", line)?;
244    Ok(())
245}
246
247pub fn remove_from_rcfile(rcfile: PathBuf, line: &str) -> Result<(), ShellError> {
248    if !rcfile.exists() {
249        return Err(ShellError::RCFileNotFound(
250            rcfile
251                .file_name()
252                .unwrap_or_else(|| OsStr::new("unknown"))
253                .to_string_lossy()
254                .into_owned(),
255        ));
256    }
257
258    let line_bytes = line.as_bytes();
259
260    let file = std::fs::read_to_string(&rcfile)?;
261    let file_bytes = file.as_bytes();
262
263    if let Some(idx) = file_bytes
264        .windows(line_bytes.len())
265        .position(|w| w == line_bytes)
266    {
267        let mut new_bytes = file_bytes[..idx].to_vec();
268        new_bytes.extend(&file_bytes[idx + line_bytes.len()..]);
269        let content = String::from_utf8(new_bytes).unwrap();
270        std::fs::write(&rcfile, content)?;
271    }
272
273    Ok(())
274}