Skip to content

Comments

fix(#1661): expand env vars for non-Unix shells (PowerShell/cmd)#1666

Merged
yottahmd merged 7 commits intomainfrom
bugfix-command-exec
Feb 13, 2026
Merged

fix(#1661): expand env vars for non-Unix shells (PowerShell/cmd)#1666
yottahmd merged 7 commits intomainfrom
bugfix-command-exec

Conversation

@yottahmd
Copy link
Collaborator

@yottahmd yottahmd commented Feb 13, 2026

Fixes #1661

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved variable expansion handling for edge cases involving single quotes and adjacent characters
    • Enhanced consistency in variable substitution across different shell contexts
  • Tests

    • Added comprehensive test coverage for variable expansion edge cases, including variables followed by single quotes and missing variable scenarios

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors variable substitution logic across multiple evaluation modules from regex-based callbacks to manual index-based parsing, introduces a single-quoted variable detection helper, and centralizes shell-aware evaluation options for improved cross-platform command interpolation support.

Changes

Cohort / File(s) Summary
Variable Substitution Refactoring
internal/cmn/eval/envscope.go, internal/cmn/eval/resolve.go, internal/cmn/eval/expand.go
Replaces regex-based expansion (ReplaceAllStringFunc) with manual index-based parsing using FindAllStringSubmatchIndex. Introduces isSingleQuotedVar helper to detect and preserve single-quoted variables (e.g., '${VAR}' or '$VAR'). Uses strings.Builder for output reconstruction, explicitly handles group-based key extraction for ${VAR} vs $VAR forms, and preserves unknown variables and dotted-path references.
Variable Substitution Tests
internal/cmn/eval/envscope_test.go, internal/cmn/eval/expand_test.go, internal/cmn/eval/resolve_test.go, internal/cmn/eval/pipeline_test.go
Adds comprehensive test coverage for variable expansion with quote adjacency, including variables followed by single quotes, missing variables, and command-like strings with single-quoted references. Tests verify that single-quoted variables are preserved and braced/simple forms expand correctly.
Shell-Aware Evaluation Options
internal/runtime/builtin/command/command.go, internal/runtime/builtin/command/eval_options_test.go
Introduces commandEvalOptions helper function to centralize shell-detection logic for evaluation options. Delegates conditional logic from init to dedicated function that returns OS expansion options for no-shell/direct mode, and conditionally applies WithoutDollarEscape and WithoutExpandEnv for Unix-like or nix shells. Adds new test file with TestCommandExecutor_GetEvalOptions to validate expansion behavior across different shell types (sh, PowerShell, pwsh).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #1632: Directly relates to the same variable-substitution infrastructure in envscope, resolve, and expand modules with identical refactoring patterns (index-based parsing, isSingleQuotedVar helper, replaceVars rewrite).
🚥 Pre-merge checks | ✅ 4 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (58 files):

⚔️ api/v1/api.gen.go (content)
⚔️ api/v1/api.yaml (content)
⚔️ internal/agent/api.go (content)
⚔️ internal/agent/bash.go (content)
⚔️ internal/agent/bash_test.go (content)
⚔️ internal/agent/hooks.go (content)
⚔️ internal/agent/hooks_test.go (content)
⚔️ internal/agent/loop.go (content)
⚔️ internal/agent/loop_test.go (content)
⚔️ internal/agent/navigate.go (content)
⚔️ internal/agent/navigate_test.go (content)
⚔️ internal/agent/patch.go (content)
⚔️ internal/agent/patch_test.go (content)
⚔️ internal/agent/session.go (content)
⚔️ internal/agent/system_prompt.go (content)
⚔️ internal/agent/system_prompt.txt (content)
⚔️ internal/agent/system_prompt_test.go (content)
⚔️ internal/agent/types.go (content)
⚔️ internal/auth/role.go (content)
⚔️ internal/auth/role_test.go (content)
⚔️ internal/auth/user_test.go (content)
⚔️ internal/cmn/config/config.go (content)
⚔️ internal/cmn/config/config_test.go (content)
⚔️ internal/cmn/eval/envscope.go (content)
⚔️ internal/cmn/eval/envscope_test.go (content)
⚔️ internal/cmn/eval/expand.go (content)
⚔️ internal/cmn/eval/expand_test.go (content)
⚔️ internal/cmn/eval/pipeline_test.go (content)
⚔️ internal/cmn/eval/resolve.go (content)
⚔️ internal/cmn/eval/resolve_test.go (content)
⚔️ internal/runtime/builtin/command/command.go (content)
⚔️ internal/service/frontend/api/v1/api.go (content)
⚔️ internal/service/frontend/api/v1/audit.go (content)
⚔️ internal/service/frontend/api/v1/export_test.go (content)
⚔️ internal/service/frontend/api/v1/resources.go (content)
⚔️ internal/service/frontend/api/v1/services.go (content)
⚔️ internal/service/frontend/api/v1/webhooks.go (content)
⚔️ internal/service/frontend/api/v1/webhooks_test.go (content)
⚔️ internal/service/frontend/api/v1/workers.go (content)
⚔️ internal/service/frontend/auth/builtin.go (content)
⚔️ internal/service/frontend/auth/middleware_test.go (content)
⚔️ internal/service/oidcprovision/rolemapper.go (content)
⚔️ internal/service/oidcprovision/rolemapper_test.go (content)
⚔️ internal/service/resource/service.go (content)
⚔️ internal/service/resource/store.go (content)
⚔️ internal/service/resource/store_test.go (content)
⚔️ ui/src/App.tsx (content)
⚔️ ui/src/api/v1/schema.ts (content)
⚔️ ui/src/components/ProtectedRoute.tsx (content)
⚔️ ui/src/contexts/AuthContext.tsx (content)
⚔️ ui/src/features/dashboard/components/MiniResourceChart.tsx (content)
⚔️ ui/src/features/system-status/components/ResourceChart.tsx (content)
⚔️ ui/src/menu.tsx (content)
⚔️ ui/src/pages/api-keys/APIKeyFormModal.tsx (content)
⚔️ ui/src/pages/audit-logs/index.tsx (content)
⚔️ ui/src/pages/system-status/index.tsx (content)
⚔️ ui/src/pages/users/UserFormModal.tsx (content)
⚔️ ui/src/pages/webhooks/index.tsx (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing environment variable expansion for non-Unix shells (PowerShell/cmd) on Windows, which directly addresses issue #1661.
Linked Issues check ✅ Passed The code changes implement Option A from issue #1661 by making GetEvalOptions shell-aware, enabling expansion for PowerShell/cmd/nu while disabling it only for Unix shells (sh/bash/zsh) that natively support ${VAR} syntax.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the Windows shell interpolation issue: refactored variable expansion logic in resolve/envscope modules, added comprehensive tests for quote adjacency and shell context handling, and made GetEvalOptions shell-aware as proposed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix-command-exec

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@yottahmd
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cmn/eval/resolve.go (1)

114-124: ⚠️ Potential issue | 🟡 Minor

Remove extractVarKey or move it to a test helper file.

The function is not called by replaceVars (which extracts keys directly from submatch indices) or any production code. It exists only in resolve_test.go, making it dead code in production. Either remove it or relocate it to a test utilities file if it serves as a reusable test helper.

🤖 Fix all issues with AI agents
In `@internal/runtime/builtin/command/command.go`:
- Around line 362-377: The commandEvalOptions logic is correct but lacks an
explicit comment explaining the intentional fish exception; update the comment
above commandEvalOptions (and/or near IsUnixLikeShell/IsNixShell references) to
state that fish intentionally returns false from IsUnixLikeShell so Dagu
performs ${VAR} expansion for fish (due to -e flag incompatibility), and mention
that PowerShell/cmd are handled by leaving ExpandEnv=true; leave the
implementation of commandEvalOptions, eval.WithoutDollarEscape(),
eval.WithoutExpandEnv(), cmdutil.IsUnixLikeShell, and cmdutil.IsNixShell
unchanged.
🧹 Nitpick comments (3)
internal/cmn/eval/resolve.go (1)

126-130: isSingleQuotedVar uses a heuristic that may misidentify nested quote contexts.

The check only looks at the immediately adjacent characters (input[start-1] and input[end]), so patterns like 'foo'${BAR}'baz' would incorrectly treat ${BAR} as single-quoted (the ' before it is actually closing a prior quoted segment). This is acceptable for the targeted use cases (nu shell $'...' syntax, simple quoting), but worth documenting.

internal/runtime/builtin/command/eval_options_test.go (1)

28-89: Good integration test covering the core shell types.

The test validates the full flow through step.EvalOptions(ctx) → registered GetEvalOptionscommandEvalOptions, which is more robust than unit-testing the helper alone.

Consider adding a case for an empty/unset shell to verify the fallback path (len(shell) == 0 in commandEvalOptions), and for other shells like bash, zsh, or nu for completeness.

internal/cmn/eval/envscope.go (1)

172-177: Add defensive guard to handle potential future regex changes safely.

The current regex \$\{([^}]+)\}|\$([a-zA-Z0-9_][a-zA-Z0-9_]*) guarantees that exactly one of the two capture groups will match, so both loc[2] and loc[4] cannot be negative simultaneously. However, adding an explicit check before accessing loc[4] provides defensive protection against future regex modifications that might break this invariant.

Optional defensive guard
 		var key string
 		if loc[2] >= 0 { // Group 1: ${...}
 			key = s[loc[2]:loc[3]]
-		} else { // Group 2: $VAR
+		} else if loc[4] >= 0 { // Group 2: $VAR
 			key = s[loc[4]:loc[5]]
+		} else {
+			// Neither group captured — preserve original text.
+			b.WriteString(match)
+			continue
 		}

@yottahmd yottahmd merged commit 99b4999 into main Feb 13, 2026
5 checks passed
@yottahmd yottahmd deleted the bugfix-command-exec branch February 13, 2026 18:41
@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 77.77778% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.84%. Comparing base (d496dde) to head (d83152b).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
internal/cmn/eval/resolve.go 68.96% 2 Missing and 7 partials ⚠️
internal/cmn/eval/envscope.go 84.00% 2 Missing and 2 partials ⚠️
internal/runtime/builtin/command/command.go 87.50% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1666      +/-   ##
==========================================
- Coverage   70.18%   69.84%   -0.35%     
==========================================
  Files         345      345              
  Lines       38664    38817     +153     
==========================================
- Hits        27135    27110      -25     
- Misses       9361     9398      +37     
- Partials     2168     2309     +141     
Files with missing lines Coverage Δ
internal/cmn/eval/expand.go 97.29% <100.00%> (ø)
internal/runtime/builtin/command/command.go 93.10% <87.50%> (-0.40%) ⬇️
internal/cmn/eval/envscope.go 87.32% <84.00%> (-11.04%) ⬇️
internal/cmn/eval/resolve.go 81.05% <68.96%> (-16.32%) ⬇️

... and 25 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d496dde...d83152b. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

${VAR} Command Interpolation Broken on Windows

1 participant