Skip to content

mailund/cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

80 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cli

TravisCI Status codecov Coverage Status Codacy Badge

Command line parsing for go.

Anatomy of command line tools as seen by cli

This package assumes that command line tools consists of a command, followed by flags, and then positional arguments:

> cmd -x -foo=42 a b c

The flags, -x and --foo are optional but with default values, while some of the positional arguments are required, except for potentially a variable number of arguments at the end of a command.

For many command line tools, there are also subcommands that looks like

> cmd -x --foo=42 subcmd -y a b c

where cmd is the command line tool, the first flags, -x and --foo are options to that, and then subcmd is a command in itself, getting its own flags, -y and arguments a b c.

Go's flag package handles flags, and does it very well, but it doesn't handle arguments or subcommands. There are other packages that implement support for subcommands, but not any that I much liked, so I implemented this module to get something more to my taste.

Commands

Commands are created with the NewCommand function from a command specification. The specification provides information about the name of the command, documentation as two strings, a short used when listing subcommands and a long used when showing usage of a specific command.

A command, that does absolutely nothing, but that you can run, could look like this:

cmd := cli.NewCommand(
  cli.CommandSpec{
    Name:  "first",
    Short: "my first command",
    Long:  "The first command I ever made",
  })

If you want to run it, you call its Run() method with a slice of strings:

cmd.Run([]string{})

This command doesn’t take any arguments, flags or positional, so you have to give it an empty slice.

If you want a command to do something when you call it, you must provide an Action in the specification. That is a function that takes an interface{} as its sole argument and doesn’t return anything. A “hello, world” command would look like this:

cmd := cli.NewCommand(
  cli.CommandSpec{
    Name:   "hello",
    Short:  "prints hello world",
    Action: func(_ interface{}) {
      fmt.Println("hello, world!")
    },
  })

If you run this command, cmd.Run([]string{}), it will print “hello, world!”.

If you want to make it a complete program, you just put the code in main and use os.Args[1:] as the arguments:

package main

import (
  "fmt"
  "os"

  "github.com/mailund/cli"
)

func main() {
  cmd := cli.NewCommand(
    cli.CommandSpec{
      Name:   "hello",
      Long:   "Prints hello world",
      Action: func(_ interface{}) { fmt.Println("hello, world!") },
  })

  cmd.Run(os.Args[1:])
}

I changed the Short paramemter to Long here, since that is what cli uses to give full information about a command, rather than listing what subcommands do, but it wouldn't matter in this case. If Long isn't provided, it will use Short.

If you run the program without any arguments, you get "hello, world!" back. Assuming you compiled the program into the executable hello, you get:

> hello
hello, world!

If you call the program with the -h or -help flag, you get the following usage information:

> hello -h
Usage: hello [flags]

Prints hello world

Flags:
  -h,--help
    show help for hello

If you call it with an unkown option, so anything but the help flag, or with any arguments, you get an error message and the same usage information. As we have specified the command, it doesn't take any arguments, so cli consider it an error if it gets any.

Command arguments

The interface{} argument to the Action is where the action gets its arguments. To supply command arguments, you must define a type and one more function.

Let’s say we want a command that adds two numbers. For that, we need to arguments, and we will make them positional. We create a type for the arguments that looks like this:

type AddArgs struct {
  X int `pos:"x" descr:"first addition argument"`
  Y int `pos:"y" descr:"second addition argument"`
}

The fields X and Y must be capitalised, because cli uses reflection to analyse the structure. The tags after the types is where we tell cli about the arguments. We make a field a positional argument using the tag pos:"name", and if we want to give the parameter a description, we do so with descr:"description". If you want a flag instead of a positional argument, you use flag:"name" instead of pos:"name". You don’t need tags for all the fields in the structure you want to give your command, but those you want cli to parse, you do.

To set up arguments, you proved another function, Init. It should return an interface{}, and it is that interface{} that cli parses for tags and that it provides to Action when the command runs.

We could set up our command for adding two numbers like this:

