Skip to content

--flag="" rejected as missing argument instead of setting empty string #2293

@idelchi

Description

@idelchi

My urfave/cli version is

v3.7.0 (also reproduced on latest main at commit 4dc6aa6)

Checklist

  • Are you running the latest v3 release? The list of releases is here.
  • Did you check the manual for your release? The v3 manual is here
  • Did you perform a search about this problem? Here's the GitHub guide about searching.

Dependency Management

  • My project is using go modules.
  • My project is using vendoring.

Describe the bug

--flag= (explicit empty value via =) is rejected with "flag needs an argument" instead of setting the flag to an empty string.

This is a side effect of the fix for #2223 in commit 2c77d6e. That fix correctly introduced valFromEqual to prevent --flag= from consuming the next argument. However, the error condition ended up slightly too broad (command_parse.go:167):

if flagVal == "" {
    if len(rargs) == 1 || valFromEqual {
        return ..., fmt.Errorf("%s%s", argumentNotProvidedErrMsg, firstArg)
    }
    flagVal = rargs[1]
    ...
}

When valFromEqual is true and flagVal is "", the code enters the outer if (because flagVal == "") and then hits the error (because valFromEqual is true). But valFromEqual == true with flagVal == "" means the user wrote --flag= — they explicitly provided an empty value. This shouldn't be an error.

The original #2223 bug (consuming the next arg) is fully prevented by simply not entering the outer block when valFromEqual is true, since there's nothing to fetch:

if flagVal == "" && !valFromEqual {
    if len(rargs) == 1 {
        return ..., fmt.Errorf("%s%s", argumentNotProvidedErrMsg, firstArg)
    }
    flagVal = rargs[1]
    ...
}

When valFromEqual is true, flagVal already holds the correct value (empty string), so the block is skipped entirely. The #2223 fix is preserved — no arg consumption happens.

Input Before #2223 After #2223 With this fix
--a= --b=bar a="--b=bar" (bug) error a="", b="bar"
--a= a="" error a=""
--a=foo a="foo" a="foo" a="foo"
--a (last arg) error error error

To reproduce

package main

import (
    "context"
    "fmt"
    "os"

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

func main() {
    var name string

    app := &cli.Command{
        Name: "app",
        Flags: []cli.Flag{
            &cli.StringFlag{
                Name:        "name",
                Destination: &name,
            },
        },
        Action: func(_ context.Context, cmd *cli.Command) error {
            fmt.Printf("name: %q\n", cmd.String("name"))
            fmt.Printf("args: %q\n", cmd.Args().Slice())
            return nil
        },
    }

    if err := app.Run(context.Background(), os.Args); err != nil {
        fmt.Fprintf(os.Stderr, "error: %v\n", err)
        os.Exit(1)
    }
}
go run . --name=

Observed behavior

error: flag needs an argument: --name=

Expected behavior

name: ""
args: []

The flag should be set to an empty string. --flag= is standard POSIX/GNU syntax for explicitly setting a flag to empty. Go's standard flag package, cobra, and getopt all accept it.

Additional context

Test against latest main (4dc6aa6):

func TestFlagEqualsEmptyValue(t *testing.T) {
    t.Run("--flag= sets empty string", func(t *testing.T) {
        var val string

        cmd := &Command{
            Flags: []Flag{
                &StringFlag{
                    Name:        "name",
                    Destination: &val,
                },
            },
            Action: func(_ context.Context, cmd *Command) error {
                val = cmd.String("name")
                return nil
            },
        }

        err := cmd.Run(buildTestContext(t), []string{"app", "--name="})
        assert.NoError(t, err)
        assert.Equal(t, "", val)
    })

    t.Run("--flag= does not consume next positional arg", func(t *testing.T) {
        var val string
        var args []string

        cmd := &Command{
            Flags: []Flag{
                &StringFlag{
                    Name:        "name",
                    Destination: &val,
                },
            },
            Action: func(_ context.Context, cmd *Command) error {
                val = cmd.String("name")
                args = cmd.Args().Slice()
                return nil
            },
        }

        err := cmd.Run(buildTestContext(t), []string{"app", "--name=", "positional"})
        assert.NoError(t, err)
        assert.Equal(t, "", val)
        assert.Equal(t, []string{"positional"}, args)
    })
}
--- FAIL: TestFlagEqualsEmptyValue/--flag=_sets_empty_string (0.00s)
--- FAIL: TestFlagEqualsEmptyValue/--flag=_does_not_consume_next_positional_arg (0.00s)

Note: two existing test cases in TestFlagAction expect --f_string= to error with "flag needs an argument". With the fix, the empty string is accepted and passed to the flag's action validator, which returns "empty string". These expectations need updating:

// command_test.go
{
    name: "flag_string_error",
    args: []string{"app", "--f_string="},
    err:  "flag needs an argument: --f_string=",  // → "empty string"
},
{
    name: "flag_string_error2",
    args: []string{"app", "--f_string=", "--f_bool"},
    err:  "flag needs an argument: --f_string=",  // → "empty string"
},

Want to fix this yourself?

Yes. The fix is a two-line change in command_parse.go that tightens the condition introduced in #2223 without reverting it, plus the test expectation updates above. I have a working patch ready.

Run go version and paste its output here

go version go1.26.1 linux/amd64

Run go env and paste its output here

n/a — bug is in urfave/cli internals, not environment-specific

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/v3relates to / is being considered for v3kind/bugdescribes or fixes a bugstatus/triagemaintainers still need to look into this

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions