informal proposal: KeepOnError handling for flag package

60 views
Skip to first unread message

Stephen Illingworth

unread,
Feb 28, 2025, 3:30:18 AMFeb 28
to golang-nuts

The following is an informal proposal for a new error handling method in the
standard flag package. I'm posting it here for a preliminary discussion. If there's any interest in it I'll prepare a more thorough proposal and submit it correctly


I am calling this method KeepOnError.

KeepOnError is similar to ContinueOnError but unlike ContinueOnError it will
retain unrecognised arguments in the argument list and will not return an error.

I find this useful for building flag hierarchies, particularly for programs that
have modes of operation.

To help illustrate what I mean I will use the following examples:

program -verbose execute -cpu=2 myfile

or:

program -verbose summarise -lang=en myfile

In these examples, 'execute' and 'summarise' are two different modes to the same
program. The -verbose flag is common to both modes.

The following is a sketch showing how I might implement the above examples using
ContinueOnError:

flgs := flag NewFlagSet("program", flag.ContinueOnError)

var verbose bool
flgs.BoolVar(&verbose, "verbose", false, "verbose output")

err := flag.Parse(args)
if err != nil {
if errors.Is(err, flag.ErrHelp) {
fmt.Println("modes: EXECUTE, SUMMARISE")
return nil
}
return err
}

args = flgs.Args()

if verbose {
setVerbose()
}

var mode string

if len(args) > 0 {
mode = strings.ToUpper(args[0])
}

switch mode {
default:
return errors.New("unrecognised mode")
case "EXECUTE":
execute(args[1:])
case "SUMMARISE":
summarise(args[1:])
}


Assuming the execute() and summarise() functions create their own flags then
that will more-or-less implement the above examples.

Now, say I want to give the program a default mode to save the user spelling it
out every time. With some small changes to the above implementation I could then
write:

program myfile

or:

program -verbose myfile

But I couldn't write:

program -cpu=2 myfile

This is because -cpu is not recognised by the top-level flag set. And even when
using ContinueOnError and ignoring the returned error, the Parse() function will swallow the -cpu flag.

The proposed KeepOnError will retain the -cpu argument in the Args() list. This
allows the argument to be passed on to the default function.

With KeepOnError we could even mix the order of the top-level arguments with the
mode specific arguments. For example:

program -cpu=2 -verbose myfile

As a user of this example "program" I would find this level of freedom more
convenient and user-friendly. I don't need to think about the ordering of
arguments. All I need to remember is that the program has a -cpu flag and a
-verbose flag.


My proposed implementation of KeepOnError is very simple. The patch is given
below. The diff has been made against the 1.24.0 version of the flag package.

The existing flag package tests all pass. I've not yet written any new tests for
KeepOnError but I'm happy to do so.


I am aware that there are well supported third-party libraries that perhaps
allow this level of control but I think this simple change to the standard
library would be very helpful.

Is there anything I've missed or is there maybe a good way of doing this already
using the standard library?




diff -urN a/flag.go b/flag.go
--- a/flag.go 2025-02-10 23:33:55.000000000 +0000
+++ b/flag.go 2025-02-28 08:22:26.843882403 +0000
@@ -100,6 +100,9 @@
 // but no such flag is defined.
 var ErrHelp = errors.New("flag: help requested")
 
+// errKeep is returned by parseOne() when an argument is not recognised
+var errKeep = errors.New("keep error")
+
 // errParse is returned by Set if a flag's value fails to parse, such as with an invalid integer for Int.
 // It then gets wrapped through failf to provide more information.
 var errParse = errors.New("parse error")
@@ -379,6 +382,7 @@
  ContinueOnError ErrorHandling = iota // Return a descriptive error.
  ExitOnError                          // Call os.Exit(2) or for -h/-help Exit(0).
  PanicOnError                         // Call panic with a descriptive error.
+ KeepOnError                          // Keep any arguments that aren't handled and continue parsing.
 )
 
 // A FlagSet represents a set of defined flags. The zero value of a FlagSet
@@ -1112,6 +1116,9 @@
  f.usage()
  return false, ErrHelp
  }
+ if f.errorHandling == KeepOnError {
+ return false, errKeep
+ }
  return false, f.failf("flag provided but not defined: -%s", name)
  }
 
@@ -1153,7 +1160,20 @@
 func (f *FlagSet) Parse(arguments []string) error {
  f.parsed = true
  f.args = arguments
+
+ // arg and keep are used by the KeepOnError handler
+ var arg string
+ var keep []string
+
+ defer func() {
+ f.args = append(keep, f.args...)
+ }()
+
  for {
+ if len(f.args) > 0 {
+ arg = f.args[0]
+ }
+
  seen, err := f.parseOne()
  if seen {
  continue
@@ -1161,6 +1181,7 @@
  if err == nil {
  break
  }
+
  switch f.errorHandling {
  case ContinueOnError:
  return err
@@ -1171,8 +1192,15 @@
  os.Exit(2)
  case PanicOnError:
  panic(err)
+ case KeepOnError:
+ if errors.Is(err, errKeep) {
+ keep = append(keep, arg)
+ } else {
+ return err
+ }
  }
  }
+
  return nil
 }
 


Reply all
Reply to author
Forward
0 new messages