feat(dotfiles): add alias import (#1938)

* feat(dotfiles): add alias import

* things

* clippy clappy
This commit is contained in:
Ellie Huxtable 2024-04-10 13:01:48 +01:00 committed by GitHub
parent 0ab9f4d9ff
commit 7ced31c354
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 192 additions and 3 deletions

2
Cargo.lock generated
View File

@ -285,6 +285,8 @@ dependencies = [
"semver",
"serde",
"sqlx",
"sysinfo",
"thiserror",
"time",
"typed-builder",
"uuid",

View File

@ -21,6 +21,8 @@ typed-builder = { workspace = true }
eyre = { workspace = true }
sqlx = { workspace = true }
semver = { workspace = true }
thiserror = { workspace = true }
sysinfo = "0.30.7"
lazy_static = "1.4.0"

View File

@ -54,4 +54,5 @@ macro_rules! new_uuid {
pub mod api;
pub mod record;
pub mod shell;
pub mod utils;

66
atuin-common/src/shell.rs Normal file
View File

@ -0,0 +1,66 @@
use sysinfo::{get_current_pid, Process, System};
use thiserror::Error;
pub enum Shell {
Sh,
Bash,
Fish,
Zsh,
Xonsh,
Nu,
Unknown,
}
#[derive(Debug, Error)]
pub enum ShellError {
#[error("shell not supported")]
NotSupported,
#[error("failed to execute shell command: {0}")]
ExecError(String),
}
pub fn shell() -> Shell {
let name = shell_name(None);
match name.as_str() {
"bash" => Shell::Bash,
"fish" => Shell::Fish,
"zsh" => Shell::Zsh,
"xonsh" => Shell::Xonsh,
"nu" => Shell::Nu,
"sh" => Shell::Sh,
_ => Shell::Unknown,
}
}
impl Shell {
/// Returns true if the shell is posix-like
/// Note that while fish is not posix compliant, it behaves well enough for our current
/// featureset that this does not matter.
pub fn is_posixish(&self) -> bool {
matches!(self, Shell::Bash | Shell::Fish | Shell::Zsh)
}
}
pub fn shell_name(parent: Option<&Process>) -> String {
let sys = System::new_all();
let parent = if let Some(parent) = parent {
parent
} else {
let process = sys
.process(get_current_pid().expect("Failed to get current PID"))
.expect("Process with current pid does not exist");
sys.process(process.parent().expect("Atuin running with no parent!"))
.expect("Process with parent pid does not exist")
};
let shell = parent.name().trim().to_lowercase();
let shell = shell.strip_prefix('-').unwrap_or(&shell);
shell.to_string()
}

View File

@ -1,3 +1,10 @@
use std::{ffi::OsStr, process::Command};
use atuin_common::shell::{shell, shell_name, ShellError};
use eyre::Result;
use crate::store::AliasStore;
pub mod bash;
pub mod fish;
pub mod xonsh;
@ -8,3 +15,96 @@ pub struct Alias {
pub name: String,
pub value: String,
}
pub fn run_interactive<I, S>(args: I) -> Result<String, ShellError>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let shell = shell_name(None);
let output = Command::new(shell)
.arg("-ic")
.args(args)
.output()
.map_err(|e| ShellError::ExecError(e.to_string()))?;
Ok(String::from_utf8(output.stdout).unwrap())
}
pub fn parse_alias(line: &str) -> Alias {
let mut parts = line.split('=');
let name = parts.next().unwrap().to_string();
let remaining = parts.collect::<Vec<&str>>().join("=").to_string();
Alias {
name,
value: remaining,
}
}
pub fn existing_aliases() -> Result<Vec<Alias>, ShellError> {
// 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 = run_interactive(["alias"])?;
let aliases: Vec<Alias> = aliases.lines().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()?;
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 {
#[test]
fn test_parse_simple_alias() {
let alias = super::parse_alias("foo=bar");
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'");
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]\"'");
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'");
assert_eq!(alias.name, "emacs");
assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
}
}

View File

@ -2,6 +2,7 @@ use std::process::Command;
use std::{env, path::PathBuf, str::FromStr};
use atuin_client::settings::Settings;
use atuin_common::shell::shell_name;
use colored::Colorize;
use eyre::Result;
use serde::{Deserialize, Serialize};
@ -144,9 +145,7 @@ impl ShellInfo {
.process(process.parent().expect("Atuin running with no parent!"))
.expect("Process with parent pid does not exist");
let shell = parent.name().trim().to_lowercase();
let shell = shell.strip_prefix('-').unwrap_or(&shell);
let name = shell.to_string();
let name = shell_name(Some(parent));
let plugins = ShellInfo::plugins(name.as_str(), parent);

View File

@ -8,9 +8,17 @@ use atuin_dotfiles::{shell::Alias, store::AliasStore};
#[derive(Subcommand, Debug)]
#[command(infer_subcommands = true)]
pub enum Cmd {
/// Set an alias
Set { name: String, value: String },
/// Delete an alias
Delete { name: String },
/// List all aliases
List,
/// Import aliases set in the current shell
Import,
}
impl Cmd {
@ -53,6 +61,16 @@ impl Cmd {
Ok(())
}
async fn import(&self, store: AliasStore) -> Result<()> {
let aliases = atuin_dotfiles::shell::import_aliases(store).await?;
for i in aliases {
println!("Importing {}={}", i.name, i.value);
}
Ok(())
}
pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
if !settings.dotfiles.enabled {
eprintln!("Dotfiles are not enabled. Add\n\n[dotfiles]\nenabled = true\n\nto your configuration file to enable them.\n");
@ -71,6 +89,7 @@ impl Cmd {
Self::Set { name, value } => self.set(alias_store, name.clone(), value.clone()).await,
Self::Delete { name } => self.delete(alias_store, name.clone()).await,
Self::List => self.list(alias_store).await,
Self::Import => self.import(alias_store).await,
}
}
}