From 8525d4177da1cebcd02cf558a9af04bfc4c7cfe2 Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Tue, 18 Nov 2025 02:10:17 -0800 Subject: [PATCH 1/2] Add env attribute to set environment variables for recipes The env attribute accepts two arguments (env_var_name, value) and can be used multiple times per recipe to set environment variables: [env('API_KEY', 'secret')] [env('LOG_LEVEL', 'debug')] deploy: ./deploy.sh --- README.md | 12 +++++++ src/attribute.rs | 19 ++++++++++- src/recipe.rs | 12 +++++++ tests/attributes.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 31deca9980..059ff9012e 100644 --- a/README.md +++ b/README.md @@ -2158,6 +2158,7 @@ change their behavior. | `[confirm(PROMPT)]`1.23.0 | recipe | Require confirmation prior to executing recipe with a custom prompt. | | `[default]`1.43.0 | recipe | Use recipe as module's default recipe. | | `[doc(DOC)]`1.27.0 | module, recipe | Set recipe or module's [documentation comment](#documentation-comments) to `DOC`. | +| `[env(ENV_VAR, VALUE)]` 1.44 | recipe | Set env vars that apply only to this recipe. | | `[extension(EXT)]`1.32.0 | recipe | Set shebang recipe script's file extension to `EXT`. `EXT` should include a period if one is desired. | | `[group(NAME)]`1.27.0 | module, recipe | Put recipe or module in in [group](#groups) `NAME`. | | `[linux]`1.8.0 | recipe | Enable recipe on Linux. | @@ -2537,6 +2538,17 @@ test $RUST_BACKTRACE="1": cargo test ``` +Or you can use the `[env(env_var, value)]` attribute to set environment variables that +apply only to this recipe: + +```just + +[env("RUST_BACKTRACE", "1")] +test: + # will print a stack trace if it crashes + cargo test +``` + Exported variables and parameters are not exported to backticks in the same scope. ```just diff --git a/src/attribute.rs b/src/attribute.rs index 1cf9a71b78..3b77140b58 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -23,6 +23,7 @@ pub(crate) enum Attribute<'src> { Confirm(Option>), Default, Doc(Option>), + Env(StringLiteral<'src>, StringLiteral<'src>), ExitMessage, Extension(StringLiteral<'src>), Group(StringLiteral<'src>), @@ -61,6 +62,7 @@ impl AttributeDiscriminant { Self::Confirm | Self::Doc => 0..=1, Self::Script => 0..=usize::MAX, Self::Arg | Self::Extension | Self::Group | Self::WorkingDirectory => 1..=1, + Self::Env => 2..=2, Self::Metadata => 1..=usize::MAX, } } @@ -179,6 +181,20 @@ impl<'src> Attribute<'src> { AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()), AttributeDiscriminant::Default => Self::Default, AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()), + AttributeDiscriminant::Env => { + let [key, value]: [StringLiteral; 2] = + arguments + .try_into() + .map_err(|arguments: Vec| { + name.error(CompileErrorKind::AttributeArgumentCountMismatch { + attribute: name, + found: arguments.len(), + min: 2, + max: 2, + }) + })?; + Self::Env(key, value) + } AttributeDiscriminant::ExitMessage => Self::ExitMessage, AttributeDiscriminant::Extension => Self::Extension(arguments.into_iter().next().unwrap()), AttributeDiscriminant::Group => Self::Group(arguments.into_iter().next().unwrap()), @@ -243,7 +259,7 @@ impl<'src> Attribute<'src> { pub(crate) fn repeatable(&self) -> bool { matches!( self, - Attribute::Arg { .. } | Attribute::Group(_) | Attribute::Metadata(_), + Attribute::Arg { .. } | Attribute::Env(_, _) | Attribute::Group(_) | Attribute::Metadata(_), ) } } @@ -307,6 +323,7 @@ impl Display for Attribute<'_> { | Self::Extension(argument) | Self::Group(argument) | Self::WorkingDirectory(argument) => write!(f, "({argument})")?, + Self::Env(key, value) => write!(f, "({key}, {value})")?, Self::Metadata(arguments) => { write!(f, "(")?; for (i, argument) in arguments.iter().enumerate() { diff --git a/src/recipe.rs b/src/recipe.rs index 087dff9e42..1ee23d6509 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -334,6 +334,12 @@ impl<'src, D> Recipe<'src, D> { cmd.stdout(Stdio::null()); } + for attribute in &self.attributes { + if let Attribute::Env(key, value) = attribute { + cmd.env(&key.cooked, &value.cooked); + } + } + cmd.export( &context.module.settings, context.dotenv, @@ -473,6 +479,12 @@ impl<'src, D> Recipe<'src, D> { command.args(positional); } + for attribute in &self.attributes { + if let Attribute::Env(key, value) = attribute { + command.env(&key.cooked, &value.cooked); + } + } + command.export( &context.module.settings, context.dotenv, diff --git a/tests/attributes.rs b/tests/attributes.rs index 7c9b5e6112..5ef6bd4c04 100644 --- a/tests/attributes.rs +++ b/tests/attributes.rs @@ -323,3 +323,80 @@ fn shell_expanded_strings_can_be_used_in_attributes() { ) .run(); } + +#[test] +fn env_attribute_single() { + Test::new() + .justfile( + " + [env('MY_VAR', 'my_value')] + foo: + echo $MY_VAR + ", + ) + .stdout("my_value\n") + .stderr("echo $MY_VAR\n") + .run(); +} + +#[test] +fn env_attribute_multiple() { + Test::new() + .justfile( + " + [env('VAR1', 'value1')] + [env('VAR2', 'value 2')] + foo: + echo $VAR1 $VAR2 + ", + ) + .stdout("value1 value 2\n") + .stderr("echo $VAR1 $VAR2\n") + .run(); +} + +#[test] +fn env_attribute_1_arg() { + Test::new() + .justfile( + " + [env('MY_VAR')] + foo: + echo bar + ", + ) + .stderr( + " + error: Attribute `env` got 1 argument but takes 2 arguments + ——▶ justfile:1:2 + │ + 1 │ [env('MY_VAR')] + │ ^^^ +", + ) + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn env_attribute_3_args() { + Test::new() + .justfile( + " + [env('A', 'B', 'C')] + foo: + echo bar + ", + ) + .stderr( + " + error: Attribute `env` got 3 arguments but takes 2 arguments + ——▶ justfile:1:2 + │ + 1 │ [env('A', 'B', 'C')] + │ ^^^ +", + ) + .status(EXIT_FAILURE) + .run(); +} From a27d30b5932bb11bf78a67fa3a4e46f35b0c43e6 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 10 Jan 2026 17:48:51 -0800 Subject: [PATCH 2/2] Unwrap argument conversion, since we check the argument count above --- src/attribute.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index 3b77140b58..cb6732b9f1 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -182,17 +182,7 @@ impl<'src> Attribute<'src> { AttributeDiscriminant::Default => Self::Default, AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()), AttributeDiscriminant::Env => { - let [key, value]: [StringLiteral; 2] = - arguments - .try_into() - .map_err(|arguments: Vec| { - name.error(CompileErrorKind::AttributeArgumentCountMismatch { - attribute: name, - found: arguments.len(), - min: 2, - max: 2, - }) - })?; + let [key, value]: [StringLiteral; 2] = arguments.try_into().unwrap(); Self::Env(key, value) } AttributeDiscriminant::ExitMessage => Self::ExitMessage,