1
use std::env;
2

            
3
use crate::error::Error;
4

            
5
mod bash;
6
mod fish;
7
mod nushell;
8
mod pwsh;
9
mod zsh;
10

            
11
/// Supported shell types
12
#[derive(Debug, PartialEq)]
13
enum ShellType {
14
    Bash,
15
    Fish,
16
    Nushell,
17
    Powershell,
18
    Zsh,
19
}
20

            
21
impl ShellType {
22
    /// Detect shell type from a hint string (shell name or full path like
23
    /// `/usr/bin/fish`). Falls back to Bash for unknown shells.
24
38
    fn detect(hint: &str) -> Self {
25
38
        match hint {
26
38
            s if s.contains("bash") => ShellType::Bash,
27
30
            s if s.contains("fish") => ShellType::Fish,
28
24
            s if s.contains("powershell") => ShellType::Powershell,
29
22
            s if s.contains("pwsh") => ShellType::Powershell,
30
18
            s if s.contains("nushell") => ShellType::Nushell,
31
14
            s if s.contains("nu") => ShellType::Nushell,
32
10
            s if s.contains("zsh") => ShellType::Zsh,
33
4
            _ => ShellType::Bash,
34
        }
35
38
    }
36

            
37
    /// Return the integration script template for this shell type
38
14
    fn script(&self) -> &'static str {
39
14
        match self {
40
6
            ShellType::Bash => bash::SCRIPT,
41
2
            ShellType::Fish => fish::SCRIPT,
42
2
            ShellType::Nushell => nushell::SCRIPT,
43
2
            ShellType::Powershell => pwsh::SCRIPT,
44
2
            ShellType::Zsh => zsh::SCRIPT,
45
        }
46
14
    }
47
}
48

            
49
/// Shell integration script
50
#[derive(Debug, PartialEq)]
51
pub struct Shell {
52
    /// Alias to use for the shell function
53
    alias: String,
54
    /// Integration script template
55
    script: String,
56
}
57

            
58
impl Shell {
59
    /// Compile the shell integration template
60
14
    pub fn compile_script(&self) -> String {
61
14
        self.script.replace("{alias}", &self.alias)
62
14
    }
63
}
64

            
65
/// Return the shell integration script for shell given in `hint`. If `hint` is
66
/// `None`, try to detect the current shell. Use `bash` as fallback.
67
///
68
/// `alias` is the name of the shell function to use in the integration script.
69
///
70
/// # Errors
71
///
72
/// If `hint` is `None` and the `SHELL` environment variable is not set, an
73
/// error is returned.
74
18
pub fn initialize(alias: String, hint: &Option<String>) -> Result<Shell, Error> {
75
18
    let shell_hint = match hint {
76
12
        Some(hint) => hint.clone(),
77
6
        None => env::var("SHELL").map_err(|_| Error::ShellNotDetected)?,
78
    };
79
14
    let shell_type = ShellType::detect(&shell_hint);
80

            
81
14
    Ok(Shell {
82
14
        alias,
83
14
        script: shell_type.script().to_string(),
84
14
    })
85
18
}
86

            
87
#[cfg(test)]
88
macro_rules! initialize_shell {
89
    ($($name: ident: $value:expr,)*) => {
90
    $(
91
        #[test]
92
10
        fn $name() {
93
            // GIVEN a shell hint
94
10
            let hint = Some($value.to_string());
95

            
96
            // WHEN trying to initialize the hinted shell
97
10
            let alias = String::from("up");
98
10
            let actual = initialize(alias, &hint);
99

            
100
            // THEN we should get the initialization function for the expected
101
            // shell
102
10
            let expected = format!("init {}", &hint.unwrap());
103
10
            assert!(actual.unwrap().compile_script().contains(&expected))
104
10
        }
105
    )*
106
    }
107
}
108

            
109
#[cfg(test)]
110
macro_rules! detect_shell {
111
    ($($name: ident: ($hint:expr, $expected:expr),)*) => {
112
    $(
113
        #[test]
114
24
        fn $name() {
115
            // GIVEN a shell hint string
116
24
            let hint = $hint;
117

            
118
            // WHEN detecting the shell type
119
24
            let actual = ShellType::detect(hint);
120

            
121
            // THEN the detected type should match
122
24
            assert_eq!(actual, $expected);
123
24
        }
124
    )*
125
    }
126
}
127

            
128
#[cfg(test)]
129
mod tests {
130
    use super::*;
131

            
132
    #[test]
133
2
    fn initialize_from_env() {
134
        // GIVEN non-existing shell
135
2
        env::set_var("SHELL", "radish");
136

            
137
        // WHEN trying initialize without a hint
138
2
        let alias = String::from("up");
139
2
        let actual = initialize(alias, &None);
140

            
141
        // THEN we should get the initialization function for the default shell
142
2
        let expected = "init bash";
143
2
        assert!(actual.unwrap().compile_script().contains(&expected))
144
2
    }
145

            
146
    #[test]
147
2
    fn initialize_non_existant() {
148
        // GIVEN empty SHELL variable
149
2
        env::remove_var("SHELL");
150

            
151
        // WHEN trying initialize without a hint
152
2
        let alias = String::from("up");
153
2
        let actual = initialize(alias, &None);
154

            
155
        // THEN we should get a ShellNotDetected error
156
2
        assert!(matches!(actual, Err(Error::ShellNotDetected)));
157
2
    }
158

            
159
    initialize_shell! {
160
        initialize_bash: ("bash"),
161
        initialize_fish: ("fish"),
162
        initialize_nushell: ("nushell"),
163
        initialize_pwsh: ("pwsh"),
164
        initialize_zsh: ("zsh"),
165
    }
166

            
167
    detect_shell! {
168
        detect_bash: ("bash", ShellType::Bash),
169
        detect_fish: ("fish", ShellType::Fish),
170
        detect_nushell: ("nushell", ShellType::Nushell),
171
        detect_nu: ("nu", ShellType::Nushell),
172
        detect_pwsh: ("pwsh", ShellType::Powershell),
173
        detect_powershell: ("powershell", ShellType::Powershell),
174
        detect_zsh: ("zsh", ShellType::Zsh),
175
        detect_unknown_falls_back_to_bash: ("radish", ShellType::Bash),
176
        detect_full_path_bash: ("/usr/bin/bash", ShellType::Bash),
177
        detect_full_path_fish: ("/usr/bin/fish", ShellType::Fish),
178
        detect_full_path_nu: ("/usr/bin/nu", ShellType::Nushell),
179
        detect_full_path_zsh: ("/usr/bin/zsh", ShellType::Zsh),
180
    }
181
}