Skip to main content

starbase_shell/shells/
ion.rs

1use super::Shell;
2use crate::helpers::{ProfileSet, get_config_dir, get_env_var_regex, quotable_into_string};
3use crate::hooks::*;
4use crate::quoter::*;
5use shell_quote::Quotable;
6use std::fmt;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10#[derive(Clone, Copy, Debug)]
11pub struct Ion;
12
13impl Ion {
14    #[allow(clippy::new_without_default)]
15    pub fn new() -> Self {
16        Self
17    }
18
19    /// Quotes a string according to Ion shell quoting rules.
20    /// @see <https://doc.redox-os.org/ion-manual/general.html>
21    fn do_quote(value: String) -> String {
22        if value.starts_with('$') {
23            // Variables expanded in double quotes
24            format!("\"{value}\"")
25        } else if value.contains('{') || value.contains('}') {
26            // Single quotes to prevent brace expansion
27            format!("'{value}'")
28        } else if value.chars().all(|c| {
29            c.is_ascii_graphic() && !c.is_whitespace() && c != '"' && c != '\'' && c != '\\'
30        }) {
31            // No quoting needed for simple values
32            value.to_string()
33        } else {
34            // Double quotes for other cases
35            format!("\"{}\"", value.replace('"', "\\\""))
36        }
37    }
38
39    // $FOO -> ${env::FOO}
40    fn replace_env(&self, value: impl AsRef<str>) -> String {
41        get_env_var_regex()
42            .replace_all(value.as_ref(), "$${env::$name}")
43            .to_string()
44    }
45}
46
47impl Shell for Ion {
48    fn create_quoter<'a>(&self, data: Quotable<'a>) -> Quoter<'a> {
49        Quoter::new(
50            data,
51            QuoterOptions {
52                // https://doc.redox-os.org/ion-manual/expansions/00-expansions.html
53                quoted_syntax: vec![
54                    Syntax::Symbol("$".into()),
55                    Syntax::Pair("${".into(), "}".into()),
56                    Syntax::Pair("$(".into(), ")".into()),
57                    Syntax::Symbol("@".into()),
58                    Syntax::Pair("@{".into(), "}".into()),
59                    Syntax::Pair("@(".into(), ")".into()),
60                ],
61                on_quote: Arc::new(|data| Ion::do_quote(quotable_into_string(data))),
62                ..Default::default()
63            },
64        )
65    }
66
67    // https://doc.redox-os.org/ion-manual/variables/05-exporting.html
68    fn format(&self, statement: Statement<'_>) -> String {
69        match statement {
70            Statement::ModifyPath {
71                paths,
72                key,
73                orig_key,
74            } => {
75                let key = key.unwrap_or("PATH");
76                let value = self.replace_env(paths.join(":"));
77
78                match orig_key {
79                    Some(orig_key) => format!(r#"export {key} = "{value}:${{env::{orig_key}}}""#,),
80                    None => format!(r#"export {key} = "{value}""#,),
81                }
82            }
83            Statement::SetEnv { key, value } => {
84                format!(
85                    "export {}={}",
86                    self.quote(key),
87                    self.quote(self.replace_env(value).as_str())
88                )
89            }
90            Statement::UnsetEnv { key } => {
91                format!("drop {}", self.quote(key))
92            }
93        }
94    }
95
96    fn get_config_path(&self, home_dir: &Path) -> PathBuf {
97        get_config_dir(home_dir).join("ion").join("initrc")
98    }
99
100    fn get_env_path(&self, home_dir: &Path) -> PathBuf {
101        self.get_config_path(home_dir)
102    }
103
104    fn get_env_regex(&self) -> regex::Regex {
105        regex::Regex::new(r"\$\{env::(?<name>[A-Za-z0-9_]+)\}").unwrap()
106    }
107
108    // https://doc.redox-os.org/ion-manual/general.html#xdg-app-dirs-support
109    fn get_profile_paths(&self, home_dir: &Path) -> Vec<PathBuf> {
110        ProfileSet::default()
111            .insert(get_config_dir(home_dir).join("ion").join("initrc"), 1)
112            .insert(home_dir.join(".config").join("ion").join("initrc"), 2)
113            .into_list()
114    }
115}
116
117impl fmt::Display for Ion {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "ion")
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn formats_env_var() {
129        assert_eq!(
130            Ion.format_env_set("PROTO_HOME", "$HOME/.proto"),
131            r#"export PROTO_HOME="${env::HOME}/.proto""#
132        );
133    }
134
135    #[test]
136    fn formats_path_prepend() {
137        assert_eq!(
138            Ion.format_path_prepend(&["$PROTO_HOME/shims".into(), "$PROTO_HOME/bin".into()]),
139            r#"export PATH = "${env::PROTO_HOME}/shims:${env::PROTO_HOME}/bin:${env::PATH}""#
140        );
141    }
142
143    #[test]
144    fn formats_path_set() {
145        assert_eq!(
146            Ion.format_path_set(&["$PROTO_HOME/shims".into(), "$PROTO_HOME/bin".into()]),
147            r#"export PATH = "${env::PROTO_HOME}/shims:${env::PROTO_HOME}/bin""#
148        );
149    }
150
151    #[test]
152    fn test_profile_paths() {
153        #[allow(deprecated)]
154        let home_dir = std::env::home_dir().unwrap();
155
156        assert_eq!(
157            Ion::new().get_profile_paths(&home_dir),
158            vec![home_dir.join(".config").join("ion").join("initrc")]
159        );
160    }
161
162    #[test]
163    fn test_ion_quoting() {
164        assert_eq!(Ion.quote("simplevalue"), "simplevalue");
165        assert_eq!(Ion.quote("value with spaces"), r#""value with spaces""#);
166        assert_eq!(
167            Ion.quote(r#"value "with" quotes"#),
168            r#""value \"with\" quotes""#
169        );
170        assert_eq!(Ion.quote("$variable"), "\"$variable\"");
171        assert_eq!(Ion.quote("{brace_expansion}"), "{brace_expansion}");
172        assert_eq!(
173            Ion.quote("value with 'single quotes'"),
174            r#""value with 'single quotes'""#
175        );
176    }
177}