simplecobra

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 27, 2023 License: MIT Imports: 5 Imported by: 9

README

Tests on Linux, MacOS and Windows Go Report Card codecov GoDoc

So, Cobra is a Go CLI library with a feature set that's hard to resist for bigger applications (autocompletion, docs and man pages auto generation etc.). But it's also complex to use beyond the simplest of applications. This package was built to help rewriting Hugo's commands package to something that's easier to understand and maintain.

I welcome suggestions to improve/simplify this further, but the core idea is that the command graph gets built in one go with a tree of struct pointers implementing a simple Commander interface:

// Commander is the interface that must be implemented by all commands.
type Commander interface {
	// The name of the command.
	Name() string

	// Init is called when the cobra command is created.
	// This is where the flags, short and long description etc. can be added.
	Init(*Commandeer) error

	// PreRun called on all ancestors and the executing command itself, before execution, starting from the root.
	// This is the place to evaluate flags and set up the this Commandeer.
	// The runner Commandeer holds the currently running command, which will be PreRun last.
	PreRun(this, runner *Commandeer) error

	// The command execution.
	Run(ctx context.Context, cd *Commandeer, args []string) error

	// Commands returns the sub commands, if any.
	Commands() []Commander
}

The Init method allows for flag compilation, referencing the parent and root etc. If needed, the full Cobra command is still available.

There's a runnable example in the documentation, but the gist of it is:

func main() {
	rootCmd := &rootCommand{name: "root",
		commands: []simplecobra.Commander{
			&lvl1Command{name: "foo"},
			&lvl1Command{name: "bar",
				commands: []simplecobra.Commander{
					&lvl2Command{name: "baz"},
				},
			},
		},
	}

	x, err := simplecobra.New(rootCmd)
	if err != nil {
		log.Fatal(err)
	}
	cd, err := x.Execute(context.Background(), []string{"bar", "baz", "--localFlagName", "baz_local", "--persistentFlagName", "baz_persistent"})
	if err != nil {
		log.Fatal(err)
	}

	// These are wired up in Init().
	lvl2 := cd.Command.(*lvl2Command)
	lvl1 := lvl2.parentCmd
	root := lvl1.rootCmd

	fmt.Printf("Executed %s.%s.%s with localFlagName %s and and persistentFlagName %s.\n", root.name, lvl1.name, lvl2.name, lvl2.localFlagName, root.persistentFlagName)
}

Differences to Cobra

You have access to the *cobra.Command pointer so there's not much you cannot do with this project compared to the more low-level Cobra, but there's one small, but important difference:

Cobra only treats the first level of misspelled commands as an unknown command with "Did you mean this?" suggestions, see see this issue for more context. The reason for this is the ambiguity between sub-command names and command arguments, but that is throwing away a very useful feature for a not very good reason. We recently rewrote Hugo's CLI using this package, and found only one sub command that needed to be adjusted to avoid this ambiguity.

Documentation

Overview

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/bep/simplecobra"
)

func main() {
	rootCmd := &rootCommand{name: "root",
		commands: []simplecobra.Commander{
			&lvl1Command{name: "foo"},
			&lvl1Command{name: "bar",
				commands: []simplecobra.Commander{
					&lvl2Command{name: "baz"},
				},
			},
		},
	}

	x, err := simplecobra.New(rootCmd)
	if err != nil {
		log.Fatal(err)
	}
	cd, err := x.Execute(context.Background(), []string{"bar", "baz", "--localFlagName", "baz_local", "--persistentFlagName", "baz_persistent"})
	if err != nil {
		log.Fatal(err)
	}

	// These are wired up in Init().
	lvl2 := cd.Command.(*lvl2Command)
	lvl1 := lvl2.parentCmd
	root := lvl1.rootCmd

	fmt.Printf("Executed %s.%s.%s with localFlagName %s and and persistentFlagName %s.\n", root.name, lvl1.name, lvl2.name, lvl2.localFlagName, root.persistentFlagName)
}

type rootCommand struct {
	name   string
	isInit bool

	persistentFlagName string
	localFlagName      string

	persistentFlagNameC string
	localFlagNameC      string

	ctx                  context.Context
	initThis             *simplecobra.Commandeer
	initRunner           *simplecobra.Commandeer
	failWithCobraCommand bool
	failRun              bool

	commands []simplecobra.Commander
}

func (c *rootCommand) Commands() []simplecobra.Commander {
	return c.commands
}

func (c *rootCommand) PreRun(this, runner *simplecobra.Commandeer) error {
	c.isInit = true
	c.persistentFlagNameC = c.persistentFlagName + "_rootCommand_compiled"
	c.localFlagNameC = c.localFlagName + "_rootCommand_compiled"
	c.initThis = this
	c.initRunner = runner
	return nil
}

func (c *rootCommand) Name() string {
	return c.name
}

