Skip to content

Add environment variables to CommandOptions - #44

Merged
matt-edmondson merged 1 commit into
mainfrom
feat/40-environment-variables
Aug 19, 2026
Merged

Add environment variables to CommandOptions#44
matt-edmondson merged 1 commit into
mainfrom
feat/40-environment-variables

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #40.

Stacked on #43 (feat/39-working-directory), which introduces CommandOptions. Review that one first; this PR's diff is against it.

Problem

ProcessStartInfo.Environment was never touched, so the child always inherited the host's environment verbatim with no way to add, override or remove a variable for a single call. Environment variables are the only control surface some tools expose, so this blocked behaviour with no flag equivalent:

  • GIT_TERMINAL_PROMPT=0 — without it a fetch/pull/push needing credentials blocks forever waiting on input that will never arrive, because output is redirected and no terminal is attached. The only mitigation left was a timeout, turning a clean "auth required" failure into an opaque hang-then-cancel.
  • GIT_ASKPASS / SSH_ASKPASS — the supported way to supply credentials to a subprocess without putting them on a command line where any process listing can read them.
  • LC_ALL=C — forces stable, machine-parseable output. Its absence is silent: parsing works on the developer's machine and fails only for users with a localized environment.

Approach

One new property on the CommandOptions record from #43 — no new overloads, which is what the options object was for:

public IReadOnlyDictionary<string, string?>? EnvironmentVariables { get; init; }

The entries are an overlay, not a replacement: a name not listed keeps whatever the caller had. A null value removes a variable, matching ProcessStartInfo.Environment semantics. Leaving the property null preserves today's behaviour exactly.

Elevation forces UseShellExecute, which has nowhere to carry an environment. Rather than let Process.Start fail with a message mentioning neither setting — or silently drop variables a caller may be relying on for credentials — combining the two throws ArgumentException up front.

Testing

  • ExecuteAsyncShouldSetAnEnvironmentVariableForTheChildProcess
  • ExecuteAsyncShouldOverrideAnInheritedEnvironmentVariable
  • ExecuteAsyncShouldRemoveAnInheritedEnvironmentVariableWhenTheValueIsNull — asserts the inherited value did not reach the child, since an unset variable prints differently in cmd and sh
  • ExecuteAsyncShouldInheritTheEnvironmentWhenNoVariablesAreGiven — pins that an unset overlay leaves previous behaviour untouched
  • ExecuteAsyncShouldRejectEnvironmentVariablesCombinedWithElevation

Each test uses a variable name derived from its own method name, since the host environment is process-wide and tests run in parallel.

Full suite: 32 passed, 2 skipped (elevated tests self-skip to avoid a UAC prompt). Builds clean across all eight target frameworks — ProcessStartInfo.Environment is available on netstandard2.0, so no conditional compilation was needed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LwKTWcmxe5mh6DsLRj5NGQ

ProcessStartInfo.Environment was never touched, so a child always
inherited the host's environment verbatim with no way to add, override
or remove a variable for a single call. That blocks a class of CLI
behaviour with no flag equivalent: GIT_TERMINAL_PROMPT=0, without which
a fetch needing credentials blocks forever on a prompt no terminal will
answer; GIT_ASKPASS and SSH_ASKPASS, the supported way to supply
credentials without exposing them in a process listing; and LC_ALL=C,
without which output parsing silently depends on the host locale.

The entries are an overlay rather than a replacement, so a name that is
not listed keeps whatever the caller had, and a null value removes a
variable, matching ProcessStartInfo.Environment semantics. Leaving the
property null preserves today's behaviour exactly.

Elevation requires UseShellExecute, which has nowhere to put an
environment, so combining the two throws rather than silently dropping
variables a caller may be relying on for credentials.

Fixes #40
@matt-edmondson
matt-edmondson force-pushed the feat/39-working-directory branch from b7b8478 to c99e717 Compare August 19, 2026 10:43
@matt-edmondson
matt-edmondson force-pushed the feat/40-environment-variables branch from f59f8a1 to ed58449 Compare August 19, 2026 10:43
@sonarqubecloud

Copy link
Copy Markdown

Base automatically changed from feat/39-working-directory to main August 19, 2026 10:58
@matt-edmondson
matt-edmondson merged commit e2be353 into main Aug 19, 2026
5 checks passed
@matt-edmondson
matt-edmondson deleted the feat/40-environment-variables branch August 19, 2026 10:59
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.

No way to set environment variables for the spawned process

1 participant