Skip to content

Binding Commands

maxlandon edited this page May 29, 2023 · 10 revisions

You should now have a configured shell readline, created menus, and prompts setup for each.

We can now come to the core of the application: commands.

Table of Contents

Principles

  • At each command line execution, the active menu will execute the command line with a normal call to the command's Execute() function, thus triggering the normal cobra execution workflow.
  • The cobra model is that of a traditional CLI library: it assumes one command execution per application lifetime. Therefore, we need to take care of resetting commands to a blank state after each execution.

Each console menu offers the following function and type to bind commands to it:

// Commands is a function yielding a root cobra.Commmand.
type Commands func() *cobra.Command

// SetCommands binds the yielder function to the menu.
func (m *Menu) SetCommands(cmds Commands)

Note: Since these commands are to be reset/regenerated after each command execution, you should not use init() functions to initialize and set them up.

Basic example

The following shows how to declare and bind a command tree to our menu:

func myMenuCommands() *cobra.Command {

    // Root -----------------------------------------------------------
    // Our root command does not need any name. Even if it had one,
    // it will be ignored when the menu is being passed a command-line
    // to parse and execute.
    root := &cobra.Command{}
        
    // We however set any wished behavior to it.
    root.Long = `An introduction help string before the commands list` 
    root.SetHelpCommand(&customHelpCommand)

    // Set Pre/Post runners
    root.PreRunE = myMenuPreRunFunc
    root.PostRunE = myMenuPostRunFunc

    // Commands --------------------------------------------------------
    versioncmd := &cobra.command{
        use:   "version",
        short: "print the version number of hugo",
        long:  `all software has versions. this is hugo's`,
        run: func(cmd *cobra.command, args []string) {
        fmt.println("hugo static site generator v0.9 -- head")
        },
        // flags, positional args functions, pre/post runners, etc.
      
    root.AddCommand(versionCmd)
}

We then bind the root to the menu with this simple call:

menu.SetCommands(myMenuCommands)

Filtering commands

There are some cases when a subset of the available commands for a given menu should not be available, (they might be specific to some context that is not met, like a given OS, etc.). The command.Hidden attribute of cobra.Commands, however, will not prevent a hidden command from being run.

Therefore, the following two methods allow users to deactivate/reactivate commands based on one or more given filter words:

func (c *Console) HideCommands(filters ...string)
func (c *Console) ShowCommands(filters ...string)

A command that should be filtered for a given filter word should thus be annotated like this:

var myCmd &cobra.Command{Annotations: make(map[string]string{})}

// Multiple filters can be specified if comma-separated.
myCmd.Annotations[console.CommandFilterKey] = "filter1,filter2" 

Then, generally when switching to another menu in the application, and since the command tree changes, you would probably do this:

console.SwitchMenu("client")

offline := getApplicationNetworkStatus()

// Note that you might want to specify both calls, since you might have 
// filtered the commands in a previous menu switch, and that this one might 
// now have network, so you need to unhide the commands.
if offline {
    console.HideCommands("networked-commands")
} else {
    console.ShowCommands("networked-commands")
}

The commands that are filtered will also be automatically marked Hidden via their cobra field: they wont appear in the menu's help/usage strings, and not proposed as completions.

Completions

The console uses the carapace completion engine for providing command completion. The following shows how to produce the completions for your commands, taking back our example above. Notice the two new calls to carapace.Gen():

import (
	"github.com/rsteube/carapace"
)

func myMenuCommands() *cobra.Command {

    // Root -----------------------------------------------------------
    // Our root command does not need any name. Even if it had one,
    // it will be ignored when the menu is being passed a command-line.
    root := &cobra.Command{}
    
    // ... root command settings ...
    

    // Commands --------------------------------------------------------
    versioncmd := &cobra.command{
        use:   "version",
        short: "print the version number of hugo",
        long:  `all software has versions. this is hugo's`,
        run: func(cmd *cobra.command, args []string) {
        fmt.println("hugo static site generator v0.9 -- head")
        },
        // flags, positional args functions, pre/post runners, etc.
      
    root.AddCommand(versionCmd)
        
    // Our version command might have positional arguments, or flags
    // arguments to complete: we also generate an engine for this
    // command, and register completers to it.
    comps := carapace.Gen(versionCmd)
    
    comps.PositionalCompletion(myPositionalCompleter)
    comps.FlagCompletion(myFlagsCompletion)
}

Check the carapace documentation for writing and register completers.

Accessing commands during execution

Each console menu embeds the root cobra.Command returned by the command yielding function we saw in the first section. This is so that you can access the command tree from within your commands: this tree being renewed after each command run, the one you will access in your code is always up-to-date.

// Your console should be a global variable somewhere.
var app *console.Console

run := func(cmd *cobra.Command, args []string) {
    menuRoot := app.CurrentMenu().Command
}

Binding reeflective/flags command trees

If you are using only reeflective/flags to generate your cobra commands, this simple snippet is sufficient to bind them to the console, and everything will be taken care of out of the box (completions and reset). In this case, your application is ready to run. The example application uses such a configuration.

func flagsCommands() *cobra.Command {
	// Our root command structure encapsulates
	// the entire command tree for our application.
	rootData := &commands.Root{}

	// Options can be used for several purposes:
	// influence the flags naming conventions, register
	// other scan handlers for specialized work, etc...
	var opts []flags.OptFunc

	// One example of specialized handler is the validator,
	// which checks for struct tags specifying validations:
	// when found, this handler wraps the generated flag into
	// a special value which will validate the user input.
	opts = append(opts, flags.Validator(validator.New()))

	// Run the scan: this generates the entire command tree
	// into a cobra root command (and its subcommands).
	// By default, the name of the command is os.Args[0].
	rootCmd := genflags.Generate(rootData, opts...)

	// Since we now dispose of a cobra command, we can further
	// set it up to our liking: modify/set fields and options, etc.
	// There is virtually no restriction to the modifications one
	// can do on them, except that their RunE() is already bound.
	rootCmd.SilenceUsage = true
	rootCmd.Short = shortUsage
	rootCmd.Long = shortUsage + "\n" + commands.LongUsage

	// We might also have longer help strings contained in our
	// various commands' packages, which we also bind now.
	commands.AddCommandsLongHelp(rootCmd)

	// The completion generator is another example of specialized
	// scan handler: it will generate completers if it finds tags
	// specifying what to complete, or completer implementations
	// by the positional arguments / command flags' types themselves.
	completions.Generate(rootCmd, rootData, nil)

	return rootCmd
}