Skip to content

feat(env): add shell-style variable expansion in env values#8029

Merged
jdx merged 1 commit intomainfrom
feat/env-shell-expand
Feb 6, 2026
Merged

feat(env): add shell-style variable expansion in env values#8029
jdx merged 1 commit intomainfrom
feat/env-shell-expand

Conversation

@jdx
Copy link
Owner

@jdx jdx commented Feb 6, 2026

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

# 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

  • New e2e test test_env_shell_expand with 6 scenarios (basic, brace, default, initial env, disabled, mixed Tera+shell)
  • Existing test_env_template passes (no regressions)
  • All 446 unit tests pass
  • Lint passes
  • No snapshot changes

🤖 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 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.

Written by Cursor Bugbot for commit e82ce59. This will update automatically on new commits. Configure here.

Copilot AI review requested due to automatic review settings February 6, 2026 12:51
@socket-security
Copy link

socket-security bot commented Feb 6, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​shellexpand@​3.1.19910093100100

View full report

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @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 mise.toml by introducing shell-style variable expansion. Users can now define environment variables that dynamically reference other variables or provide fallback values, similar to how shell scripts handle variables. This feature is introduced with a controlled rollout mechanism, allowing users to opt-in or out, and is designed to work seamlessly alongside existing Tera templating capabilities.

Highlights

  • Shell-style Variable Expansion: Introduces support for shell-style variable expansion (e.g., '$VAR', '${VAR}', and '${VAR:-default}') within [env] values in mise.toml configuration files.
  • Configurable Expansion Behavior: A new env_shell_expand setting is added, allowing users to explicitly enable (true), disable (false), or use the default (unset) behavior. The default currently disables expansion but warns if $ is detected, with a plan to enable it by default in a future release.
  • Integration with Tera Templating: The shell expansion process runs after Tera template rendering, enabling the use of both templating syntaxes for dynamic environment variable definitions.
  • New Dependency: The shellexpand Rust crate has been integrated to handle the variable expansion logic.
Changelog
  • Cargo.lock
    • Updated windows-sys from 0.59.0 to 0.61.2 (and 0.60.2 in one instance).
    • Updated socket2 from 0.5.10 to 0.6.2.
    • Updated base64 from 0.21.7 to 0.22.1.
    • Added dirs version 6.0.0 as a new dependency.
    • Added shellexpand version 3.1.1 as a new dependency.
  • Cargo.toml
    • Added shellexpand = "3" to the dependencies.
  • e2e/env/test_env_shell_expand
    • Added a new end-to-end test file to verify various scenarios of shell-style environment variable expansion, including basic $VAR, brace ${VAR}, default value ${VAR:-default}, expansion from initial environment, disabled expansion, and mixed Tera + shell expansion.
  • schema/mise.json
    • Added a new boolean property env_shell_expand to the schema with a description for enabling shell-style variable expansion.
  • settings.toml
    • Added a new setting env_shell_expand with detailed documentation explaining its purpose, behavior (true/false/unset), and examples of its usage in mise.toml.
  • src/config/env_directive/mod.rs
    • Imported Cow and BTreeMap for use in environment variable handling.
    • Modified the render_value function to implement a two-step expansion process: first Tera template rendering, then shell-style variable expansion.
    • Integrated the shellexpand crate to perform the shell-style variable expansion.
    • Implemented logic to respect the env_shell_expand setting, including a warning for unset values when $ is detected, and a debug_assert! to remind about changing the default in a future release.
Activity
  • The pull request introduces a new feature to add shell-style variable expansion in environment values.
  • The author, jdx, provided a comprehensive summary, behavior description, example, and test plan in the PR body.
  • New end-to-end tests were added to cover various scenarios of the new shell expansion feature.
  • All existing unit tests passed, indicating no regressions.
  • Lint checks passed, and no snapshot changes were observed.
  • The PR description indicates it was generated with Claude Code.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 shellexpand crate with custom variable lookup from the Tera context
  • Adds 3-way env_shell_expand setting (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.

.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()))
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)
}

Copilot uses AI. Check for mistakes.
(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`).
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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`).

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +691 to +694
let env_vars: BTreeMap<String, String> = ctx
.get("env")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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();

@jdx jdx force-pushed the feat/env-shell-expand branch from ac27600 to 6165e8e Compare February 6, 2026 12:58
@github-actions
Copy link

github-actions bot commented Feb 6, 2026

Hyperfine Performance

mise x -- echo

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%

@jdx jdx force-pushed the feat/env-shell-expand branch from 6165e8e to 51e45b2 Compare February 6, 2026 13:21
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but a Cloud Agent failed to start.

@jdx jdx force-pushed the feat/env-shell-expand branch from 51e45b2 to 51a8d3f Compare February 6, 2026 13:38
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>
@jdx jdx force-pushed the feat/env-shell-expand branch from 51a8d3f to e82ce59 Compare February 6, 2026 13:39
@jdx jdx enabled auto-merge (squash) February 6, 2026 13:47
@jdx jdx merged commit aff23b1 into main Feb 6, 2026
36 checks passed
@jdx jdx deleted the feat/env-shell-expand branch February 6, 2026 13:53
mise-en-dev added a commit that referenced this pull request Feb 7, 2026
### 🚀 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)
german-molins added a commit to german-molins/dotfiles that referenced this pull request Feb 11, 2026
lucasew pushed a commit to lucasew/CONTRIB-mise that referenced this pull request Feb 18, 2026
## 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>
lucasew pushed a commit to lucasew/CONTRIB-mise that referenced this pull request Feb 18, 2026
### 🚀 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants