From d7ee07c563883eb8583e9af4d7fccdadb8d57a7f Mon Sep 17 00:00:00 2001 From: Surma Date: Wed, 10 Dec 2025 16:02:49 +0000 Subject: [PATCH 1/3] Implement nushell support --- sh/shadowenv.nushell.in | 20 +++++++++++++++++ src/cli.rs | 7 ++++++ src/hook.rs | 48 +++++++++++++++++++++++++++++++++++++++++ src/init.rs | 5 +++++ 4 files changed, 80 insertions(+) create mode 100644 sh/shadowenv.nushell.in diff --git a/sh/shadowenv.nushell.in b/sh/shadowenv.nushell.in new file mode 100644 index 0000000..8ed279b --- /dev/null +++ b/sh/shadowenv.nushell.in @@ -0,0 +1,20 @@ +$env.config = ($env.config | upsert hooks.env_change.PWD { |config| + let existing = $config | get -o hooks.env_change.PWD | default [] + $existing | append {|| + mut flags = ["--json"] + + if ($env.__shadowenv_force_run? | default false) { + hide-env -i __shadowenv_force_run + $flags = ($flags | append "--force") + } + + let result = @SELF@ hook ...$flags | complete + if $result.exit_code != 0 { + return + } + + $result.stdout | from json | get -o exported | default {} | load-env + } +}) + +$env.__shadowenv_force_run = true diff --git a/src/cli.rs b/src/cli.rs index 8c3d56c..3211fd5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -78,6 +78,10 @@ pub struct FormatOptions { #[arg(long)] pub fish: bool, + /// Format variable assignments for nushell. + #[arg(long)] + pub nushell: bool, + /// Format variable assignments as JSON. #[arg(long)] pub json: bool, @@ -103,6 +107,9 @@ pub enum InitCmd { /// Prints a script which can be eval'd by fish to set up shadowenv. Fish, + + /// Prints a script which can be eval'd by nushell to set up shadowenv. + Nushell, } /// Options shared by all init subcommands diff --git a/src/hook.rs b/src/hook.rs index 248ab70..f6d6d14 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -15,6 +15,7 @@ use std::{borrow::Cow, collections::HashMap, env, path::PathBuf, result::Result, pub enum VariableOutputMode { Fish, + Nushell, Porcelain, Posix, Json, @@ -43,6 +44,8 @@ pub fn run(cmd: HookCmd) -> Result<(), Error> { VariableOutputMode::Porcelain } else if cmd.format.fish { VariableOutputMode::Fish + } else if cmd.format.nushell { + VariableOutputMode::Nushell } else if cmd.format.json { VariableOutputMode::Json } else if cmd.format.pretty_json { @@ -219,6 +222,33 @@ pub fn apply_env(shadowenv: &Shadowenv, mode: VariableOutputMode) -> Result<(), shadowenv.features(), ); } + VariableOutputMode::Nushell => { + for (k, v) in shadowenv.exports()? { + match v { + Some(s) => { + if k == "PATH" { + let paths: Vec<&str> = s.split(':').collect(); + let pathlist = paths + .iter() + .map(|p| nushell_escape(p)) + .collect::>() + .join(", "); + println!("$env.{} = [{}]", k, pathlist); + } else { + println!("$env.{} = {}", k, nushell_escape(&s)); + } + } + None => { + println!("hide-env {}", k); + } + } + } + output::print_activation_to_tty( + shadowenv.current_dirs(), + shadowenv.prev_dirs(), + shadowenv.features(), + ); + } VariableOutputMode::Porcelain => { // three fields: : : // opcodes: 1: set, unexported (unused) @@ -249,6 +279,24 @@ fn shell_escape(s: &str) -> String { shell::escape(Cow::from(s)).to_string() } +fn nushell_escape(s: &str) -> String { + let mut result = String::with_capacity(s.len() + 2); + result.push('"'); + for c in s.chars() { + match c { + '"' => result.push_str("\\\""), + '\\' => result.push_str("\\\\"), + '\n' => result.push_str("\\n"), + '\r' => result.push_str("\\r"), + '\t' => result.push_str("\\t"), + '$' => result.push_str("\\$"), + _ => result.push(c), + } + } + result.push('"'); + result +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/init.rs b/src/init.rs index b231597..00027f0 100644 --- a/src/init.rs +++ b/src/init.rs @@ -21,6 +21,11 @@ pub fn run(cmd: InitCmd) { include_bytes!("../sh/shadowenv.fish.in"), true, // Fish doesn't use hookbook ), + Nushell => print_script( + pb, + include_bytes!("../sh/shadowenv.nushell.in"), + true, // Nushell doesn't use hookbook + ), }; } From 149c4b4cc9e048695f6e2cb7025cec5f5c09a71b Mon Sep 17 00:00:00 2001 From: Surma Date: Wed, 10 Dec 2025 16:08:22 +0000 Subject: [PATCH 2/3] Remove nu hook --- src/cli.rs | 4 ---- src/hook.rs | 48 ------------------------------------------------ 2 files changed, 52 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 3211fd5..e3af9f1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -78,10 +78,6 @@ pub struct FormatOptions { #[arg(long)] pub fish: bool, - /// Format variable assignments for nushell. - #[arg(long)] - pub nushell: bool, - /// Format variable assignments as JSON. #[arg(long)] pub json: bool, diff --git a/src/hook.rs b/src/hook.rs index f6d6d14..248ab70 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -15,7 +15,6 @@ use std::{borrow::Cow, collections::HashMap, env, path::PathBuf, result::Result, pub enum VariableOutputMode { Fish, - Nushell, Porcelain, Posix, Json, @@ -44,8 +43,6 @@ pub fn run(cmd: HookCmd) -> Result<(), Error> { VariableOutputMode::Porcelain } else if cmd.format.fish { VariableOutputMode::Fish - } else if cmd.format.nushell { - VariableOutputMode::Nushell } else if cmd.format.json { VariableOutputMode::Json } else if cmd.format.pretty_json { @@ -222,33 +219,6 @@ pub fn apply_env(shadowenv: &Shadowenv, mode: VariableOutputMode) -> Result<(), shadowenv.features(), ); } - VariableOutputMode::Nushell => { - for (k, v) in shadowenv.exports()? { - match v { - Some(s) => { - if k == "PATH" { - let paths: Vec<&str> = s.split(':').collect(); - let pathlist = paths - .iter() - .map(|p| nushell_escape(p)) - .collect::>() - .join(", "); - println!("$env.{} = [{}]", k, pathlist); - } else { - println!("$env.{} = {}", k, nushell_escape(&s)); - } - } - None => { - println!("hide-env {}", k); - } - } - } - output::print_activation_to_tty( - shadowenv.current_dirs(), - shadowenv.prev_dirs(), - shadowenv.features(), - ); - } VariableOutputMode::Porcelain => { // three fields: : : // opcodes: 1: set, unexported (unused) @@ -279,24 +249,6 @@ fn shell_escape(s: &str) -> String { shell::escape(Cow::from(s)).to_string() } -fn nushell_escape(s: &str) -> String { - let mut result = String::with_capacity(s.len() + 2); - result.push('"'); - for c in s.chars() { - match c { - '"' => result.push_str("\\\""), - '\\' => result.push_str("\\\\"), - '\n' => result.push_str("\\n"), - '\r' => result.push_str("\\r"), - '\t' => result.push_str("\\t"), - '$' => result.push_str("\\$"), - _ => result.push(c), - } - } - result.push('"'); - result -} - #[cfg(test)] mod tests { use super::*; From 886944fa44194a58f6b172a53ae8fb3136ad6cb8 Mon Sep 17 00:00:00 2001 From: Surma Date: Wed, 10 Dec 2025 16:25:12 +0000 Subject: [PATCH 3/3] Update README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e268950..087701d 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,14 @@ some limited ability to make the manipulations dynamic. ![shadowenv in action](https://burkelibbey.s3.amazonaws.com/shadowenv.gif) -In order to use shadowenv, add a line to your shell profile (`.zshrc`, `.bash_profile`, or -`config.fish`) reading: +In order to use shadowenv, add a line to your shell profile (`.zshrc`, `.bash_profile`, +`config.fish`, or `config.nu`) reading: ```bash eval "$(shadowenv init bash)" # for bash eval "$(shadowenv init zsh)" # for zsh shadowenv init fish | source # for fish +# for nushell, paste the output of `shadowenv init nushell` into config.nu ``` With this code loaded, upon entering a directory containing a `.shadowenv.d` directory,