func (c *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
	if c.failRun {
		return errors.New("failRun")
	}
	c.ctx = ctx
	return nil
}

func (c *rootCommand) Init(cd *simplecobra.Commandeer) error {
	if c.failWithCobraCommand {
		return errors.New("failWithCobraCommand")
	}
	cmd := cd.CobraCommand
	localFlags := cmd.Flags()
	persistentFlags := cmd.PersistentFlags()

	localFlags.StringVar(&c.localFlagName, "localFlagName", "", "set localFlagName")
	persistentFlags.StringVar(&c.persistentFlagName, "persistentFlagName", "", "set persistentFlagName")

	return nil
}

type lvl1Command struct {
	name   string
	isInit bool

	aliases []string

	localFlagName  string
	localFlagNameC string

	failInit             bool
	failWithCobraCommand bool
	disableSuggestions   bool

	rootCmd *rootCommand

	commands []simplecobra.Commander

	ctx context.Context
}

func (c *lvl1Command) Commands() []simplecobra.Commander {
	return c.commands
}

func (c *lvl1Command) PreRun(this, runner *simplecobra.Commandeer) error {
	if c.failInit {
		return fmt.Errorf("failInit")
	}
	c.isInit = true
	c.localFlagNameC = c.localFlagName + "_lvl1Command_compiled"
	c.rootCmd = this.Root.Command.(*rootCommand)
	return nil
}

func (c *lvl1Command) Name() string {
	return c.name
}

func (c *lvl1Command) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
	c.ctx = ctx
	return nil
}

func (c *lvl1Command) Init(cd *simplecobra.Commandeer) error {
	if c.failWithCobraCommand {
		return errors.New("failWithCobraCommand")
	}
	cmd := cd.CobraCommand
	cmd.DisableSuggestions = c.disableSuggestions
	cmd.Aliases = c.aliases
	localFlags := cmd.Flags()
	localFlags.StringVar(&c.localFlagName, "localFlagName", "", "set localFlagName for lvl1Command")
	return nil
}

type lvl2Command struct {
	name          string
	isInit        bool
	localFlagName string

	ctx       context.Context
	rootCmd   *rootCommand
	parentCmd *lvl1Command
}

func (c *lvl2Command) Commands() []simplecobra.Commander {
	return nil
}

func (c *lvl2Command) PreRun(this, runner *simplecobra.Commandeer) error {
	c.isInit = true
	c.rootCmd = this.Root.Command.(*rootCommand)
	c.parentCmd = this.Parent.Command.(*lvl1Command)
	return nil
}

func (c *lvl2Command) Name() string {
	return c.name
}

func (c *lvl2Command) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
	c.ctx = ctx
	return nil
}

func (c *lvl2Command) Init(cd *simplecobra.Commandeer) error {
	cmd := cd.CobraCommand
	localFlags := cmd.Flags()
	localFlags.StringVar(&c.localFlagName, "localFlagName", "", "set localFlagName for lvl2Command")
	return nil
}
Output:

Executed root.bar.baz with localFlagName baz_local and and persistentFlagName baz_persistent.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsCommandError

func IsCommandError(err error) bool

IsCommandError reports whether any error in err's tree matches CommandError.

Types

type CommandError

type CommandError struct {
	Err error
}

CommandError is returned when a command fails because of a user error (unknown command, invalid flag etc.). All other errors comes from the execution of the command.

func (*CommandError) Error

func (e *CommandError) Error() string

Error implements error.

func (*CommandError) Is added in v0.2.0

func (*CommandError) Is(e error) bool

Is reports whether e is of type *CommandError.

type Commandeer

type Commandeer struct {
	Command      Commander
	CobraCommand *cobra.Command

	Root   *Commandeer
	Parent *Commandeer
	// contains filtered or unexported fields
}

Commandeer holds the state of a command and its subcommands.

type Commander

type Commander interface {
	// The name of the command.
	Name() string

	// Init is called when the cobra command is created.
	// This is where the flags, short and long description etc. can be added.
	Init(*Commandeer) error

	// PreRun called on all ancestors and the executing command itself, before execution, starting from the root.
	// This is the place to evaluate flags and set up the this Commandeer.
	// The runner Commandeer holds the currently running command, which will be PreRun last.
	PreRun(this, runner *Commandeer) error

	// The command execution.
	Run(ctx context.Context, cd *Commandeer, args []string) error

	// Commands returns the sub commands, if any.
	Commands() []Commander
}

Commander is the interface that must be implemented by all commands.

type Exec

type Exec struct {
	// contains filtered or unexported fields
}

Exec provides methods to execute the command tree.

func New

func New(rootCmd Commander) (*Exec, error)

New creates a new Executer from the command tree in Commander.

func (*Exec) Execute

func (r *Exec) Execute(ctx context.Context, args []string) (*Commandeer, error)

Execute executes the command tree starting from the root command. The args are usually filled with os.Args[1:].

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL