Skip to content

Binding Commands

maxlandon edited this page Jan 4, 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

  • Each menu embeds a *cobra.Command type (not nil by default), to which users can bind any number of commands with any desired structure, behavior and specifications.
  • 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.
  • Thus, you can also leverage the reeflective/flags library to generate command trees, mix them with traditionally declared cobra commands, and use all of them in your console application.

Peculiarities

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.

Fortunately, cobra provides two functions to register such functions:

  • cobra.OnInitialize(func()): adds one or more functions to be ran before executing a target command.
  • cobra.OnFinalize(func()): adds one or more functions to be ran after executing the target command.

Users should thus ensure to bind a function reinstantiating their command tree to one of those. It is strongly advised to bind them with OnFinalize(), although the same effect will be probably achieved if used with OnInitialize(). Such an example is shown below.

Note: If you happen to use the reeflective/flags library, as in the section below, you won't need to take care of those reset functions: the library will automatically register them, as it also needs them for completion stuff.

1) Setting default behavior for the menu

Since each menu embeds a root *cobra.Command as its parser, we can set various things at the parser level: behavior, app-wise help and usage strings, etc... Note that you could even register flags to it, and you could call those flags without a preceding command: not very classic, nor very useful, but valid and working still.

Note, though, that one of the advantages of using cobra commands for execution is that you get most of the utility stuff you need for free: formatted help for commands, root to leaf, various execution stuff, etc.

Taking our current menu (main), and setting a few things on it:

func main() {
    app := console.New()
    configureReadline(app)
    createMenus(app)

    // Our menu of interest, and its root command
    root := app.CurrentMenu().Command

    // Set any behavior we want
    root.Long = `An introduction help string before the commands list` 
    root.SetHelpCommand(&customHelpCommand)

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

2) Binding classic cobra commands

Important: Since as mentionned, we need a way to reset commands on each execution loop, you should do 3 things:

  • Declare/bind all your commands with a function returning either a root command, or a list of them.
  • Pass this function in a call to cobra.OnFinalize() or cobra.OnInitialize().
  • Do not use init() functions for setting up your commands.

Suppose the following cobra command, wrapped into a function returning a list of commands

func myMenuCommands() (cmds []*cobra.Command) {

    var 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.
      
    cmds = append(cmds, versioncmd)
}

You then add all of these commands to your menu:

func bindCommands(menu *console.Menu) {
    for _, cmd := range myMenuCommands() {
        menu.AddCommand(cmd)
    }
}

Alternatively, if you bound all your commands to a root one that is to be your root parser, and on which you did set various things like in Section 1, you can replace the menu root command altogether:

func bindCommands(menu *console.Menu) {
    myMenuCommandRoot := menuTreeRoot()
    menu.Command = myMenuCommandRoot
        
    // Don't forget to pass this function to cobra for post-run reset.
    cobra.OnFinalize(func() { menu.Command = menuTreeRoot()})
}

3) 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 getCommands() (*cobra.Command, *carapace.Carapace) {
    // Our root command structure encapsulates
    // the entire command tree for our application.
    rootData := &commands.Root{}

    // Add validations
    var opts []flags.OptFunc
    opts = append(opts, flags.Validator(validator.New()))

    // Generate the command tree
    rootCmd := genflags.Generate(rootData, opts...)

    // Set any details we want to the root.
    rootCmd.SilenceUsage = true
    rootCmd.Short = shortUsage
    commands.AddCommandsLongHelp(rootCmd)

    // Generate the completion engine, which also takes care
    // of calling cobra for binding the reset routines.
    comps, _ := completions.Generate(rootCmd, rootData, nil)

    return rootCmd, comps
}

...

func main() {
    app := console.New()
    configureReadline(app)
    createMenus(app)
    menu := app.CurrentMenu()

    // Bind the generated commands and completions.
    menu.Command, menu.Carapace = getCommands()
}

4) Mixing reeflective/flags generated and traditionally declared cobra commands

The following section provides precisions and advices in the case where you would happen to use a command tree that has been build with interspersed cobra commands, some of them being traditionally declared like in Section 2, and others being generated out of structs like in Section 3.

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.

Clone this wiki locally