urfave / cli

A simple, fast, and fun package for building command line apps in Go
https://cli.urfave.org
MIT License
21.9k stars 1.69k forks source link

v3 MutuallyExclusive flags are not included in help text #1858

Closed joshfrench closed 4 months ago

joshfrench commented 5 months ago

Checklist

Hi hi! I recognize v3 is alpha software, so my question is more "is this implemented yet or am I doing it wrong?" Assuming it just hasn't been worked on yet, if someone can point me to where I might start investigating I'll see about a PR :)

package main

import (
    "context"
    "os"

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

func main() {
    cmd := &cli.Command{
        Name: "test",
        Flags: []cli.Flag{
            &cli.StringFlag{
                Name:  "s",
                Usage: "this works",
            },
        },
        MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{
            Flags: [][]cli.Flag{
                {
                    &cli.BoolFlag{
                        Name:  "b",
                        Usage: "no display :(",
                    },
                },
            },
        },
        },
    }

    cmd.Run(context.Background(), os.Args)
}
% test -h
NAME:
   test - A new cli application

USAGE:
   test [global options] [command [command options]] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   -s value    this works
   --help, -h  show help (default: false)

The same holds true for subcommands as well.

dearchap commented 5 months ago

@joshfrench This feature hasnt been implemented yet. You are welcome to submit a PR. The changes would be primarily in the template.go file which has the template to render help text.

joshfrench commented 4 months ago

Just getting around to this...

The basic case is straightforward, but once you account for flag categories I can see at least two approaches. The first doesn't discriminate between mutex and regular flags:

cmd := &cli.Command{
        Name: "test",
        MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{
            Flags: [][]cli.Flag{
                {
                    &cli.BoolFlag{
                        Name:  "b1",
                        Category: "one",
                    },
                },
                {
                    &cli.BoolFlag{
                        Name:  "b2",
                        Category: "two",
                    },
                },
            },
        },
        },
    }
GLOBAL OPTIONS:
   one

   --b1

  two

  --b2

This is flexible but doesn't quite track with my mental model of how mutex flags are meant to be used. The second approach would treat each mutex group as its own implicit category:

cmd := &cli.Command{
        Name: "test",
        Flags: []cli.Flag{
            &cli.StringFlag{
                    Name: "s1",
                    Category: "one",
            }
        },
        MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{
            Flags: [][]cli.Flag{
                {
                    &cli.BoolFlag{
                        Name:  "b1",
                    },
                },
                {
                    &cli.BoolFlag{
                        Name:  "b1",
                    },
                },
            },
        },
        },
    }
GLOBAL OPTIONS:
   one

   --s1

  ???

  --b1 | --b2

This approach is more prescriptive, but is closer to how I would expect things to work. Additionally:

Thoughts before I dive into category handling? I think I'm inclined to go with option 1 and leave it up to the user to categorize mutex flags how they wish:

cmd := &cli.Command{
        Name: "test",
        MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{
            Flags: [][]cli.Flag{
                {
                    &cli.BoolFlag{
                        Name:  "b1",
                        Category: "one",
                    },
                },
                {
                    &cli.BoolFlag{
                        Name:  "b2",
                        Category: "one",
                    },
                    &cli.BoolFlag{
                        Name:  "b3",
                        Category: "i know what i'm doing",
                    },
                },
            },
        },
        },
    }
dearchap commented 4 months ago

@joshfrench Yes option 1 would be good to provide maximum flexibility. But also we can have a check that all the mutex flags should be in the same category. It doesnt make sense to have these flag groups across multiple categories.

joshfrench commented 4 months ago

@dearchap Is there a better place for that check than the MutuallyExclusiveFlags.check method? That seems aimed at catching user error, not cli developer error.

joshfrench commented 4 months ago

Some alternatives to treating it as an error:

The latter strikes me as both easiest to implement and to understand as an end-user.

dearchap commented 4 months ago

Yes I think the latter approach of adding a Category field to the group is better.

joshfrench commented 4 months ago

PR updated! I did add a SetCategory() method to allow mutex groups to propagate their category downward to the flags within the group, which is triggering a docs warning -- I'll wait for a review before doing anything else. Thanks!