11//! Provides the execution context for a command, giving access to parsed arguments and flags.
22const std = @import ("std" );
3- const builtin = @import ("builtin" );
43const types = @import ("types.zig" );
54const command = @import ("command.zig" );
65const errors = @import ("errors.zig" );
76
8- /// Returns true on targets where POSIX environment variable lookup is available.
9- fn canLookupEnv () bool {
10- return switch (builtin .os .tag ) {
11- .windows , .wasi , .emscripten , .freestanding , .other = > false ,
12- else = > true ,
13- };
14- }
15-
167/// (Private) Describes the source of a value for error reporting.
178const ValueSource = enum {
189 parsed ,
@@ -76,10 +67,14 @@ pub const CommandContext = struct {
7667 /// Retrieves the value for a flag, searching parsed values, then environment
7768 /// variables, and finally falling back to the default value.
7869 ///
79- /// NOTE: If the value comes from an environment variable, the returned slice
80- /// borrows directly from the process environment block (valid for the
81- /// lifetime of the process). If you need to hold it past an environment
82- /// change, copy it using `app_allocator`.
70+ /// Memory model for env-var-sourced values: the raw string read from the
71+ /// environment is allocated on `tmp_allocator`. For flags of type
72+ /// `.String`, that allocation IS the returned value and stays alive until
73+ /// `tmp_allocator`'s arena is released (in the normal `execute` flow,
74+ /// this is when the `exec` function returns). For non-`.String` flags
75+ /// the raw string is freed immediately after it has been parsed into a
76+ /// primitive `FlagValue`. Tests that call `getFlag` directly should use
77+ /// an arena-backed `tmp_allocator` to avoid manual cleanup.
8378 pub fn getFlag (self : * const CommandContext , name : []const u8 , comptime T : type ) errors.Error ! T {
8479 // 1. Check for a parsed value from the command line.
8580 if (self .command .getFlagValue (name )) | parsed_value | {
@@ -91,14 +86,24 @@ pub const CommandContext = struct {
9186 std .debug .panic ("Attempted to access an undefined flag: '{s}'" , .{name });
9287
9388 // 3. Check for a value from an environment variable.
94- // getPosix is only available on POSIX targets (not Windows/WASI/freestanding).
95- if (comptime canLookupEnv ()) {
96- if (flag_def .env_var ) | env_name | {
97- const environ = std .Options .debug_threaded_io .? .environ .process_environ ;
98- if (std .process .Environ .getPosix (environ , env_name )) | env_val_str | {
99- const env_value = try types .parseValue (flag_def .type , env_val_str );
100- return castFlagValueTo (env_value , T , "flag" , name , .environment );
101- }
89+ // `Environ.getAlloc` is cross-platform (POSIX and Windows): it
90+ // returns an owned WTF-8 copy of the value or `EnvironmentVariableMissing`.
91+ if (flag_def .env_var ) | env_name | {
92+ const environ = std .Options .debug_threaded_io .? .environ .process_environ ;
93+ if (environ .getAlloc (self .tmp_allocator , env_name )) | env_val_str | {
94+ // For `.String` flags, env_val_str becomes the returned value
95+ // and must stay allocated for the caller. For other types,
96+ // `parseValue` copies into a primitive `FlagValue`, so we
97+ // release the source string eagerly — this keeps lookups
98+ // clean even when `tmp_allocator` is not an arena.
99+ const keep_alloc = flag_def .type == .String ;
100+ defer if (! keep_alloc ) self .tmp_allocator .free (env_val_str );
101+ const env_value = try types .parseValue (flag_def .type , env_val_str );
102+ return castFlagValueTo (env_value , T , "flag" , name , .environment );
103+ } else | err | switch (err ) {
104+ error .EnvironmentVariableMissing = > {}, // fall through to default
105+ error .OutOfMemory = > return error .OutOfMemory ,
106+ error .InvalidWtf8 = > return error .InvalidWtf8 ,
102107 }
103108 }
104109
@@ -340,17 +345,11 @@ test "context: getArgsAs for typed variadic" {
340345 try testing .expectError (error .InvalidCharacter , ctx_int .getArgsAs (i64 , "numbers" , allocator ));
341346}
342347
343- test "context: getFlag reads env var via debug_threaded_io environ" {
344- // Verify that the Environ.getPosix lookup through debug_threaded_io
345- // can find a real environment variable (PATH is always set on POSIX).
346- //
347- // `canLookupEnv()` in context.zig returns false on Windows/WASI/etc.,
348- // so the lookup is skipped and getFlag falls back to the default on
349- // those targets. This test only exercises the POSIX lookup path; the
350- // sibling tests above and below cover the default-fallback path that
351- // is what Windows runs through unconditionally.
352- if (comptime ! canLookupEnv ()) return ;
353-
348+ test "context: getFlag reads env var on all platforms via Environ.getAlloc" {
349+ // Regression: env-var lookup previously used the POSIX-only
350+ // `Environ.getPosix`, so Windows silently fell through to the default.
351+ // `Environ.getAlloc` works on both POSIX and Windows, and this test
352+ // covers both (PATH is set on both).
354353 const allocator = testing .allocator ;
355354 var cmd = try command .Command .init (allocator , .{ .name = "test" , .description = "" , .exec = dummyExec });
356355 defer cmd .deinit ();
@@ -363,14 +362,55 @@ test "context: getFlag reads env var via debug_threaded_io environ" {
363362 .description = "" ,
364363 });
365364
366- const ctx = CommandContext { .app_allocator = allocator , .tmp_allocator = allocator , .command = cmd , .data = null };
365+ // For `.String` flags sourced from an env var, the returned slice lives
366+ // on `tmp_allocator`. Back it with an arena here so the test does not
367+ // have to guess the origin of the slice it owns. This mirrors how the
368+ // normal `execute` flow wires up `tmp_allocator`.
369+ var arena = std .heap .ArenaAllocator .init (allocator );
370+ defer arena .deinit ();
371+ const ctx = CommandContext {
372+ .app_allocator = allocator ,
373+ .tmp_allocator = arena .allocator (),
374+ .command = cmd ,
375+ .data = null ,
376+ };
367377
368- // PATH is always set; getFlag should return its value, not the default
378+ // PATH is set on both POSIX and Windows; getFlag should return its
379+ // value, not the default.
369380 const path_value = try ctx .getFlag ("search-path" , []const u8 );
370381 try testing .expect (path_value .len > 0 );
371382 try testing .expect (! std .mem .eql (u8 , path_value , "fallback" ));
372383}
373384
385+ test "regression: env-var Int parse failure does not leak source string" {
386+ // For non-`.String` env-var flags, `getFlag` parses the raw string into
387+ // a primitive `FlagValue` and must then free the source. This test
388+ // forces a parse failure (PATH is not a valid integer) and relies on
389+ // `testing.allocator` to flag any leak.
390+ const allocator = testing .allocator ;
391+ var cmd = try command .Command .init (allocator , .{ .name = "test" , .description = "" , .exec = dummyExec });
392+ defer cmd .deinit ();
393+
394+ try cmd .addFlag (.{
395+ .name = "bad-number" ,
396+ .type = .Int ,
397+ .default_value = .{ .Int = 0 },
398+ .env_var = "PATH" , // always set, never a valid integer
399+ .description = "" ,
400+ });
401+
402+ const ctx = CommandContext {
403+ .app_allocator = allocator ,
404+ .tmp_allocator = allocator ,
405+ .command = cmd ,
406+ .data = null ,
407+ };
408+
409+ // Parse should fail with InvalidCharacter; the source string allocated
410+ // on `tmp_allocator` must be freed before the error propagates.
411+ try testing .expectError (error .InvalidCharacter , ctx .getFlag ("bad-number" , i64 ));
412+ }
413+
374414test "context: getFlag env var lookup returns default for unset var" {
375415 const allocator = testing .allocator ;
376416 var cmd = try command .Command .init (allocator , .{ .name = "test" , .description = "" , .exec = dummyExec });
0 commit comments