1use eyre::{ensure, eyre, Result};
2use rmp::{decode, encode};
3use serde::Serialize;
4
5use atuin_common::shell::{Shell, ShellError};
6
7use crate::store::AliasStore;
8
9pub mod bash;
10pub mod fish;
11pub mod xonsh;
12pub mod zsh;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15pub struct Alias {
16 pub name: String,
17 pub value: String,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21pub struct Var {
22 pub name: String,
23 pub value: String,
24
25 pub export: bool,
28}
29
30impl Var {
31 pub fn serialize(&self, output: &mut Vec<u8>) -> Result<()> {
34 encode::write_array_len(output, 3)?; encode::write_str(output, self.name.as_str())?;
37 encode::write_str(output, self.value.as_str())?;
38 encode::write_bool(output, self.export)?;
39
40 Ok(())
41 }
42
43 pub fn deserialize(bytes: &mut decode::Bytes) -> Result<Self> {
44 fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
45 eyre!("{err:?}")
46 }
47
48 let nfields = decode::read_array_len(bytes).map_err(error_report)?;
49
50 ensure!(
51 nfields == 3,
52 "too many entries in v0 dotfiles env create record, got {}, expected {}",
53 nfields,
54 3
55 );
56
57 let bytes = bytes.remaining_slice();
58
59 let (key, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
60 let (value, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
61
62 let mut bytes = decode::Bytes::new(bytes);
63 let export = decode::read_bool(&mut bytes).map_err(error_report)?;
64
65 ensure!(
66 bytes.remaining_slice().is_empty(),
67 "trailing bytes in encoded dotfiles env record, malformed"
68 );
69
70 Ok(Var {
71 name: key.to_owned(),
72 value: value.to_owned(),
73 export,
74 })
75 }
76}
77
78pub fn parse_alias(line: &str) -> Option<Alias> {
79 let is_fish = line.split(' ').next().unwrap_or("") == "alias";
85
86 let parts: Vec<&str> = if is_fish {
87 line.split(' ')
88 .enumerate()
89 .filter_map(|(n, i)| if n == 0 { None } else { Some(i) })
90 .collect()
91 } else {
92 line.split('=').collect()
93 };
94
95 if parts.len() <= 1 {
96 return None;
97 }
98
99 let mut parts = parts.iter().map(|s| s.to_string());
100
101 let name = parts.next().unwrap().to_string();
102
103 let remaining = if is_fish {
104 parts.collect::<Vec<String>>().join(" ").to_string()
105 } else {
106 parts.collect::<Vec<String>>().join("=").to_string()
107 };
108
109 Some(Alias {
110 name,
111 value: remaining.trim().to_string(),
112 })
113}
114
115pub fn existing_aliases(shell: Option<Shell>) -> Result<Vec<Alias>, ShellError> {
116 let shell = if let Some(shell) = shell {
117 shell
118 } else {
119 Shell::current()
120 };
121
122 if !shell.is_posixish() {
124 return Err(ShellError::NotSupported);
125 }
126
127 let aliases = shell.run_interactive(["alias"])?;
130
131 let aliases: Vec<Alias> = aliases.lines().filter_map(parse_alias).collect();
132
133 Ok(aliases)
134}
135
136pub async fn import_aliases(store: &AliasStore) -> Result<Vec<Alias>> {
140 let shell_aliases = existing_aliases(None)?;
141 let store_aliases = store.aliases().await?;
142
143 let mut res = Vec::new();
144
145 for alias in shell_aliases {
146 if store_aliases.contains(&alias) {
149 continue;
150 }
151
152 res.push(alias.clone());
153 store.set(&alias.name, &alias.value).await?;
154 }
155
156 Ok(res)
157}
158
159#[cfg(test)]
160mod tests {
161 use crate::shell::{parse_alias, Alias};
162
163 #[test]
164 fn test_parse_simple_alias() {
165 let alias = super::parse_alias("foo=bar").expect("failed to parse alias");
166 assert_eq!(alias.name, "foo");
167 assert_eq!(alias.value, "bar");
168 }
169
170 #[test]
171 fn test_parse_quoted_alias() {
172 let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw'")
173 .expect("failed to parse alias");
174
175 assert_eq!(alias.name, "emacs");
176 assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw'");
177
178 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");
179 assert_eq!(git_alias.name, "gwip");
180 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]\"'");
181 }
182
183 #[test]
184 fn test_parse_quoted_alias_equals() {
185 let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw --foo=bar'")
186 .expect("failed to parse alias");
187 assert_eq!(alias.name, "emacs");
188 assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
189 }
190
191 #[test]
192 fn test_parse_fish() {
193 let alias = super::parse_alias("alias foo bar").expect("failed to parse alias");
194 assert_eq!(alias.name, "foo");
195 assert_eq!(alias.value, "bar");
196
197 let alias =
198 super::parse_alias("alias x 'exa --icons --git --classify --group-directories-first'")
199 .expect("failed to parse alias");
200
201 assert_eq!(alias.name, "x");
202 assert_eq!(
203 alias.value,
204 "'exa --icons --git --classify --group-directories-first'"
205 );
206 }
207
208 #[test]
209 fn test_parse_with_fortune() {
210 let shell = "
216/ In a consumer society there are \\
217| inevitably two kinds of slaves: the |
218| prisoners of addiction and the |
219\\ prisoners of envy. /
220 -------------------------------------
221 \\ ^__^
222 \\ (oo)\\_______
223 (__)\\ )\\/\\
224 ||----w |
225 || ||
226emacs='TERM=xterm-24bits emacs -nw --foo=bar'
227k=kubectl
228";
229
230 let aliases: Vec<Alias> = shell.lines().filter_map(parse_alias).collect();
231 assert_eq!(aliases[0].name, "emacs");
232 assert_eq!(aliases[0].value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
233
234 assert_eq!(aliases[1].name, "k");
235 assert_eq!(aliases[1].value, "kubectl");
236 }
237}