cli icon indicating copy to clipboard operation
cli copied to clipboard

Flag Action should be called when value is set by alternative sources.

Open project0 opened this issue 1 year ago • 5 comments

Checklist

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

What problem does this solve?

Fix action beeing called for all circumstances when a value is set by either a flag or alternative source. Currently the flag action is only executed when given as argument.

example:

		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:    flag_log_level,
				Usage:   "The log level (trace, debug, info, warn, error, fatal, panic)",
				Value:   "info",
				Sources: cli.EnvVars(flagEnv(flag_log_level)),
				Action: func(ctx context.Context, cmd *cli.Command, value string) error {
					log.Info("Setting log level to ", value)
					level, err := log.ParseLevel(value)
					if err != nil {
						return err
					}
					log.SetLevel(level)
					return nil
				},
			},

Solution description

When a env var is set, the action should be called.

Describe alternatives you've considered

not using in this case as it is has no consistent experience for users.

project0 avatar Jan 06 '25 16:01 project0

@project0 Can you set Local: true for cli.StringFlag and try ?

dearchap avatar Jan 12 '25 15:01 dearchap

@dearchap indeed, setting Local true populates the value. Is this the intended behavior? If so, documenting it would be beneficial in this case.

project0 avatar Jan 12 '25 16:01 project0

The problem is that by default we arent running FlagAction for "non local" flags. Flags which are "non local" are available to all subcommands as well and there was a discussion as to how flag actions should be run for these kinds of flags. Local means that the flag is local only to current command and will have its flag actions run. Let me think how to proceed with this.

cc @urfave/cli

dearchap avatar Jan 12 '25 16:01 dearchap

In this case suppose your flag is "non local" you could invoke it something like this

root cmd subcmd --log-level debug

we can run action for all flags(including log level ) for subcmd

but say you invoke it like this

LOG_LEVEL=debug root cmd subcmd

the log-level flag is set from environment when should the flag action be run ? or for example

LOG_LEVEL=debug root cmd --log-level info subcmd

It starts getting complicated.

dearchap avatar Jan 12 '25 23:01 dearchap

I've run into this same situation. This seems to be an undocumented breaking change in behaviour between v2 and v3:

  • github.com/urfave/cli/v2

    Click to expand code
    package main
    
    import (
    	"fmt"
    	"log"
    	"os"
    
    	"github.com/urfave/cli/v2"
    )
    
    func main() {
    	app := cli.NewApp()
    	app.Commands = []*cli.Command{
    		{
    			Name: "serve",
    			Flags: []cli.Flag{
    				&cli.StringFlag{
    					Name:     "custom-var",
    					EnvVars:  []string{"CUSTOM_VAR"},
    					Required: false,
    					Action: func(_ *cli.Context, s string) error {
    						fmt.Println("CUSTOM_VAR Action")
    						return nil
    					},
    				},
    			},
    			Action: func(ctx *cli.Context) error {
    				fmt.Println("Serve")
    				return nil
    			},
    		},
    	}
    
    	if err := app.Run(os.Args); err != nil {
    		log.Fatal(err)
    	}
    }
    

    Output:

    go run . serve
    CUSTOM_VAR Action
    Serve
    
  • github.com/urfave/cli/v3

    Click to expand code
    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"os"
    
    	"github.com/urfave/cli/v3"
    )
    
    func main() {
    	app := &cli.Command{
    		Name: "my app",
    		Commands: []*cli.Command{
    			{
    				Name: "serve",
    				Flags: []cli.Flag{
    					&cli.StringFlag{
    						Name:     "custom-var",
    						Sources:  cli.EnvVars("CUSTOM_VAR"),
    						Required: false,
    						Action: func(_ context.Context, _ *cli.Command, s string) error {
    							fmt.Println("CUSTOM_VAR Action")
    							return nil
    						},
    					},
    				},
    				Action: func(_ context.Context, cmd *cli.Command) error {
    					fmt.Println("Serve")
    					return nil
    				},
    			},
    		},
    	}
    
    	if err := app.Run(context.Background(), os.Args); err != nil {
    		log.Fatal(err)
    	}
    }
    

    Output:

    go run . serve
    Serve
    

I've also tried adding Local: true to &cli.StringFlag, but this doesn't change the behaviour:

Click to expand code
package main

import (
	"context"
	"fmt"
	"log"
	"os"

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

func main() {
	app := &cli.Command{
		Name: "my app",
		Commands: []*cli.Command{
			{
				Name: "serve",
				Flags: []cli.Flag{
					&cli.StringFlag{
						Name:     "custom-var",
						Sources:  cli.EnvVars("CUSTOM_VAR"),
						Local: true,
						Required: false,
						Action: func(_ context.Context, _ *cli.Command, s string) error {
							fmt.Println("CUSTOM_VAR Action")
							return nil
						},
					},
				},
				Action: func(_ context.Context, cmd *cli.Command) error {
					fmt.Println("Serve")
					return nil
				},
			},
		},
	}

	if err := app.Run(context.Background(), os.Args); err != nil {
		log.Fatal(err)
	}
}

Output:

go run . serve
Serve

I have the following questions:

  1. Is there a reason this change of behaviour isn't documented in the v2-to-v3 migration guide?
  2. It was mentioned here that a workaround is to set Local: true, why doesn't it work in the above example?

Thanks!

adamcohen2 avatar Apr 17 '25 04:04 adamcohen2