atuin_dotfiles/
shell.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
use eyre::{ensure, eyre, Result};
use rmp::{decode, encode};
use serde::Serialize;

use atuin_common::shell::{Shell, ShellError};

use crate::store::AliasStore;

pub mod bash;
pub mod fish;
pub mod xonsh;
pub mod zsh;

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Alias {
    pub name: String,
    pub value: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Var {
    pub name: String,
    pub value: String,

    // False? This is a _shell var_
    // True? This is an _env var_
    pub export: bool,
}

impl Var {
    /// Serialize into the given vec
    /// This is intended to be called by the store
    pub fn serialize(&self, output: &mut Vec<u8>) -> Result<()> {
        encode::write_array_len(output, 3)?; // 3 fields

        encode::write_str(output, self.name.as_str())?;
        encode::write_str(output, self.value.as_str())?;
        encode::write_bool(output, self.export)?;

        Ok(())
    }

    pub fn deserialize(bytes: &mut decode::Bytes) -> Result<Self> {
        fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
            eyre!("{err:?}")
        }

        let nfields = decode::read_array_len(bytes).map_err(error_report)?;

        ensure!(
            nfields == 3,
            "too many entries in v0 dotfiles env create record, got {}, expected {}",
            nfields,
            3
        );

        let bytes = bytes.remaining_slice();

        let (key, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
        let (value, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;

        let mut bytes = decode::Bytes::new(bytes);
        let export = decode::read_bool(&mut bytes).map_err(error_report)?;

        ensure!(
            bytes.remaining_slice().is_empty(),
            "trailing bytes in encoded dotfiles env record, malformed"
        );

        Ok(Var {
            name: key.to_owned(),
            value: value.to_owned(),
            export,
        })
    }
}

pub fn parse_alias(line: &str) -> Option<Alias> {
    // consider the fact we might be importing a fish alias
    // 'alias' output
    // fish: alias foo bar
    // posix: foo=bar

    let is_fish = line.split(' ').next().unwrap_or("") == "alias";

    let parts: Vec<&str> = if is_fish {
        line.split(' ')
            .enumerate()
            .filter_map(|(n, i)| if n == 0 { None } else { Some(i) })
            .collect()
    } else {
        line.split('=').collect()
    };

    if parts.len() <= 1 {
        return None;
    }

    let mut parts = parts.iter().map(|s| s.to_string());

    let name = parts.next().unwrap().to_string();

    let remaining = if is_fish {
        parts.collect::<Vec<String>>().join(" ").to_string()
    } else {
        parts.collect::<Vec<String>>().join("=").to_string()
    };

    Some(Alias {
        name,
        value: remaining.trim().to_string(),
    })
}

pub fn existing_aliases(shell: Option<Shell>) -> Result<Vec<Alias>, ShellError> {
    let shell = if let Some(shell) = shell {
        shell
    } else {
        Shell::current()
    };

    // this only supports posix-y shells atm
    if !shell.is_posixish() {
        return Err(ShellError::NotSupported);
    }

    // This will return a list of aliases, each on its own line
    // They will be in the form foo=bar
    let aliases = shell.run_interactive(["alias"])?;

    let aliases: Vec<Alias> = aliases.lines().filter_map(parse_alias).collect();

    Ok(aliases)
}

/// Import aliases from the current shell
/// This will not import aliases already in the store
/// Returns aliases that were set
pub async fn import_aliases(store: &AliasStore) -> Result<Vec<Alias>> {
    let shell_aliases = existing_aliases(None)?;
    let store_aliases = store.aliases().await?;

    let mut res = Vec::new();

    for alias in shell_aliases {
        // O(n), but n is small, and imports infrequent
        // can always make a map
        if store_aliases.contains(&alias) {
            continue;
        }

        res.push(alias.clone());
        store.set(&alias.name, &alias.value).await?;
    }

    Ok(res)
}

#[cfg(test)]
mod tests {
    use crate::shell::{parse_alias, Alias};

    #[test]
    fn test_parse_simple_alias() {
        let alias = super::parse_alias("foo=bar").expect("failed to parse alias");
        assert_eq!(alias.name, "foo");
        assert_eq!(alias.value, "bar");
    }

    #[test]
    fn test_parse_quoted_alias() {
        let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw'")
            .expect("failed to parse alias");

        assert_eq!(alias.name, "emacs");
        assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw'");

        let git_alias = super::parse_alias("gwip='git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'").expect("failed to parse alias");
        assert_eq!(git_alias.name, "gwip");
        assert_eq!(git_alias.value, "'git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'");
    }

    #[test]
    fn test_parse_quoted_alias_equals() {
        let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw --foo=bar'")
            .expect("failed to parse alias");
        assert_eq!(alias.name, "emacs");
        assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
    }

    #[test]
    fn test_parse_fish() {
        let alias = super::parse_alias("alias foo bar").expect("failed to parse alias");
        assert_eq!(alias.name, "foo");
        assert_eq!(alias.value, "bar");

        let alias =
            super::parse_alias("alias x 'exa --icons --git --classify --group-directories-first'")
                .expect("failed to parse alias");

        assert_eq!(alias.name, "x");
        assert_eq!(
            alias.value,
            "'exa --icons --git --classify --group-directories-first'"
        );
    }

    #[test]
    fn test_parse_with_fortune() {
        // Because we run the alias command in an interactive subshell
        // there may be other output.
        // Ensure that the parser can handle it
        // Annoyingly not all aliases are picked up all the time if we use
        // a non-interactive subshell. Boo.
        let shell = "
/ In a consumer society there are     \\
| inevitably two kinds of slaves: the |
| prisoners of addiction and the      |
\\ prisoners of envy.                  /
 -------------------------------------
        \\   ^__^
         \\  (oo)\\_______
            (__)\\       )\\/\\
                ||----w |
                ||     ||
emacs='TERM=xterm-24bits emacs -nw --foo=bar'
k=kubectl
";

        let aliases: Vec<Alias> = shell.lines().filter_map(parse_alias).collect();
        assert_eq!(aliases[0].name, "emacs");
        assert_eq!(aliases[0].value, "'TERM=xterm-24bits emacs -nw --foo=bar'");

        assert_eq!(aliases[1].name, "k");
        assert_eq!(aliases[1].value, "kubectl");
    }
}