cli icon indicating copy to clipboard operation
cli copied to clipboard

Environment variables not read when flag is defined on both root and subcommand

Open peterdeme opened this issue 2 months ago • 4 comments

My urfave/cli version is

v3.5.0 (latest as of 2025-10-30)

Checklist

  • Are you running the latest v3 release? The list of releases is https://github.com/urfave/cli/releases. ✅
  • Did you check the manual for your release? The v3 manual is https://cli.urfave.org/v3/getting-started/ ✅
  • Did you perform a search about this problem? Here's the https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests about searching. ✅

Dependency Management

My project is using go modules.

Describe the bug

When the same flag (same flag object instance) is defined in both a root command's Flags array and a subcommand's Flags array, environment variables specified via Sources: cli.EnvVars() are not read correctly when executing the subcommand. However, CLI flag values passed via command-line arguments work fine.

This appears to be a regression or unintended behavior in v3, as the flag duplication pattern was common in v2 for creating "global" flags.

Minimal reproduction code

  package main

  import (
        "context"
        "fmt"
        "os"

        "github.com/urfave/cli/v3"
  )

  func main() {
        // Define flag once
        flagTest := &cli.StringFlag{
                Name:     "test-flag",
                Sources:  cli.EnvVars("TEST_FLAG"),
                Usage:    "Test flag",
                Required: true,
        }

        // Subcommand that uses the flag
        subCmd := &cli.Command{
                Name: "subcmd",
                Flags: []cli.Flag{
                        flagTest,  // Flag on subcommand
                },
                Action: func(ctx context.Context, cmd *cli.Command) error {
                        value := cmd.String(flagTest.Name)
                        fmt.Printf("Subcommand - Flag value: '%s'\n", value)
                        return nil
                },
        }

        // Root command that ALSO has the same flag
        rootCmd := &cli.Command{
                Flags: []cli.Flag{
                        flagTest,  // SAME flag on root
                },
                Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
                        value := cmd.String(flagTest.Name)
                        fmt.Printf("Root Before - Flag value: '%s'\n", value)
                        return ctx, nil
                },
                Commands: []*cli.Command{
                        subCmd,
                },
        }

        if err := rootCmd.Run(context.Background(), os.Args); err != nil {
                fmt.Fprintf(os.Stderr, "Error: %v\n", err)
                os.Exit(1)
        }
  }

Steps to reproduce

  1. Save the code above as main.go
  2. Run with environment variable set: TEST_FLAG="my-value" go run main.go subcmd

Observed behavior

Root Before - Flag value: '' Subcommand - Flag value: ''

The environment variable TEST_FLAG is not read, and the flag value is empty.

However, if you pass the flag via CLI: TEST_FLAG="my-value" go run main.go subcmd --test-flag="cli-value"

Output: Root Before - Flag value: 'cli-value' Subcommand - Flag value: 'cli-value'

The CLI flag does work correctly.

Expected behavior

The environment variable should be read and the flag value should be:

Root Before - Flag value: 'my-value' Subcommand - Flag value: 'my-value'

This is the behavior when the flag is not duplicated on the root command (i.e., when flagTest is only in the subcommand's Flags array).

Additional context

Workaround: Remove the flag from the root command's Flags array if it's only used by subcommands. Only include flags on the root that are actually used by the root's Before/After hooks.

This issue was discovered while migrating a large codebase from v2 to v3. In v2, it was common to define flags on both root and subcommands to make them "globally available." The migration guide doesn't mention this breaking change.

Impact

This could affect many projects migrating from v2 to v3, as the duplication pattern was used for global flags. The issue is subtle because:

  1. CLI flags still work (masking the problem during development)
  2. Environment variables silently fail (causing issues in production/CI)

Questions

  • Is this intended behavior?
  • Should the migration guide warn about this?
  • Is there a recommended pattern for "global" flags in v3 that work with both CLI args and env vars?

Run go version and paste its output here

go version go1.24.6 darwin/arm64

Run go env and paste its output here

GO111MODULE='on' GOARCH='arm64' GOOS='darwin' GOVERSION='go1.24.6'

peterdeme avatar Oct 30 '25 11:10 peterdeme

Might be related to https://github.com/urfave/cli/issues/2132#issuecomment-3220922073

peterdeme avatar Oct 30 '25 11:10 peterdeme

@peterdeme By default in v3 all flags are global(persistent) unless explicitly set to non-persistent. So in your example you wouldnt need to define the flag multiple times. You can define it at the root and all subcommands would parse that flag automatically .

dearchap avatar Oct 30 '25 14:10 dearchap

@dearchap thanks, I missed this. Where is this in the migration guide?

Secondly, why does this make the value of the env var an empty string? I understand that there is auto inheritance but duplication should be still fine, imo.

peterdeme avatar Oct 30 '25 14:10 peterdeme

@peterdeme Dont think its in the migration guide. I'll update it. Duplication should be fine but I dont think we have tested with both being defined as "global". We have test cases for a flag being defined globally and the same flag name being defined in a subcommand as "local"(non-persistent) and that case is handled.

dearchap avatar Nov 01 '25 22:11 dearchap