feat(env): add shell-style variable expansion in env values#8029
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Summary of ChangesHello @jdx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the flexibility of environment variable management within Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Pull request overview
This PR adds shell-style variable expansion ($VAR, ${VAR}, ${VAR:-default}) to mise.toml [env] values, controlled by a new env_shell_expand setting. The feature operates in three modes: explicitly enabled (true), explicitly disabled (false), or unset (default) which disables expansion but warns when $ is detected. Expansion runs after Tera template rendering, allowing both syntaxes to be combined.
Changes:
- Implements shell variable expansion using the
shellexpandcrate with custom variable lookup from the Tera context - Adds 3-way
env_shell_expandsetting (true/false/unset) with deprecation warning for unset mode - Includes comprehensive e2e tests covering basic expansion, brace syntax, defaults, initial environment, disabled mode, and mixed Tera+shell syntax
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/config/env_directive/mod.rs | Core expansion logic added after Tera rendering with conditional behavior based on env_shell_expand setting |
| settings.toml | New env_shell_expand setting definition with documentation and environment variable mapping |
| schema/mise.json | JSON schema update to include the new boolean env_shell_expand property |
| e2e/env/test_env_shell_expand | Six test scenarios validating expansion behavior across different modes and syntaxes |
| Cargo.toml | Added shellexpand crate dependency |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/config/env_directive/mod.rs
Outdated
| .and_then(|v| serde_json::from_value(v.clone()).ok()) | ||
| .unwrap_or_default(); | ||
| output = shellexpand::env_with_context_no_errors(&output, |var| { | ||
| env_vars.get(var).map(|v| Cow::Borrowed(v.as_str())) |
There was a problem hiding this comment.
The expansion logic doesn't fall back to system environment variables when a variable is not found in env_vars. According to the test case on line 32 of test_env_shell_expand, INITIAL_VAR from the initial environment should be expanded, but this implementation only looks up variables in the env context. Consider documenting this limitation or updating the lookup to include system environment variables.
| env_vars.get(var).map(|v| Cow::Borrowed(v.as_str())) | |
| if let Some(v) = env_vars.get(var) { | |
| Some(Cow::Borrowed(v.as_str())) | |
| } else { | |
| std::env::var(var).ok().map(Cow::Owned) | |
| } |
| (shell expansion will become the default in a future release) | ||
|
|
||
| Expansion happens after Tera template rendering, so both syntaxes can be mixed. | ||
| Undefined variables are left unexpanded (e.g., `$MISSING` stays as `$MISSING`). |
There was a problem hiding this comment.
The documentation states that undefined variables are left unexpanded, but this behavior may be misleading given that shellexpand::env_with_context_no_errors is used. When a variable is not found in the provided context, it will be left as-is, but this should be clarified to explain that it only checks the env map from the Tera context, not system environment variables (unless that's the intended behavior).
| Undefined variables are left unexpanded (e.g., `$MISSING` stays as `$MISSING`). | |
| Variables are resolved only from the `[env]` table / Tera context (`env` map), not from the process environment; if a variable is not found there, it is left unexpanded (e.g., `$MISSING` stays as `$MISSING`). |
There was a problem hiding this comment.
Code Review
This pull request introduces shell-style variable expansion for [env] values in mise.toml, a useful feature for more dynamic configurations. The implementation is well-structured, gated behind a new env_shell_expand setting, and includes comprehensive e2e tests, schema updates, and documentation. The core logic change in src/config/env_directive/mod.rs is sound. I've identified a minor performance improvement opportunity related to cloning data from the Tera context and have provided a suggestion to optimize it. Overall, this is a great addition to mise.
| let env_vars: BTreeMap<String, String> = ctx | ||
| .get("env") | ||
| .and_then(|v| serde_json::from_value(v.clone()).ok()) | ||
| .unwrap_or_default(); |
There was a problem hiding this comment.
To improve performance, especially with large environments, you can avoid deeply cloning the env JSON value from the Tera context. Instead of serde_json::from_value(v.clone()), you can iterate over the JSON object directly to build the BTreeMap. This avoids the potentially expensive clone operation.
let env_vars: BTreeMap<String, String> = ctx
.get("env")
.and_then(|v| v.as_object())
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();ac27600 to
6165e8e
Compare
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.5 x -- echo |
21.4 ± 1.1 | 20.3 | 36.9 | 1.00 |
mise x -- echo |
22.4 ± 0.7 | 20.9 | 27.0 | 1.05 ± 0.06 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.5 env |
20.8 ± 0.7 | 19.5 | 27.8 | 1.00 |
mise env |
21.5 ± 1.2 | 19.8 | 37.3 | 1.03 ± 0.07 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.5 hook-env |
21.6 ± 0.5 | 20.4 | 23.9 | 1.00 |
mise hook-env |
22.3 ± 1.3 | 20.7 | 41.9 | 1.03 ± 0.06 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.5 ls |
19.5 ± 0.5 | 18.4 | 22.1 | 1.00 |
mise ls |
20.2 ± 0.9 | 18.8 | 28.4 | 1.04 ± 0.05 |
xtasks/test/perf
| Command | mise-2026.2.5 | mise | Variance |
|---|---|---|---|
| install (cached) | 114ms | 114ms | +0% |
| ls (cached) | 71ms | 71ms | +0% |
| bin-paths (cached) | 77ms | 77ms | +0% |
| task-ls (cached) | 545ms | 543ms | +0% |
6165e8e to
51e45b2
Compare
51e45b2 to
51a8d3f
Compare
Add support for `$VAR`, `${VAR}`, and `${VAR:-default}` syntax in
`mise.toml` `[env]` values, gated by a 3-way `env_shell_expand` setting:
- `true`: enable shell expansion after Tera template rendering
- `false`: disable expansion, no warning
- unset (default): disable expansion but warn if `$` is detected
Uses the `shellexpand` crate with custom variable lookup from the
Tera context's env map. Expansion runs after Tera templates, so both
syntaxes can be mixed (e.g., `{{ env.FOO }}-$BAR`).
A debug_assert reminds to change the default to `true` in 2026.7.0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
51a8d3f to
e82ce59
Compare
### 🚀 Features - **(env)** add shell-style variable expansion in env values by @jdx in [#8029](#8029) - **(list)** add --all-sources flag to list command by @TylerHillery in [#8019](#8019) ### 🐛 Bug Fixes - **(gem)** Windows support for gem backend by @my1e5 in [#8031](#8031) - **(gem)** revert gem.rs script newline change by @my1e5 in [#8034](#8034) - **(lock)** write tools to lockfile matching their source config by @jdx in [#8012](#8012) - **(ls)** sort sources deterministically in --all-sources output by @jdx in [#8037](#8037) - **(task)** auto-install tools from mise.toml for file tasks by @jdx in [#8030](#8030) ### 📚 Documentation - fix wrong positions of `mise run` flags by @muzimuzhi in [#8036](#8036) ### 📦️ Dependency Updates - update ghcr.io/jdx/mise:copr docker digest to 3e00d7d by @renovate[bot] in [#8023](#8023) - update ghcr.io/jdx/mise:alpine docker digest to 0ced1b3 by @renovate[bot] in [#8022](#8022) ### 📦 Registry - add tirith ([github:sheeki03/tirith](https://github.com/sheeki03/tirith)) by @sheeki03 in [#8024](#8024) - add mas by @TyceHerrman in [#8032](#8032) ### Security - **(deps)** update time crate to 0.3.47 to fix RUSTSEC-2026-0009 by @jdx in [#8026](#8026) ### New Contributors - @sheeki03 made their first contribution in [#8024](#8024) - @TylerHillery made their first contribution in [#8019](#8019) ## 📦 Aqua Registry Updates #### New Packages (1) - [`kubernetes-sigs/kubectl-validate`](https://github.com/kubernetes-sigs/kubectl-validate) #### Updated Packages (6) - [`flux-iac/tofu-controller/tfctl`](https://github.com/flux-iac/tofu-controller/tfctl) - [`gogs/gogs`](https://github.com/gogs/gogs) - [`j178/prek`](https://github.com/j178/prek) - [`syncthing/syncthing`](https://github.com/syncthing/syncthing) - [`tuist/tuist`](https://github.com/tuist/tuist) - [`yaml/yamlscript`](https://github.com/yaml/yamlscript)
## Summary
- Adds support for `$VAR`, `${VAR}`, and `${VAR:-default}` syntax in
`mise.toml` `[env]` values
- Gated by a 3-way `env_shell_expand` setting (`true`/`false`/unset) to
allow gradual migration
- Uses the `shellexpand` crate with custom variable lookup from the Tera
context env map
- Expansion runs after Tera template rendering, so both syntaxes can be
mixed
### Behavior
| Setting | Behavior |
|---------|----------|
| `env_shell_expand = true` | Enable `$VAR`/`${VAR}`/`${VAR:-default}`
expansion |
| `env_shell_expand = false` | No expansion, no warning |
| unset (default) | No expansion, but warn if `$` detected in env values
|
A `debug_assert!` fires in 2026.7.x to remind to change the default to
`true`.
### Example
```toml
# settings
env_shell_expand = true
[env]
FOO = "hello"
BAR = "$FOO-world" # "hello-world"
BAZ = "${FOO}_suffix" # "hello_suffix"
QUX = "${UNDEF:-fallback}" # "fallback"
MIX = "{{ env.FOO }}-$FOO" # "hello-hello"
```
## Test plan
- [x] New e2e test `test_env_shell_expand` with 6 scenarios (basic,
brace, default, initial env, disabled, mixed Tera+shell)
- [x] Existing `test_env_template` passes (no regressions)
- [x] All 446 unit tests pass
- [x] Lint passes
- [x] No snapshot changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes environment variable rendering semantics and warning behavior,
which can affect downstream tooling if users opt in (or when defaults
change later). Uses a new parsing/expansion path (`shellexpand`) that
may introduce subtle edge cases around `$` handling and undefined
variables.
>
> **Overview**
> Adds an opt-in shell-style `$VAR`/`${VAR}`/`${VAR:-default}` expansion
step for `mise.toml` `[env]` values, executed *after* Tera rendering in
`EnvResults::parse_template`.
>
> Introduces a new tri-state `env_shell_expand` setting
(true/false/unset): `true` enables expansion via `shellexpand`, `false`
leaves `$` literals, and unset emits a one-time warning when `$` is
detected (with additional warnings for undefined vars lacking defaults).
Updates dependencies (`shellexpand` and related lockfile churn),
settings/schema definitions, documentation, and adds a new e2e test
covering enabled/disabled behavior, defaults, chaining, and tools-aware
ordering.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e82ce59. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
### 🚀 Features - **(env)** add shell-style variable expansion in env values by @jdx in [jdx#8029](jdx#8029) - **(list)** add --all-sources flag to list command by @TylerHillery in [jdx#8019](jdx#8019) ### 🐛 Bug Fixes - **(gem)** Windows support for gem backend by @my1e5 in [jdx#8031](jdx#8031) - **(gem)** revert gem.rs script newline change by @my1e5 in [jdx#8034](jdx#8034) - **(lock)** write tools to lockfile matching their source config by @jdx in [jdx#8012](jdx#8012) - **(ls)** sort sources deterministically in --all-sources output by @jdx in [jdx#8037](jdx#8037) - **(task)** auto-install tools from mise.toml for file tasks by @jdx in [jdx#8030](jdx#8030) ### 📚 Documentation - fix wrong positions of `mise run` flags by @muzimuzhi in [jdx#8036](jdx#8036) ### 📦️ Dependency Updates - update ghcr.io/jdx/mise:copr docker digest to 3e00d7d by @renovate[bot] in [jdx#8023](jdx#8023) - update ghcr.io/jdx/mise:alpine docker digest to 0ced1b3 by @renovate[bot] in [jdx#8022](jdx#8022) ### 📦 Registry - add tirith ([github:sheeki03/tirith](https://github.com/sheeki03/tirith)) by @sheeki03 in [jdx#8024](jdx#8024) - add mas by @TyceHerrman in [jdx#8032](jdx#8032) ### Security - **(deps)** update time crate to 0.3.47 to fix RUSTSEC-2026-0009 by @jdx in [jdx#8026](jdx#8026) ### New Contributors - @sheeki03 made their first contribution in [jdx#8024](jdx#8024) - @TylerHillery made their first contribution in [jdx#8019](jdx#8019) ## 📦 Aqua Registry Updates #### New Packages (1) - [`kubernetes-sigs/kubectl-validate`](https://github.com/kubernetes-sigs/kubectl-validate) #### Updated Packages (6) - [`flux-iac/tofu-controller/tfctl`](https://github.com/flux-iac/tofu-controller/tfctl) - [`gogs/gogs`](https://github.com/gogs/gogs) - [`j178/prek`](https://github.com/j178/prek) - [`syncthing/syncthing`](https://github.com/syncthing/syncthing) - [`tuist/tuist`](https://github.com/tuist/tuist) - [`yaml/yamlscript`](https://github.com/yaml/yamlscript)
Summary
$VAR,${VAR}, and${VAR:-default}syntax inmise.toml[env]valuesenv_shell_expandsetting (true/false/unset) to allow gradual migrationshellexpandcrate with custom variable lookup from the Tera context env mapBehavior
env_shell_expand = true$VAR/${VAR}/${VAR:-default}expansionenv_shell_expand = false$detected in env valuesA
debug_assert!fires in 2026.7.x to remind to change the default totrue.Example
Test plan
test_env_shell_expandwith 6 scenarios (basic, brace, default, initial env, disabled, mixed Tera+shell)test_env_templatepasses (no regressions)🤖 Generated with Claude Code
Note
Medium Risk
Changes environment variable rendering semantics and warning behavior, which can affect downstream tooling if users opt in (or when defaults change later). Uses a new parsing/expansion path (
shellexpand) that may introduce subtle edge cases around$handling and undefined variables.Overview
Adds an opt-in shell-style
$VAR/${VAR}/${VAR:-default}expansion step formise.toml[env]values, executed after Tera rendering inEnvResults::parse_template.Introduces a new tri-state
env_shell_expandsetting (true/false/unset):trueenables expansion viashellexpand,falseleaves$literals, and unset emits a one-time warning when$is detected (with additional warnings for undefined vars lacking defaults). Updates dependencies (shellexpandand related lockfile churn), settings/schema definitions, documentation, and adds a new e2e test covering enabled/disabled behavior, defaults, chaining, and tools-aware ordering.Written by Cursor Bugbot for commit e82ce59. This will update automatically on new commits. Configure here.