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