add := cli.NewCommand(
  cli.CommandSpec{
    Name:  "add",
    Short: "adds two floating point arguments",
    Long:  "<long description of how addition works>",
    Init:  func() interface{} { return new(AddArgs) },
    Action: func(args interface{}) {
      // Get the argumens through casting the args
      argv, _ := args.(*AddArgs)

      // Then we can do our calculations with the arguments
      fmt.Printf("Result: %d\n", argv.X+argv.Y)
    },
  })

The function we provide to Init returns a pointer to a new AddArgs, and when Action is called, it can cast the interface{} argument to *AddArgs and get the positional values. If you run it with add.Run([]string{"2", "2"}) it will print Result: 4 as expected.

Here’s a variation that adds a flag as well:

type NumArgs struct {
  Round bool      `flag:"round" short:"r" descr:"should result be rounded to nearest integer"`
  Args  []float64 `pos:"args" descr:"numerical arguments to command"`
}

cmd := cli.NewCommand(
  cli.CommandSpec{
    Name:  "sum",
    Short: "adds floating point arguments",
    Long:  "<long description of how addition works>",
    Init:  func() interface{} { return new(NumArgs) },
    Action: func(args interface{}) {
      argv, _ := args.(*NumArgs)
      res := 0.0
      for _, x := range argv.Args {
        res += x
      }
      if argv.Round {
        res = math.Round(res)
      }
      fmt.Printf("Result: %f\n", res)
    },
  })

The type NumArgs has both a flag: and pos: tag, creating a boolean flag determining whether we should round the result of the calculations and a variadic (multiple argument) positional variable of type []float64, which means that the command will be able to take any number of float arguments.

The Init function again just creates a new(NumArgs), and the Action is a little more complicated, but only because it calculates the sum of multiple values. The way it handles the arguments is the same: it casts the argument to *NumArgs and from there it can get both the flag and the float positional arguments.

Run it with cmd.Run([]string{"0.42", "3.14", "0.3"}) it and will print the sum (3.860000) and with cmd.Run([]string{"--round", "0.42", "3.14", "0.3"}) and it will round the result (4.000000 — it is a silly example, so I still print it as a float…).

The string after flag: in the tags defines the name of the flag. If it is a single letter, it can be used as both a long flag, --f and as a short flag -f, but if it is more than one letter, you can only use it as a long flag --flag. If you want both a long and a short flag, you can use the short: tag, as we did above, and if you only want a short flag, you can provide the short tag and leave the name after flag: empty. By adding short:"r", we install two flags, the long --round and the short -r. Short flags can only have one letter, but you can combine them, so -xyz is equivalent to -x -y -z. You cannot combine long flags, but you can provide values to them with the syntax --flag=value, where short flags can only take values as -f value.

Flags have default values, so how do we deal with that? The short answer is that the default values for flags are the values the struct’s fields have when you give it to cli. That means that your Init function can set the default values simply by setting the struct’s fields before it returns it.

This is how we could make the --round flag default to true:

cmd := cli.NewCommand(
  cli.CommandSpec{
    // The other arguments are the same as before…
    Init: func() interface{} {
      return &NumArgs{Round: true}
    },
    Action: func(args interface{}) {
      // the Action function is the same as before
    },
  })

Simply setting argv.Round = true before we return from Init will make true the default value for the flag.

You can use any of the types string, bool, int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, complex64 and complex128 for flags and positional arguments. That's all the non-composite types except uintptr and unsafe pointers, which I wouldn't know how to handle generically...

Slice types with the same underlying types will be consider variadic arguments, and you can use those for positional arguments as long as there is only one variadic parameter per command, and provided that the command does not have sub-commands (see below).

Any type that implements the interface

type PosValue interface {
  Set(string) error
}

for positional values or

type FlagValue interface {
  String() string
  Set(string) error
}

as pointer-receivers will also work. Types that implement

type VariadicValue interface {
	Set([]string) error
}

can be used as variadic parameters.

Subcommands

