cli: Global flag not accessible with many levels of sub-commands

Hello community,

My aim is to have some global flags that can be inserted anywhere. The code below registers the admin command which has a sub-command service which itself has another sub-command called status. Debug flag is global. However, when I type the following command, debug flag is not activated

$ ./binary admin -d service status
status: => local (false), global (false)

The code:

package main

import (
	"fmt"
	"os"
	"strconv"

	"github.com/urfave/cli"
)

func main() {

	globalFlags := []cli.Flag{
		cli.BoolFlag{Name: "debug, d", Usage: "Run in debug mode"},
	}

	adminServiceStatusCmd := cli.Command{
		Name:  "status",
		Flags: append([]cli.Flag{}, globalFlags...),
		Action: func(c *cli.Context) {
			global := strconv.FormatBool(c.GlobalBool("debug"))
			local := strconv.FormatBool(c.Bool("debug"))
			fmt.Printf("%s: => local (%s), global (%s)\n", c.Command.Name, local, global)
		},
	}

	adminServiceCmd := cli.Command{
		Name:        "service",
		Flags:       append([]cli.Flag{}, globalFlags...),
		Subcommands: []cli.Command{adminServiceStatusCmd},
	}

	adminCmd := cli.Command{
		Name:        "admin",
		Flags:       append([]cli.Flag{}, globalFlags...),
		Subcommands: []cli.Command{adminServiceCmd},
	}

	app := cli.NewApp()
	app.Name = "lookup"
	app.Flags = append([]cli.Flag{}, globalFlags...)
	app.Commands = []cli.Command{adminCmd}

	app.Run(os.Args)
}

Is this a bug ? or am I just misunderstanding the global flag concept ?

Thanks,

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 10
  • Comments: 15 (7 by maintainers)

Most upvoted comments

Closing this as it has become stale.

The example above is probably still true! I would be very much in favor of someone creating a PR to make this better 🙏

This behaviour is not present in v2

package main

import (
	"fmt"
	"log"
	"os"

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

func main() {
	app := cli.NewApp()
	app.Name = "myprogramname"
	app.Action = func(c *cli.Context) error {
		fmt.Println("c.App.Name for app.Action is", c.App.Name)
		return nil
	}
	app.Flags = []cli.Flag{
		&cli.Int64Flag{
			Name:  "myi",
			Value: 10,
		},
	}
	app.Commands = []*cli.Command{
		{
			Name: "foo",
			Action: func(c *cli.Context) error {
				fmt.Println("c.App.Name for app.Commands.Action is", c.App.Name)
				return nil
			},
			Subcommands: []*cli.Command{
				{
					Name: "bar",
					/*Before: func(c *cli.Context) error {
						return fmt.Errorf("before error")
					},*/
					Action: func(c *cli.Context) error {
						log.Printf("%v", c.Int64("myi"))
						fmt.Println("c.App.Name for App.Commands.Subcommands.Action is", c.App.Name)
						return nil
					},
				},
			},
		},
	}

	err := app.Run(os.Args)
	if err != nil {
		log.Fatal()
	}
}
$ go run main.go foo bar
2022/10/21 18:06:57 10
c.App.Name for App.Commands.Subcommands.Action is myprogramname
$ go run main.go -myi 11 foo bar
2022/10/21 18:07:06 11
c.App.Name for App.Commands.Subcommands.Action is myprogramname

Since there are workaround for v1 I am closing this issue

@AndreasBackx I know I’m necrobumping this and I hope you’ve found some workaround, but for other devs who are seeing this and thinking of XKCD 979, here’s something I’ve cobbled together which actually works for finding flag values from variously nested levels of cli.Contexts.


import (
	"time"

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

func flagExistsInContext(c *cli.Context, flagName string) bool {
	for _, f := range c.LocalFlagNames() {
		if f == flagName {
			return true
		}
	}

	return false
}

func contextWithFlag(c *cli.Context, flagName string) (*cli.Context, bool) {
	var (
		ctx = c
		ok  = false
	)

	lineage := c.Lineage()
	if len(lineage) == 1 {
		return c, flagExistsInContext(c, flagName)
	}

	for i := range lineage {
		if flagExistsInContext(lineage[i], flagName) {
			ctx = lineage[i]
			ok = true
			break
		}
	}

	return ctx, ok
}

func GetInt64Slice(c *cli.Context, flagName string) (val []int64) {
	flagCtx, ok := contextWithFlag(c, flagName)
	if ok {
		val = flagCtx.Int64Slice(flagName)
	} else {
		val = make([]int64, 0)
	}

	return
}

func GetStringSlice(c *cli.Context, flagName string) (val []string) {
	flagCtx, ok := contextWithFlag(c, flagName)
	if ok {
		val = flagCtx.StringSlice(flagName)
	} else {
		val = make([]string, 0)
	}

	return
}

func GetString(c *cli.Context, flagName string, defaultValue ...string) (val string) {
	flagCtx, ok := contextWithFlag(c, flagName)
	if ok {
		val = flagCtx.String(flagName)
	} else if len(defaultValue) > 0 {
		val = defaultValue[0]
	}

	return
}

func GetInt64(c *cli.Context, flagName string, defaultValue ...int64) (val int64) {
	flagCtx, ok := contextWithFlag(c, flagName)
	if ok {
		val = flagCtx.Int64(flagName)
	} else if len(defaultValue) > 0 {
		val = defaultValue[0]
	}

	return
}

func GetUint64(c *cli.Context, flagName string, defaultValue ...uint64) (val uint64) {
	flagCtx, ok := contextWithFlag(c, flagName)
	if ok {
		val = flagCtx.Uint64(flagName)
	} else if len(defaultValue) > 0 {
		val = defaultValue[0]
	}

	return
}

func GetDuration(c *cli.Context, flagName string, defaultValue ...time.Duration) (val time.Duration) {
	flagCtx, ok := contextWithFlag(c, flagName)
	if ok {
		val = flagCtx.Duration(flagName)
	} else if len(defaultValue) > 0 {
		val = defaultValue[0]
	}

	return
}

func GetBool(c *cli.Context, flagName string, defaultValue ...bool) (val bool) {
	flagCtx, ok := contextWithFlag(c, flagName)
	if ok {
		val = flagCtx.Bool(flagName)
	} else if len(defaultValue) > 0 {
		val = defaultValue[0]
	}

	return
}

func GetPath(c *cli.Context, flagName string, defaultValue ...string) (val string) {
	flagCtx, ok := contextWithFlag(c, flagName)
	if ok {
		val = flagCtx.Path(flagName)
	} else if (len(defaultValue)) > 0 {
		val = defaultValue[0]
	}

	return
}