From 704a2e0fc94c755439432d230ffb7fd2e3f6c6f0 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Sun, 19 Feb 2023 21:43:36 -0800 Subject: [PATCH 1/6] support for optional command before flags --- cli.go | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/cli.go b/cli.go index a4e5aa5..7284fc5 100644 --- a/cli.go +++ b/cli.go @@ -27,16 +27,20 @@ import ( // These variables is how to setup the arguments, flags and usage parsing for [Main] and [ServerMain]. // At minium set the MinArgs should be set. var ( + // Out parameters: // *Version will be filled automatically by the cli package, using [fortio.org/version.FromBuildInfo()]. ShortVersion string // x.y.z from tag/install LongVersion string // version plus go version plus OS/arch FullVersion string // LongVersion plus build date and git sha + Command string // first argument, if [CommandBeforeFlags] is true. // Following can/should be specified. ProgramName string // Used at the beginning of Usage() + // Optional for programs using subcommand, command will be set in [Command]. + CommandBeforeFlags bool // Cli usage/arguments example, ie "url1..." program name and "[flags]" will be added" // can include \n for additional details in the Usage() before the flags are dumped. ArgsHelp string - MinArgs int // Minimum number of arguments expected + MinArgs int // Minimum number of arguments expected, not counting (optional) command. MaxArgs int // Maximum number of arguments expected. 0 means same as MinArgs. -1 means no limit. // If not set to true, will setup static loglevel flag and logger output for client tools. ServerMode = false @@ -46,10 +50,15 @@ var ( ) func usage(w io.Writer, msg string, args ...any) { - _, _ = fmt.Fprintf(w, "%s %s usage:\n\t%s [flags]%s\nor 1 of the special arguments\n\t%s {help|version|buildinfo}\nflags:\n", + cmd := "" + if CommandBeforeFlags { + cmd = "command " + } + _, _ = fmt.Fprintf(w, "%s %s usage:\n\t%s %s[flags]%s\nor 1 of the special arguments\n\t%s {help|version|buildinfo}\nflags:\n", ProgramName, ShortVersion, baseExe, + cmd, ArgsHelp, os.Args[0], ) @@ -66,7 +75,8 @@ func usage(w io.Writer, msg string, args ...any) { // Will either have called [ExitFunction] (defaults to [os.Exit]) // or returned if all validations passed. func Main() { - quietFlag := flag.Bool("quiet", false, "Quiet mode, sets log level to warning") + quietFlag := flag.Bool("quiet", false, + "Quiet mode, sets loglevel to Error (quietly) to reduces the output") ShortVersion, LongVersion, FullVersion = version.FromBuildInfo() log.Config.FatalExit = ExitFunction baseExe = filepath.Base(os.Args[0]) @@ -95,8 +105,7 @@ func Main() { log.LoggerStaticFlagSetup("loglevel") } flag.CommandLine.Usage = func() { usage(os.Stderr, "") } // flag handling will exit 1 after calling usage, except for -h/-help - flag.Parse() - nArgs := len(flag.Args()) + nArgs := len(os.Args) if nArgs == 1 { switch strings.ToLower(flag.Arg(0)) { case "version": @@ -113,6 +122,16 @@ func Main() { return // not typically reached, unless ExitFunction doesn't exit } } + if CommandBeforeFlags { + if nArgs == 1 { + ErrUsage("Missing command argument") + return // not typically reached, unless ExitFunction doesn't exit + } + Command = os.Args[1] + os.Args = append([]string{os.Args[0]}, os.Args[2:]...) + nArgs-- + } + flag.Parse() argsRange := (MinArgs != MaxArgs) exactly := "Exactly" if nArgs < MinArgs { @@ -134,20 +153,18 @@ func Main() { return // not typically reached, unless ExitFunction doesn't exit } if *quietFlag { - log.SetLogLevelQuiet(log.Warning) + log.SetLogLevelQuiet(log.Error) } } -func errArgCount(prefix string, expected, actual int) bool { - return ErrUsage("%s %d %s expected, got %d", prefix, expected, Plural(expected, "argument"), actual) +func errArgCount(prefix string, expected, actual int) { + ErrUsage("%s %d %s expected, got %d", prefix, expected, Plural(expected, "argument"), actual) } // Show usage and error message on stderr and exit with code 1 or returns false. -func ErrUsage(msg string, args ...any) bool { +func ErrUsage(msg string, args ...any) { usage(os.Stderr, msg, args...) ExitFunction(1) - // not reached, typically - return false } // Plural adds an "s" to the noun if i is not 1. From 8859acfbb452dcdce53612a8aafd94b196863d67 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 20 Feb 2023 08:14:37 -0800 Subject: [PATCH 2/6] on os.args 0 is the program name --- cli.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli.go b/cli.go index 7284fc5..a96732e 100644 --- a/cli.go +++ b/cli.go @@ -106,8 +106,8 @@ func Main() { } flag.CommandLine.Usage = func() { usage(os.Stderr, "") } // flag handling will exit 1 after calling usage, except for -h/-help nArgs := len(os.Args) - if nArgs == 1 { - switch strings.ToLower(flag.Arg(0)) { + if nArgs == 2 { + switch strings.ToLower(os.Args[1]) { case "version": fmt.Println(ShortVersion) ExitFunction(0) From 1d92d6ea19a6dc3072397512f221da1ea76b68fd Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 20 Feb 2023 08:25:25 -0800 Subject: [PATCH 3/6] recalc nArgs after flag.Parse --- cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.go b/cli.go index a96732e..6e549da 100644 --- a/cli.go +++ b/cli.go @@ -129,9 +129,9 @@ func Main() { } Command = os.Args[1] os.Args = append([]string{os.Args[0]}, os.Args[2:]...) - nArgs-- } flag.Parse() + nArgs = len(flag.Args()) argsRange := (MinArgs != MaxArgs) exactly := "Exactly" if nArgs < MinArgs { From 281025ff03a213e5a8546817a5fcfe15f1f2099a Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 20 Feb 2023 09:33:28 -0800 Subject: [PATCH 4/6] add a bit of doc/info about new mode --- README.md | 2 ++ cli.go | 1 + 2 files changed, 3 insertions(+) diff --git a/README.md b/README.md index 074a7a7..15e8c38 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ It abstracts the repetitive parts of a `main()` command line tool, flag parsing, You can see real use example in a tool like [multicurl](https://github.com/fortio/multicurl) or a server like [proxy](https://github.com/fortio/proxy). +It also supports (sub)commands style (where there is a word/command before the flags and remaining arguments, [fortio](https://github.com/fortio/fortio) uses that mode). + ## Tool Example Client/Tool example (no dynamic flag url or config) [sampleTool](sampleTool/main.go) diff --git a/cli.go b/cli.go index 6e549da..4a6d1d5 100644 --- a/cli.go +++ b/cli.go @@ -9,6 +9,7 @@ // binary only accepts flags), setup additional [flag] before calling // [Main] or [fortio.org/scli.ServerMain] for configmap and dynamic flags // setup. +// Also supports (sub)commands style, see [CommandBeforeFlags] and [Command]. package cli // import "fortio.org/cli" import ( From 3363cafa6f63bcfb37ad1d30421a1a680e140af8 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 20 Feb 2023 09:39:38 -0800 Subject: [PATCH 5/6] doc update for no more bool return --- cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.go b/cli.go index 4a6d1d5..39130a6 100644 --- a/cli.go +++ b/cli.go @@ -162,7 +162,7 @@ func errArgCount(prefix string, expected, actual int) { ErrUsage("%s %d %s expected, got %d", prefix, expected, Plural(expected, "argument"), actual) } -// Show usage and error message on stderr and exit with code 1 or returns false. +// Show usage and error message on stderr and calls ExitFunction with code 1. func ErrUsage(msg string, args ...any) { usage(os.Stderr, msg, args...) ExitFunction(1) From 333d79c6ec464e62e48831feac1ea0385c7a678e Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 20 Feb 2023 09:40:07 -0800 Subject: [PATCH 6/6] link ExitFunction --- cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.go b/cli.go index 39130a6..e54e488 100644 --- a/cli.go +++ b/cli.go @@ -162,7 +162,7 @@ func errArgCount(prefix string, expected, actual int) { ErrUsage("%s %d %s expected, got %d", prefix, expected, Plural(expected, "argument"), actual) } -// Show usage and error message on stderr and calls ExitFunction with code 1. +// Show usage and error message on stderr and calls [ExitFunction] with code 1. func ErrUsage(msg string, args ...any) { usage(os.Stderr, msg, args...) ExitFunction(1)