You can nest commands to make subcommands. Say we want a tool that can do both addition and multiplication. We can create a command with two subcommands to achieve this. The straightforward way to do this is to give a command a Subcommands in its specification. It can look like this:

type CalcArgs struct {
  X int `pos:"x" descr:"first argument"`
  Y int `pos:"y" descr:"second argument"`
}

add := cli.NewCommand(
  cli.CommandSpec{
    Name:  "add",
    Short: "adds two floating point arguments",
    Long:  "<long description of how addition works>",
    Init:  func() interface{} { return new(CalcArgs) },
    Action: func(args interface{}) {
      fmt.Printf("Result: %d\n", args.(*CalcArgs).X+args.(*CalcArgs).Y)
    },
  })

mult := cli.NewCommand(
  cli.CommandSpec{
    Name:  "mult",
    Short: "multiplies two floating point arguments",
    Long:  "<long description of how multiplication works>",
    Init:  func() interface{} { return new(CalcArgs) },
    Action: func(args interface{}) {
      fmt.Printf("Result: %d\n", args.(*CalcArgs).X*args.(*CalcArgs).Y)
    },
  })

calc := cli.NewCommand(
  cli.CommandSpec{
    Name:        "calc",
    Short:       "does calculations",
    Long:        "<long explanation of arithmetic>",
    Subcommands: []*cli.Command{add, mult},
  })

Now calc has two subcommands, and you can invoke addition with calc.Run([]string{"add", "2", "3"}) and multiplication with calc.Run([]string{"must", "2", "3"}).

Turn it into a complete program:

package main

import (
  "fmt"
  "os"

  "github.com/mailund/cli"
)

type CalcArgs struct {
  X int `pos:"x" descr:"first argument"`
  Y int `pos:"y" descr:"second argument"`
}

func main() {
  add := cli.NewCommand(
    cli.CommandSpec{
      Name:  "add",
      Short: "adds two floating point arguments",
      Long:  "<long description of how addition works>",
      Init:  func() interface{} { return new(CalcArgs) },
      Action: func(args interface{}) {
        fmt.Printf("Result: %d\n", args.(*CalcArgs).X+args.(*CalcArgs).Y)
      },
    })

  mult := cli.NewCommand(
    cli.CommandSpec{
      Name:  "mult",
      Short: "multiplies two floating point arguments",
      Long:  "<long description of how multiplication works>",
      Init:  func() interface{} { return new(CalcArgs) },
      Action: func(args interface{}) {
        fmt.Printf("Result: %d\n", args.(*CalcArgs).X*args.(*CalcArgs).Y)
      },
    })

  calc := cli.NewCommand(
    cli.CommandSpec{
      Name:        "calc",
      Short:       "does calculations",
      Long:        "<long explanation of arithmetic>",
      Subcommands: []*cli.Command{add, mult},
    })

  calc.Run(os.Args[1:])
}

compile it into an executable called calc, and then on the command line you can get information about how to use it using the -h flag

> calc -h
Usage: calc [flags] cmd ...

<long explanation of arithmetic>

Flags:
  -h,--help
    show help for calc

Arguments:
  cmd
	sub-command to call
  ...
	argument for sub-commands

Commands:
  add
	adds two floating point arguments
  mult
	multiplies two floating point arguments

