Skip to content

Positionals

maxlandon edited this page Jan 2, 2023 · 3 revisions

The model for declaring and using positional arguments is one of the places where this library shines the most, and where lies one of the -relative- weaknesses of cobra. In cobra, positional arguments are loosely typed: always passed as string arrays. On top, library users must write a function either for converting or analyzing what is being fed to their commands.

This library changes that for the best, with mostly three features:

  • A mechanism for specifying various positional slots with their own requirements, on native types.
  • Almost for-free validation on each of them (see here).
  • A correspondingly exact, automatically generated Args function and its associated completion handler.

The positional struct

For any given command, only one embedded struct can be used to store the entire array of positional arguments 'slots':

type Create struct {
    Args struct {
        // Positional slots will come here
    } `positional-args:"yes"`
}

The snippet above declares a struct with no global requirements (eg. a required:"yes" tag on the struct). That is, the various fields we will declare are not required to be provided at least one argument each.

Positional fields (slots)

A simple example

We can now declare various slots, meant to receive our positional arguments. The following is an example where all of the positionals are required to be given at least one word each:

type Create struct {
    Args struct {
      Host   string   `desc:"Host where to create the resource"`
      Files  []string `desc:"Files to create on the remote host"`
    } `positional-args:"yes" required:"true"`
}

This command would then be invoked this way, supposing our Root CLI from the commands page:

./program create 10.10.10.10 file.go config.yml

Any arguments after the 10.10.10.10 host would then be contained by the Files slot, since it is a list placed in the last position, with no maximum requirement specified on the field.

You would then access the fields like this in your Execute(args []string) method:

func (c *Create) Execute(args []string) error {
    conn, err := net.Dial(c.Host)

    for _, file := range c.Files {
            // Use the connection to write/create the files however you want.
    }
}

Global requirements

Suppose the following command:

type Scan struct {
    Args struct {
      Host      string   `desc:"A host to scan"`
      Protocols []string `desc:"Protocols scans to run"`
      Port      uint16   `desc:"A port to scan on the host`
    } `positional-args:"yes" required:"true"`
}

As it is, the command above will require at least 3 arguments to the command: one for each field. If more than 3 arguments are passed, Host will get the first, Port will get the last, and all those in the middle will be put into Protocols:

./program scan host@domain.com ssh https imap 443 

*Note that parsing this positional struct would panic it there wasn't this global requirement tag: otherwise, the Protocols slice, having no maximum, would shadow Port and prevent any argument word from ever being put into it.

Individual requirements

We can also use more complex and per-field quantity requirements: the following will also demonstrate the role of the args parameter of the Execute() command above:

type Unmarshal struct {
	Args struct {
	  Files      []string `desc:"A list of files " required:"1-2"`
	  JSONConfig string `description:"a config to use for unmarshalling"`
	} `positional-args:"yes"`
}

func (u *Unmarshal) Execute(args []string) error {
}

Since there no global requirements, none of the individual fields are required by default. Thus, the JSONConfig field is optional, while the Files field will require at least one argument, and at most two. Consequently, and given the following invocation:

./program unmarshal file1.txt file2.txt config.json file3 file4

will parse file1.txt and file2.txt in the Files slot, the config.json in JSONConfig, and will pass the file3 and file4 words as args to the command's Execute() method.

Other examples

The last example above is quite an unrealistic one. It's actually the moment to say that this library even supports positional argument slots' arrangements so weird that no one would possibly would use those.

See the example binary and source code for other examples of positional arguments configurations.