From d6c2cd9091a68ffb0ce9eece35db870535584311 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 8 Dec 2024 16:21:23 -0600 Subject: [PATCH] feat: shell hooks (#3414) Fixes https://github.com/direnv/direnv/issues/443 ;) --- docs/hooks.md | 27 +++++++++++++++++ schema/mise.json | 30 +++++++++++++++++-- src/cli/hook_env.rs | 42 ++++++++++++++++++++++---- src/config/config_file/mise_toml.rs | 19 ++++++++---- src/hooks.rs | 46 ++++++++++++++++++++++++++--- src/shell/bash.rs | 7 +++++ src/shell/elvish.rs | 10 +++++-- src/shell/fish.rs | 7 +++++ src/shell/mod.rs | 2 +- src/shell/nushell.rs | 6 ++++ src/shell/xonsh.rs | 7 +++++ src/shell/zsh.rs | 7 +++++ 12 files changed, 190 insertions(+), 20 deletions(-) diff --git a/docs/hooks.md b/docs/hooks.md index 87bb10b88b..ce2987deb6 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -60,3 +60,30 @@ Hooks are executed with the following environment variables set: - `MISE_ORIGINAL_CWD`: The directory that the user is in. - `MISE_PROJECT_DIR`: The root directory of the project. - `MISE_PREVIOUS_DIR`: The directory that the user was in before the directory change (only if a directory change occurred). + +## Shell hooks + +Hooks can be executed in the current shell, for example if you'd like to add bash completions when entering a directory: + +```toml +[hooks.enter] +shell = "bash" +script = "source completions.sh" +``` + +## Multiple hooks syntax + +You can use arrays to define multiple hooks in the same file: + +```toml +[hooks] +enter = [ + "echo 'I entered the project'", + "echo 'I am in the project'" +] + +[[hooks.cd]] +script = "echo 'I changed directories'" +[[hooks.cd]] +script = "echo 'I also directories'" +``` diff --git a/schema/mise.json b/schema/mise.json index 937db82a1c..b64709cf15 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -961,8 +961,34 @@ "description": "hooks to run", "type": "object", "additionalProperties": { - "description": "script to run", - "type": "string" + "oneOf": [ + { + "description": "script to run", + "type": "string" + }, + { + "description": "script to run", + "items": { + "description": "script to run", + "type": "string" + }, + "type": "array" + }, + { + "additionalProperties": false, + "properties": { + "script": { + "description": "script to run", + "type": "string" + }, + "shell": { + "description": "specify the shell to run the script inside of", + "type": "string" + } + }, + "type": "object" + } + ] } }, "watch_files": { diff --git a/src/cli/hook_env.rs b/src/cli/hook_env.rs index df30548666..b80754210e 100644 --- a/src/cli/hook_env.rs +++ b/src/cli/hook_env.rs @@ -1,17 +1,17 @@ -use std::env::{join_paths, split_paths}; -use std::ops::Deref; -use std::path::PathBuf; - use console::truncate_str; use eyre::Result; use itertools::Itertools; +use std::env::{join_paths, split_paths}; +use std::ops::Deref; +use std::path::PathBuf; use crate::config::{Config, Settings}; use crate::direnv::DirenvDiff; use crate::env::{PATH_KEY, TERM_WIDTH, __MISE_DIFF}; use crate::env_diff::{EnvDiff, EnvDiffOperation}; use crate::hook_env::WatchFilePattern; -use crate::shell::{get_shell, ShellType}; +use crate::hooks::Hooks; +use crate::shell::{get_shell, Shell, ShellType}; use crate::toolset::{Toolset, ToolsetBuilder}; use crate::{dirs, env, hook_env, hooks, watch_files}; @@ -66,12 +66,44 @@ impl HookEnv { let output = hook_env::build_env_commands(&*shell, &patches); miseprint!("{output}")?; self.display_status(&config, &ts)?; + + self.run_shell_hooks(&config, &*shell)?; hooks::run_all_hooks(&ts); watch_files::execute_runs(&ts); Ok(()) } + fn run_shell_hooks(&self, config: &Config, shell: &dyn Shell) -> Result<()> { + let hooks = config.hooks()?; + for h in hooks::SCHEDULED_HOOKS.lock().unwrap().iter() { + let hooks = hooks + .iter() + .map(|(_p, hook)| hook) + .filter(|hook| hook.hook == *h && hook.shell == Some(shell.to_string())) + .collect_vec(); + match *h { + Hooks::Enter => { + for hook in hooks { + miseprintln!("{}", hook.script); + } + } + Hooks::Cd => { + for hook in hooks { + miseprintln!("{}", hook.script); + } + } + Hooks::Leave => { + for _hook in hooks { + warn!("leave hook not yet implemented"); + } + } + _ => {} + } + } + Ok(()) + } + fn display_status(&self, config: &Config, ts: &Toolset) -> Result<()> { let settings = Settings::get(); if self.status || settings.status.show_tools { diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index 7de862c3f1..c97aaabe2a 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -49,7 +49,7 @@ pub struct MiseToml { #[serde(skip)] doc: OnceCell, #[serde(default)] - hooks: IndexMap, + hooks: IndexMap, #[serde(default)] tools: IndexMap, #[serde(default)] @@ -455,13 +455,20 @@ impl ConfigFile for MiseToml { } fn hooks(&self) -> eyre::Result> { - self.hooks + Ok(self + .hooks .iter() - .map(|(hook, run)| { - let run = self.parse_template(run)?; - Ok(Hook { hook: *hook, run }) + .map(|(hook, val)| { + let mut hooks = Hook::from_toml(*hook, val.clone())?; + for hook in hooks.iter_mut() { + hook.script = self.parse_template(&hook.script)?; + } + eyre::Ok(hooks) }) - .collect() + .collect::>>()? + .into_iter() + .flatten() + .collect()) } fn vars(&self) -> eyre::Result<&IndexMap> { diff --git a/src/hooks.rs b/src/hooks.rs index a9f829deda..2a2330c057 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -2,7 +2,7 @@ use crate::cmd::cmd; use crate::config::{Config, SETTINGS}; use crate::toolset::Toolset; use crate::{dirs, hook_env}; -use eyre::Result; +use eyre::{eyre, Result}; use indexmap::IndexSet; use itertools::Itertools; use once_cell::sync::Lazy; @@ -35,7 +35,8 @@ pub enum Hooks { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Hook { pub hook: Hooks, - pub run: String, + pub script: String, + pub shell: Option, } pub static SCHEDULED_HOOKS: Lazy>> = Lazy::new(Default::default); @@ -56,7 +57,7 @@ pub fn run_one_hook(ts: &Toolset, hook: Hooks) { let config = Config::get(); let hooks = config.hooks().unwrap_or_default(); for (root, h) in hooks { - if hook != h.hook { + if hook != h.hook || h.shell.is_some() { continue; } trace!("running hook {hook} in {root:?}"); @@ -86,6 +87,43 @@ pub fn run_one_hook(ts: &Toolset, hook: Hooks) { } } +impl Hook { + pub fn from_toml(hook: Hooks, value: toml::Value) -> Result> { + match value { + toml::Value::String(run) => Ok(vec![Hook { + hook, + script: run, + shell: None, + }]), + toml::Value::Table(tbl) => { + let script = tbl + .get("script") + .ok_or_else(|| eyre!("missing `script` key"))?; + let script = script + .as_str() + .ok_or_else(|| eyre!("`run` must be a string"))?; + let shell = tbl + .get("shell") + .and_then(|s| s.as_str()) + .map(|s| s.to_string()); + Ok(vec![Hook { + hook, + script: script.to_string(), + shell, + }]) + } + toml::Value::Array(arr) => { + let mut hooks = vec![]; + for v in arr { + hooks.extend(Self::from_toml(hook, v)?); + } + Ok(hooks) + } + v => panic!("invalid hook value: {v}"), + } + } +} + fn execute(ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> { SETTINGS.ensure_experimental("hooks")?; #[cfg(unix)] @@ -97,7 +135,7 @@ fn execute(ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> { .iter() .skip(1) .map(|s| s.as_str()) - .chain(once(hook.run.as_str())) + .chain(once(hook.script.as_str())) .collect_vec(); let mut env = ts.full_env()?; if let Some(cwd) = dirs::CWD.as_ref() { diff --git a/src/shell/bash.rs b/src/shell/bash.rs index a5830c04d6..96aa74658d 100644 --- a/src/shell/bash.rs +++ b/src/shell/bash.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::path::Path; use indoc::formatdoc; @@ -104,6 +105,12 @@ impl Shell for Bash { } } +impl Display for Bash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "bash") + } +} + #[cfg(test)] mod tests { use insta::assert_snapshot; diff --git a/src/shell/elvish.rs b/src/shell/elvish.rs index 3812e21763..88af436d53 100644 --- a/src/shell/elvish.rs +++ b/src/shell/elvish.rs @@ -1,8 +1,8 @@ +use std::fmt::Display; use std::path::Path; -use indoc::formatdoc; - use crate::shell::Shell; +use indoc::formatdoc; #[derive(Default)] pub struct Elvish {} @@ -84,6 +84,12 @@ impl Shell for Elvish { } } +impl Display for Elvish { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "elvish") + } +} + #[cfg(test)] mod tests { use insta::assert_snapshot; diff --git a/src/shell/fish.rs b/src/shell/fish.rs index efb02ba0d8..e9bdb78699 100644 --- a/src/shell/fish.rs +++ b/src/shell/fish.rs @@ -1,3 +1,4 @@ +use std::fmt::{Display, Formatter}; use std::path::Path; use crate::config::Settings; @@ -126,6 +127,12 @@ impl Shell for Fish { } } +impl Display for Fish { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "fish") + } +} + #[cfg(test)] mod tests { use insta::assert_snapshot; diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 94e04ae017..706750a170 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -65,7 +65,7 @@ impl Display for ShellType { } } -pub trait Shell { +pub trait Shell: Display { fn activate(&self, exe: &Path, flags: String) -> String; fn deactivate(&self) -> String; fn set_env(&self, k: &str, v: &str) -> String; diff --git a/src/shell/nushell.rs b/src/shell/nushell.rs index bfd053e74f..06ba4e7aa1 100644 --- a/src/shell/nushell.rs +++ b/src/shell/nushell.rs @@ -118,6 +118,12 @@ impl Shell for Nushell { } } +impl Display for Nushell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "nu") + } +} + #[cfg(test)] mod tests { use insta::assert_snapshot; diff --git a/src/shell/xonsh.rs b/src/shell/xonsh.rs index cbbcc8250d..790d2f6d2d 100644 --- a/src/shell/xonsh.rs +++ b/src/shell/xonsh.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::fmt::Display; use std::path::Path; use indoc::formatdoc; @@ -138,6 +139,12 @@ impl Shell for Xonsh { } } +impl Display for Xonsh { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "xonsh") + } +} + #[cfg(test)] mod tests { use insta::assert_snapshot; diff --git a/src/shell/zsh.rs b/src/shell/zsh.rs index ad2342b907..c5ff82b486 100644 --- a/src/shell/zsh.rs +++ b/src/shell/zsh.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::path::Path; use indoc::formatdoc; @@ -104,6 +105,12 @@ impl Shell for Zsh { } } +impl Display for Zsh { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Zsh") + } +} + #[cfg(test)] mod tests { use insta::assert_snapshot;