You get the long description (which doesn't say much here, admittedly), then the options and arguments, since the calc command has subcommands those are cmd and ..., and a list of the subcommands with their short description.

You can get help about the subcommands by providing those with the -h flag (or calling them incorrectly). To get help about add, we can use:

> calc add -h
Usage: add [flags] x y

<long description of how addition works>

Flags:
  -h,--help
    show help for add

Arguments:
  x
	first argument
  y
	second argument

To invoke the commands, you do what experience has taught you and type the subcommand names after the main command:

> calc add 2 3
Result: 5
> calc mult 2 3
Result: 6

The parent command, calc doesn’t have any action itself, and commands with subcommands do not have to. When a command has subcommands, cli automatically dispatches to the subcommands. If you provide an Action, however, it is called before the dispatch, and the dispatching is done after it complets. If we changed the calc command to this:

calc := cli.NewCommand(
  cli.CommandSpec{
    Name:        "calc",
    Short:       "does calculations",
    Long:        "<long explanation of arithmetic>",
    Action:      func(_ interface{}) {
      fmt.Println("Hello from calc")
    },
    Subcommands: []*cli.Command{add, mult},
  })

the tool would print “Hello from calc” before dispatching:

> calc add 2 3
Hello from calc
Result: 5
> calc mult 2 3
Hello from calc
Result: 6

The syntax for subcommands, especially the []*cli.Command{…} bit is somewhat cumbersome, and we usually do not associate arguments and actions to most commands that merely function as menus, so there is a convenience function that does the same thing:

calc := cli.NewMenu(
  "calc", "does calculations",
  "<long explanation of arithmetic>",
  add, mult)

It takes the name, short and long parameters and then a variadic number of commands as arguments.

You can nest commands and subcommands arbitrarily deep:

say := func(x string) func(interface{}) {
  return func(argv interface{}) { fmt.Println(x) }
}
menu := cli.NewMenu("menu", "", "",
  cli.NewMenu("foo", "", "",
    cli.NewCommand(cli.CommandSpec{Name: "x", Action: say("menu/foo/x")}),
    cli.NewCommand(cli.CommandSpec{Name: "y", Action: say("menu/foo/y")})),
  cli.NewMenu("bar", "", "",
    cli.NewCommand(cli.CommandSpec{Name: "x", Action: say("menu/bar/x")}),
    cli.NewCommand(cli.CommandSpec{Name: "y", Action: say("menu/bar/y")})),
  cli.NewCommand(cli.CommandSpec{Name: "baz", Action: say("menu/baz")}))

menu.Run([]string{"baz"})      // will say menu/baz
menu.Run([]string{"foo", "x"}) // will say menu/foo/x
menu.Run([]string{"bar", "y"}) // will say menu/bar/y

When you use subcommands, as you can see above, you don’t provide the name of the root command in the arguments. The root will usually be the executable, so if the executable for the example above is called menu, we would have:

> menu baz
menu/baz
> menu foo x
menu/foo/x
> menu bar y
menu/bar/y

so it all works out.

Callbacks

Instead of using parameters as values to set, you can install callback functions that will be called when the user provide a flag, or called on positional arguments. There are six types of functions you can use as callbacks, in different situations.

type (
  NVCB  = func()
  NVCBI = func(interface{})

  NVCBE  = func() error
  NVCBIE = func(interface{}) error

  CB  = func(string) error
  CBI = func(string, interface{}) error

  VCB  = func([]string) error
  VCBI = func([]string, interface{}) error
)

The first four, NVCB, NVCBI, NVCBE and NVCBIE do not take any command-line arguments. They can only be used as flags. When they are, a flag, -f without arguments, will call the function. A NVCB and NVCBE flag will be called without arguments, of course, and a NVCBI or NVCBIE function will be called with the command line's argument structure, as returned from the Init function. The -E functions return an error and the other two do not. Callbacks that do not receive any input might not want to implement error handling, so cli support both boolean callbacks with and without an error return value. The functions that receive input should all return an error (although I might change that in the future).

The CB and CBI functions work with both flags and positional arguments. For flags, --f=arg or -f arg, the arg string is passed as the first argument, and for positional arguments, whatever argument is on the command line will be the callback's argument. They differ only in the second argument to functions of type CBI, which will be the command's argument-struct when the function is called (so values set before the function is called can be found in the struct, but flags and positional arguments that come after will not have been set yet).

The VCB and VCBI functions work the same as CB and CBI but take multiple arguments in the form of a string slice and can only be used for variadic parameters.

The example below shows callbacks and Value interfaces in action. We define a type, Validator that holds a string that must be validated according to some rules that are quite simple in this example, but you might be able to imagine something more complex.

We have two rules for validating the string in Validator, we might require that it is all uppercase or all lowercase, and we have functions to check this, and two methods for setting the rule. If we don't set a rule, then any string is valid.

If we implement the PosValue interface mentioned above, i.e. we implement a Set(string) error method, then we can use a Validator a positional argument, so we do that.

After that, we can make the type for command line arguments. We have a positional argument for the Validator, so it will get a positional argument, and we provide two flags to choose between the validation rules. If we don't provide a flag, we get the default (no checking), if we use -u we validate that the positional argument is all uppercase, and if we provide -l we validate that the argument is all lowercase.

The two callbacks have signature func() (the BCB type above), so they are valid types for cli. We put the methods from the validator the arguments holds in the fields. When we write args.Val.setUpperValidator we have already bound the receiver, so the method becomes a function that doesn't take any arguments, and thus we can use it as the field in the structure.

package main

import (
  "fmt"
  "os"
  "strings"

  "github.com/mailund/cli"
  "github.com/mailund/cli/interfaces"
)

type Validator struct {
  validator func(string) bool
  x         string
}

func upperValidator(x string) bool { return strings.ToUpper(x) == x }
func lowerValidator(x string) bool { return strings.ToLower(x) == x }

func (val *Validator) setUpperValidator() {
  val.validator = upperValidator
}

func (val *Validator) setLowerValidator() {
  val.validator = lowerValidator
}

// Implementing Set(string) error means we can use a Validator as a
// positional argument
func (val *Validator) Set(x string) error {
  if val.validator != nil && !val.validator(x) {
    return interfaces.ParseErrorf("'%s' is not a valid string", x)
  }
  val.x = x
  return nil
}

type Args struct {
  Upper func()    `flag:"" short:"u" descr:"sets the validator to upper"`
  Lower func()    `flag:"" short:"l" descr:"sets the validator to lower"`
  Val   Validator `pos:"string" descr:"string we might do something to, if valid"`
}

// Now we have everything ready to set up the command.
func Init() interface{} {
  args := Args{}
  args.Upper = args.Val.setUpperValidator
  args.Lower = args.Val.setLowerValidator
  return &args
}

func Action(args interface{}) {
  fmt.Println("A valid string:", args.(*Args).Val.x)
}

func main() {
  cmd := cli.NewCommand(
    cli.CommandSpec{
      Name:   "validator",
      Long:   "Will only accept valid strings",
      Init:   Init,
      Action: Action,
    })
  cmd.Run(os.Args[1:])
}

The two flag's names are empty, flag:"", so they only get the short flag names, -u and -l.

Run it with -h to see a usage message:

> validate -h
Usage: validator [flags] string

Will only accept valid strings

Flags:
  -h,-help
  show help for validator
  -l
  sets the validator to lower
  -u
  sets the validator to upper

Arguments:
  string
	string we might do something to, if valid

Without any flags, the program will accept any string:

> validate Foo
A valid string: Foo

but set a validator, and it will complain if the string doesn't satisfy the validation method:

validate -l Foo
Error parsing parameter string='Foo', 'Foo' is not a valid string.

> validate -l foo
A valid string: foo

Protocols

Most of the behaviour of cli is handled through protocols and interfaces, making it relatively easy to extend.

Any type the implements PosValue as pointer-receiver can be used as a positional argument.

type PosValue interface {
  Set(string) error // Should set the value from a string
}

The base types are wrapped in types that do this, and integers, for example, are handled like this:

type IntValue int

func (val *IntValue) Set(x string) error {
  v, err := strconv.ParseInt(x, 0, strconv.IntSize)
  if err != nil {
    err = interfaces.ParseErrorf("argument \"%s\" cannot be parsed as int", x)
  } else {
    *val = IntValue(v)
  }
  return err
}

Flags also need a way to obtain the default value when printing usage help, so flags have the FlagValue interface:

type FlagValue interface {
  String() string   // Should return a string representation of the value
  Set(string) error // Should set the value from a string
}

This is the same Set(string) error method as for positional arguments and a function for printing a value as a string. For integers, cli's implementation is:

func (val *IntValue) String() string {
	return strconv.Itoa(int(*val))
}

Variadic arguments implement the VariadicValue interface:

type VariadicValue interface {
  Set([]string) error // Should set the value from a slice of strings
}

For integers, again, cli implements it as:

type VariadicIntValue []int

func (vals *VariadicIntValue) Set(xs []string) error {
  *vals = make([]int, len(xs))

  for i, x := range xs {
    val, err := strconv.ParseInt(x, 0, strconv.IntSize)
    if err != nil {
      return interfaces.ParseErrorf("cannot parse '%s' as int", x)
    }

    (*vals)[i] = int(val)
  }

  return nil
}

Implement your own type with the right methods, and you can use it as flags and positional arguments in cli. For example, if you want a type where you can select one of a small number of options, you could implement:

type Choice struct {
  Choice  string
  Options []string
}

func (c *Choice) Set(x string) error {
  for _, v := range c.Options {
    if v == x {
      c.Choice = v
      return nil
    }
  }

  return interfaces.ParseErrorf(
    "%s is not a valid choice, must be in %s", x, "{" + strings.Join(c.Options, ",") + "}")
}

func (c *Choice) String() string {
  return c.Choice
}

This Choice type implements the FlagValue, so you can use it as both flags and positional arguments.

We can use it like this:

package main

import (
	"fmt"
	"os"
	"strings"

	"github.com/mailund/cli"
)

type Args struct {
  A Choice `flag:"" short:"a" descr:"optional choice"`
  B Choice `pos:"b" descr:"mandatory choice"`
}

func Init() interface{} {
  return &Args{
    A: Choice{"A", []string{"A", "B", "C"}},
    B: Choice{"B", []string{"A", "B", "C"}},
  }
}

func Action(i interface{}) {
  args, _ := i.(*Args)
  fmt.Printf("Choice A was %s and choice B was %s\n", args.A.Choice, args.B.Choice)
}

func main() {
  cmd := cli.NewCommand(
    cli.CommandSpec{
      Name:   "choices",
      Long:   "Demonstration of the difficult task of making choices.",
      Init:   Init,
      Action: Action,
    })

  cmd.Run(os.Args[1:])
}

and you will find that if you provide the flag or positional with an invalid choice, you get an error:

> choices -a X
Error parsing flag:  error parsing flag -a: X is not a valid choice, must be in {A,B,C}.

A slightly annoying thing here is, though, that the only way to get information about the valid choices is to provide an invalid one. You probably want this in the description string, but since the Choice value and the description string are independent values, you risk that they end up inconsistent with each other. Ideally, you want the documentation string to get its information from the object you actually use in your arguments.

There are hooks for fixing that as well:

// ArgumentDescription provides a value a way to add to the description string for a flag or positional.
type ArgumentDescription interface {
  ArgumentDescription(flag bool, descr string) string // Modify or add to the description string
}

// FlagValueDescription provides a value a way to add to the "value" string of a flag.
type FlagValueDescription interface {
  FlagValueDescription() string // Modify or add to the description string
}

The ArgumentDescription protocol lets you modify the description, and the flag argument tells you if it is the description for a flag or a positional argument (you often want to handle those differently). The FlagValueDescription protocol lets you modify the string that comes after the flags in the usage output. By defaul it is value, but if you implement this method, you can change that.

We can use those methods to modify how Choice objects are printed:

package main

import (
  "fmt"
  "os"
  "strings"

  "github.com/mailund/cli"
  "github.com/mailund/cli/interfaces"
)

type Choice struct {
  Choice  string
  Options []string
}

func (c *Choice) Set(x string) error { /* same as before */ }
func (c *Choice) String() string     { /* same as before */ }

func (c *Choice) ArgumentDescription(flag bool, descr string) string {
	if flag {
		// Don't modify the flag description (we handle it on the value)
		return descr
	}

	return descr + " (choose from " + "{" + strings.Join(c.Options, ",") + "})"
}

func (c *Choice) FlagValueDescription() string {
	return "{" + strings.Join(c.Options, ",") + "}"
}

type Args struct { /* same as before */ }

func Init() interface{}    { /* same as before */ }
func Action(i interface{}) { /* same as before */ }
func main()                { /* same as before */ }

Run the program with -h now, and you get the more informative help:

> choice -h
Usage: choices [flags] b

Demonstration of the difficult task of making choices.

Flags:
  -h,--help
  show help for choices
  -a {A,B,C}
  optional choice (default A)

Arguments:
  b
  mandatory choice (choose from {A,B,C})

The Choice type is already implemented in cli, so if you want it as described here, you can use cli.Choice. Ideally, it should get its options as keys from a map, as that is often how we handle choices, but it is an ugly interface to do this through reflection, and cumbersome to do it using code generation, so I am waiting for generics before I add that feature.

For flags, there can be additional constraints. Some flags, for example, should not take arguments. If you don't want them to, then implement the NoValueFlag interface:

type NoValueFlag interface {
  NoValueFlag() bool
}

This is how callbacks that do not take arguments inform the flag parser about that. This is how callbacks without arguments (but that may raise errors) are implemented:

type FuncNoValue func() error         // Wrapping functions that do not take parameter arguments

func (f FuncNoValue) Set(_ string) error { return f() }
func (f FuncNoValue) String() string     { return "" }
func (f FuncNoValue) NoValueFlag() bool  { return true }

The first two methods ensure that we can use them as both flags and positional arguments, and the last tells the flags parser that it is an error to provide an argument, and a command line that looks like --foo bar baz, where --foo is such a flag, should not consider bar a value to be given to --foo.

Boolean flags are a little different. There, we usually want --foo to mean that the boolean value should be set to true, but we also want --foo=true and --foo=false to be valid. So, those flags have defaults. Well, all flags have defaults, but there is the default value when the flag is not used, and then the default value when we use it. For a boolean flag, usually the default if false, but --foo is interpreted as --foo=true, so true is the default string we should pass to the flag's Set(string) error method.

To handle those flags, we have the DefaultValueFlag interface:

type DefaultValueFlag interface {
  DefaultValueFlag() string
}

The DefaultValueFlag() string method should return the default string to give to Set(string) error when the flag is used without an argument. For booleans, cli implement it as:

func (val *BoolValue) DefaultValueFlag() string { return "true" }

Flags that implement DefaultValueFlag can only take arguments as --flag=arg, since there is no way of knowing if --flag foo should interpret foo as a value for --flag, or if --flag should use its default and we should treat foo as a positional argument.

There are values that we want to validate as soon as we have linked struct fields to flags and parameters. Some data will crash the program when we try to parse a command line, and although we cannot capture this at compile time, when we use reflection to connect a struct with cli, we want to capture it early. We will eventually discover it when the parser reaches a point it cannot handle, of course, but it is better to check the data as soon as commands are connected, because then we catch it every time we run the program, rather than when commands are parsed.

The Validator interface gives hook for that:

type Validator interface {
  Validate(flag bool) error // Should return nil if everything is fine, or an error otherwise
}

The flag boolean indicates if we are validating a flag rather than a positional argument. We sometimes want to distinguish between the two, since positional and variadic argumentns will always be initialised throug their Set(string) method when we parse a command-line, while flags might have to rely on a default value. Thus, for flags we might want to validate that the default is valid, which we do not need to for other arguments.

Flags and parameters that implement the interface will have their Validate(bool) error method called as soon as a command is created. Callbacks cannot be nil (that will certainly crash the program when we call them), and functions in cli check this using a Validator function:

func (f FuncNoValue) Validate(bool) error {
  if f == nil {
    return interfaces.SpecErrorf("callbacks cannot be nil")
  }

  return nil
}

We don't need to distinguish between flags and other parameters for callbacks, because the callback will be called regardless, and can never be nil.

Sometimes, we want to do something to a value again after parsing and before running a command. That gives us a hook to modify a value or handle default values that aren't quite ready to go. For that, we have Prepare.

type Prepare interface {
  PrepareValue() error // Called after parsing and before we run a command
}

cli has a file type for input and output files (called InFile and OutFile respectively). The idea is that you can specify a file argument, and cli will make sure that you have an open file when your command-action is called. For flags, a file can have a default stream, stdin for InFile and stdout/stderr for OutFile, or a default file name, and for flags, the default is used if the flag isn't provided.

The file objects are implemented using protocols, and for OutFile the PosValue and FlagValue protocols are implemented like this:

// OutFile represents an open file as an argument
type OutFile struct {
  io.Writer
  Fname string
}

func (o *OutFile) Open(fname string) error {
  f, err := os.Create(fname)
  if err != nil {
    return interfaces.ParseErrorf("couldn't open file %s: %s", fname, err)
  }

  o.Writer = f

  return nil
}

// Close implements the io.Closer interface by forwarding to the writer
func (o *OutFile) Close() error {
  var err error

  if closer, ok := o.Writer.(io.Closer); ok {
    err = closer.Close()
  }

  o.Writer = nil

  return err
}


// Set implements the PosValue/FlagValue interface
func (o *OutFile) Set(fname string) error {
  return o.Open(fname)
}

// String implements the FlagValue interface
func (o *OutFile) String() string {
  switch o.Writer {
  case os.Stdout:
    return "stdout"
  case os.Stderr:
    return "stderr"
  default:
    return `"` + o.Fname + `"`
  }
}

For positional arguments, this is all we need. A positional argument must be provided, so we never rely on the default, and we will always call Set(string) to open a file.

For flags, we must have a valid default in case the flag isn't used, and we can use the Validator protocol to ensure this:

func (o *OutFile) Validate(flag bool) error {
  if !flag || o.Writer != nil || o.Fname != "" {
    return nil // we have a valid default, or we will get an argument
  }

  return interfaces.SpecErrorf("outfile does not have a valid default")
}

If the value is not a flag, or if it has a stream or a filename, then we are fine, otherwise we are missing a valid default and that is an error.

But this isn't quite enough either. We want an open file when the command's action run, but we only guarantee that we have a valid default which is either a stream or a file name. We need to open the file if the default is a name, and we need to do that before we run the action. That is where we need the Prepare protocol.

func (o *OutFile) PrepareValue() error {
  if o.Writer != nil {
    return nil // we already have a writer
  }

  return o.Open(o.Fname)
}

Here we check if we already have an output stream. If we do, then the default was a stream or we opened a file because we got an argument, and then we are fine. Otherwise, we must open the default file that we only have the name of.

Here's a (somewhat primitive) program that copies data from an input file to an output file, with default streams for both:

package main

import (
  "io/ioutil"
  "log"
  "os"

  "github.com/mailund/cli"
)

type args struct {
  In  cli.InFile  `flag:"in" short:"i" descr:"input file"`
  Out cli.OutFile `flag:"out" short:"o" descr:"output file"`
}

func initCat() interface{} {
  return &args{
    In:  cli.InFile{Reader: os.Stdin},
    Out: cli.OutFile{Writer: os.Stdout},
  }
}

func catAction(i interface{}) {
  a, _ := i.(*args)

  buf, err := ioutil.ReadAll(a.In)
  if err != nil {
    log.Fatalf("error reading file: %s", err)
  }

  err = a.In.Close()
  if err != nil {
    log.Fatalf("error closing file: %s", err)
  }

  _, err = a.Out.Write(buf)
  if err != nil {
    log.Fatalf("error writing file: %s", err)
  }

  err = a.Out.Close()
  if err != nil {
    log.Fatalf("error closing file: %s", err)
  }
}

var catCmd = cli.NewCommand(
  cli.CommandSpec{
    Name:   "cat",
    Long:   "Writes the content of one file to another.",
    Init:   initCat,
    Action: catAction,
  })

func main() {
  catCmd.Run(os.Args[1:])
}

When catAction is called, the files are already open, whether we are using the defaults or have provided files via flags, and any errors that might happen opening the files are handled by cli. There is still a lot of error handling, because that is needed when working with files in go, but you know you have a valid file when the action starts.