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
 }
 


-- 
You received this message because you are subscribed to the Google Groups 
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to golang-nuts+unsubscr...@googlegroups.com.
To view this discussion visit 
https://groups.google.com/d/msgid/golang-nuts/6ae5687f-948c-4e6d-b392-cac6ceeb3a7dn%40googlegroups.com.

Reply via email to