Skip to main content

atuin_dotfiles/shell/
powershell.rs

1use crate::shell::{Alias, Var};
2use crate::store::{AliasStore, var::VarStore};
3use std::path::PathBuf;
4
5async fn cached_aliases(path: PathBuf, store: &AliasStore) -> String {
6    match tokio::fs::read_to_string(path).await {
7        Ok(aliases) => aliases,
8        Err(r) => {
9            // we failed to read the file for some reason, but the file does exist
10            // fallback to generating new aliases on the fly
11
12            store.powershell().await.unwrap_or_else(|e| {
13                format!("echo 'Atuin: failed to read and generate aliases: \n{r}\n{e}'",)
14            })
15        }
16    }
17}
18
19async fn cached_vars(path: PathBuf, store: &VarStore) -> String {
20    match tokio::fs::read_to_string(path).await {
21        Ok(vars) => vars,
22        Err(r) => {
23            // we failed to read the file for some reason, but the file does exist
24            // fallback to generating new vars on the fly
25
26            store.powershell().await.unwrap_or_else(|e| {
27                format!("echo 'Atuin: failed to read and generate vars: \n{r}\n{e}'",)
28            })
29        }
30    }
31}
32
33/// Return powershell dotfile config
34///
35/// Do not return an error. We should not prevent the shell from starting.
36///
37/// In the worst case, Atuin should not function but the shell should start correctly.
38///
39/// While currently this only returns aliases, it will be extended to also return other synced dotfiles
40pub async fn alias_config(store: &AliasStore) -> String {
41    // First try to read the cached config
42    let aliases = atuin_common::utils::dotfiles_cache_dir().join("aliases.ps1");
43
44    if aliases.exists() {
45        return cached_aliases(aliases, store).await;
46    }
47
48    if let Err(e) = store.build().await {
49        return format!("echo 'Atuin: failed to generate aliases: {e}'");
50    }
51
52    cached_aliases(aliases, store).await
53}
54
55pub async fn var_config(store: &VarStore) -> String {
56    // First try to read the cached config
57    let vars = atuin_common::utils::dotfiles_cache_dir().join("vars.ps1");
58
59    if vars.exists() {
60        return cached_vars(vars, store).await;
61    }
62
63    if let Err(e) = store.build().await {
64        return format!("echo 'Atuin: failed to generate vars: {e}'");
65    }
66
67    cached_vars(vars, store).await
68}
69
70pub fn format_alias(alias: &Alias) -> String {
71    // Set-Alias doesn't support adding implicit arguments, so use a function.
72    // See https://github.com/PowerShell/PowerShell/issues/12962
73
74    let mut result = secure_command(&format!(
75        "function {} {{\n    {}{} @args\n}}",
76        alias.name,
77        if alias.value.starts_with(['"', '\'']) {
78            "& "
79        } else {
80            ""
81        },
82        alias.value
83    ));
84
85    // This makes the file layout prettier
86    result.insert(0, '\n');
87    result
88}
89
90pub fn format_var(var: &Var) -> String {
91    secure_command(&format!(
92        "${}{} = '{}'",
93        if var.export { "env:" } else { "" },
94        var.name,
95        var.value.replace("'", "''")
96    ))
97}
98
99/// Wraps the given command in an Invoke-Expression to ensure the outer script is not halted
100/// if the inner command contains a syntax error.
101fn secure_command(command: &str) -> String {
102    format!(
103        "Invoke-Expression -ErrorAction Continue -Command '{}'\n",
104        command.replace("'", "''")
105    )
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn aliases() {
114        assert_eq!(
115            format_alias(&Alias {
116                name: "gp".to_string(),
117                value: "git push".to_string(),
118            }),
119            "\n".to_string()
120                + &secure_command(
121                    "function gp {
122    git push @args
123}"
124                )
125        );
126
127        assert_eq!(
128            format_alias(&Alias {
129                name: "spc".to_string(),
130                value: "\"path with spaces\" arg".to_string(),
131            }),
132            "\n".to_string()
133                + &secure_command(
134                    "function spc {
135    & \"path with spaces\" arg @args
136}"
137                )
138        );
139    }
140
141    #[test]
142    fn vars() {
143        assert_eq!(
144            format_var(&Var {
145                name: "FOO".to_owned(),
146                value: "bar 'baz'".to_owned(),
147                export: true,
148            }),
149            secure_command("$env:FOO = 'bar ''baz'''")
150        );
151
152        assert_eq!(
153            format_var(&Var {
154                name: "TEST".to_owned(),
155                value: "1".to_owned(),
156                export: false,
157            }),
158            secure_command("$TEST = '1'")
159        );
160    }
161
162    #[test]
163    fn invoke_expression() {
164        assert_eq!(
165            secure_command("echo 'foo'"),
166            "Invoke-Expression -ErrorAction Continue -Command 'echo ''foo'''\n"
167        )
168    }
169}