-
Notifications
You must be signed in to change notification settings - Fork 6
Binding Commands
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.
- 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.
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.
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
}
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()
orcobra.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()})
}
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()
}
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.
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.