commander

package module
Version: v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2019 License: GPL-3.0 Imports: 10 Imported by: 1

README

Documentation Go Report Card Coverage Status Build Status

This is a simple Go library to manage commands for your CLI tool. Easy to use and now you can focus on Business Logic instead of building the command routing.

What this library does for you?

Manage your separated commands. How? Generates a general help and command specific helps for your commands. If your command fails somewhere (panic for example), commander will display the error message and the command specific help to guide your user.

Install

$ go get github.com/yitsushi/go-commander

Sample output (from totp-cli)

$ totp-cli help

change-password                   Change password
update                            Check and update totp-cli itself
version                           Print current version of this application
generate <namespace>.<account>    Generate a specific OTP
add-token [namespace] [account]   Add new token
list [namespace]                  List all available namespaces or accounts under a namespace
delete <namespace>[.account]      Delete an account or a whole namespace
help [command]                    Display this help or a command specific help

Usage

Every single command has to implement CommandHandler. Check this project for examples.

package main

// Import the package
import "github.com/yitsushi/go-commander"

// Your Command
type YourCommand struct {
}

// Executed only on command call
func (c *YourCommand) Execute(opts *commander.CommandHelper) {
  // Command Action
}

func NewYourCommand(appName string) *commander.CommandWrapper {
  return &commander.CommandWrapper{
    Handler: &YourCommand{},
    Help: &commander.CommandDescriptor{
      Name:             "your-command",
      ShortDescription: "This is my own command",
      LongDescription:  `This is a very long
description about this command.`,
      Arguments:        "<filename> [optional-argument]",
      Examples:         []string {
        "test.txt",
        "test.txt copy",
        "test.txt move",
      },
    },
  }
}

// Main Section
func main() {
	registry := commander.NewCommandRegistry()

	registry.Register(NewYourCommand)

	registry.Execute()
}

Now you have a CLI tool with two commands: help and your-command.

❯ go build mytool.go

❯ ./mytool
your-command <filename> [optional-argument]   This is my own command
help [command]                                Display this help or a command specific help

❯ ./mytool help your-command
Usage: mytool your-command <filename> [optional-argument]

This is a very long
description about this command.

Examples:
  mytool your-command test.txt
  mytool your-command test.txt copy
  mytool your-command test.txt move

How to use subcommand pattern?

When you create your main command, just create a new CommandRegistry inside the Execute function like you did in your main() and change Depth.

import subcommand "github.com/yitsushi/mypackage/command/something"

func (c *Something) Execute(opts *commander.CommandHelper) {
	registry := commander.NewCommandRegistry()
	registry.Depth = 1
	registry.Register(subcommand.NewSomethingMySubCommand)
	registry.Execute()
}

PreValidation

If you want to write a general pre-validation for your command or just simply keep your validation logic separated:

// Or you can define inline if you want
func MyValidator(c *commander.CommandHelper) {
  if c.Arg(0) == "" {
    panic("File?")
  }

  info, err := os.Stat(c.Arg(0))
  if err != nil {
    panic("File not found")
  }

  if !info.Mode().IsRegular() {
    panic("It's not a regular file")
  }

  if c.Arg(1) != "" {
    if c.Arg(1) != "copy" && c.Arg(1) != "move" {
      panic("Invalid operation")
    }
  }
}

func NewYourCommand(appName string) *commander.CommandWrapper {
  return &commander.CommandWrapper{
    Handler: &YourCommand{},
    Validator: MyValidator
    Help: &commander.CommandDescriptor{
      Name:             "your-command",
      ShortDescription: "This is my own command",
      LongDescription:  `This is a very long
description about this command.`,
      Arguments:        "<filename> [optional-argument]",
      Examples:         []string {
        "test.txt",
        "test.txt copy",
        "test.txt move",
      },
    },
  }
}

Define arguments with type

&commander.CommandWrapper{
  Handler: &MyCommand{},
  Arguments: []*commander.Argument{
    &commander.Argument{
      Name: "list",
      Type: "StringArray[]",
    },
  },
  Help: &commander.CommandDescriptor{
    Name: "my-command",
  },
}

In your command:

if opts.HasValidTypedOpt("list") == nil {
  myList := opts.TypedOpt("list").([]string)
  if len(myList) > 0 {
    mockPrintf("My list: %v\n", myList)
  }
}

Define own type

Yes you can ;)

// Define your struct (optional)
type MyCustomType struct {
	ID   uint64
	Name string
}

// register your own type with parsing/validation
commander.RegisterArgumentType("MyType", func(value string) (interface{}, error) {
  values := strings.Split(value, ":")

  if len(values) < 2 {
    return &MyCustomType{}, errors.New("Invalid format! MyType => 'ID:Name'")
  }

  id, err := strconv.ParseUint(values[0], 10, 64)
  if err != nil {
    return &MyCustomType{}, errors.New("Invalid format! MyType => 'ID:Name'")
  }

  return &MyCustomType{
      ID:   id,
      Name: values[1],
    },
    nil
})

// Define your command
&commander.CommandWrapper{
  Handler: &MyCommand{},
  Arguments: []*commander.Argument{
    &commander.Argument{
      Name:        "owner",
      Type:        "MyType",
      FailOnError: true,          // Optional boolean
    },
  },
  Help: &commander.CommandDescriptor{
    Name: "my-command",
  },
}

In your command:

if opts.HasValidTypedOpt("owner") == nil {
  owner := opts.TypedOpt("owner").(*MyCustomType)
  mockPrintf("OwnerID: %d, Name: %s\n", owner.ID, owner.Name)
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var FmtPrintf = fmt.Printf

FmtPrintf is fmt.Printf

View Source
var OSExtExecutable = osext.Executable

OSExtExecutable returns current executable path

Functions

func RegisterArgumentType added in v1.1.0

func RegisterArgumentType(name string, f argumentTypeFunction)

RegisterArgumentType registers a new argument type

Example (CustomStruct)
package main

import (
	"errors"
	"strconv"
	"strings"

	commander "github.com/yitsushi/go-commander"
)

type MyCustomType struct {
	ID   uint64
	Name string
}

func main() {
	commander.RegisterArgumentType("MyType", func(value string) (interface{}, error) {
		values := strings.Split(value, ":")

		if len(values) < 2 {
			return &MyCustomType{}, errors.New("Invalid format! MyType => 'ID:Name'")
		}

		id, err := strconv.ParseUint(values[0], 10, 64)
		if err != nil {
			return &MyCustomType{}, errors.New("Invalid format! MyType => 'ID:Name'")
		}

		return &MyCustomType{
				ID:   id,
				Name: values[1],
			},
			nil
	})
}
Output:

Example (Simple)
package main

import (
	"strconv"

	commander "github.com/yitsushi/go-commander"
)

func main() {
	commander.RegisterArgumentType("Int32", func(value string) (interface{}, error) {
		return strconv.ParseInt(value, 10, 32)
	})
}
Output:

Types

type Argument added in v1.1.0

type Argument struct {
	Name          string
	Type          string
	OriginalValue string
	Value         interface{}
	Error         error
	FailOnError   bool
}

Argument represents a single argument

func (*Argument) SetValue added in v1.1.0

func (a *Argument) SetValue(original string) error

SetValue saves the original value to the argument. Returns with an error if conversion failed

type CommandDescriptor added in v1.1.0

type CommandDescriptor struct {
	// Required! name of the command
	Name string
	// Optional: argument list as a string
	// Basic convention: <required_argument> [optional_argument]
	Arguments string
	// Optional: Short description is used in general help
	ShortDescription string
	// Optional: Long description is used in command specific help
	LongDescription string
	// Optional: Examples array is used in command specific help
	Examples []string
}

CommandDescriptor describes a command for Help calls

type CommandHandler added in v1.1.0

type CommandHandler interface {
	// Execute function will be executed when the command is called
	// opts can be used for logging, parsing flags like '-v'
	Execute(opts *CommandHelper)
}

CommandHandler defines a command. If a struct implements all the required function, it is acceptable as a CommandHandler for CommandRegistry

type CommandHelper added in v1.1.0

type CommandHelper struct {
	// If -d is defined
	DebugMode bool
	// If -v is defined
	VerboseMode bool
	// Boolean opts
	Flags map[string]bool
	// Other opts passed
	Opts map[string]string
	// Non-flag arguments
	Args []string
	// contains filtered or unexported fields
}

CommandHelper is a helper struct CommandHandler.Execute will get this as an argument and you can access extra functions, farsed flags with this

func (*CommandHelper) Arg added in v1.1.0

func (c *CommandHelper) Arg(index int) string

Arg return with an item from Flags based on the given index emtpy string if not exists

Example
package main

import (
	"fmt"

	commander "github.com/yitsushi/go-commander"
)

// (c *MyCommand) Execute(opts *commander.CommandHelper)
var opts *commander.CommandHelper

func main() {
	opts.Parse([]string{"my-command", "plain-argument"})

	fmt.Println(opts.Arg(0))
}
Output:

plain-argument

func (*CommandHelper) AttachArgumentList added in v1.1.0

func (c *CommandHelper) AttachArgumentList(argumets []*Argument)

AttachArgumentList binds an Argument list to CommandHelper

func (*CommandHelper) ErrorForTypedOpt added in v1.1.0

func (c *CommandHelper) ErrorForTypedOpt(key string) error

ErrorForTypedOpt returns an error if the given value for the key is defined but not valid

func (*CommandHelper) Flag added in v1.1.0

func (c *CommandHelper) Flag(key string) bool

Flag return with an item from Flags based on the given key false if not exists

Example
package main

import (
	"fmt"

	commander "github.com/yitsushi/go-commander"
)

// (c *MyCommand) Execute(opts *commander.CommandHelper)
var opts *commander.CommandHelper

func main() {
	opts.Parse([]string{"my-command", "plain-argument", "-l", "--no-color"})

	if opts.Flag("l") {
		fmt.Println("-l is defined")
	}

	if opts.Flag("no-color") {
		fmt.Println("Color mode is disabled")
	}

}
Output:

-l is defined
Color mode is disabled

func (*CommandHelper) Log added in v1.1.0

func (c *CommandHelper) Log(message string)

Log is a logger function for debug messages it prints a message if DebugeMode is true

func (*CommandHelper) Opt added in v1.1.0

func (c *CommandHelper) Opt(key string) string

Opt return with an item from Opts based on the given key empty string if not exists

func (*CommandHelper) Parse added in v1.1.0

func (c *CommandHelper) Parse(flag []string)

Parse is a helper method that parses all passed arguments flags, opts and arguments

func (*CommandHelper) TypedOpt added in v1.1.0

func (c *CommandHelper) TypedOpt(key string) interface{}

TypedOpt return with an item from the predifined argument list based on the given key empty string if not exists

Example
package main

import (
	"fmt"
	"log"

	commander "github.com/yitsushi/go-commander"
)

// (c *MyCommand) Execute(opts *commander.CommandHelper)
var opts *commander.CommandHelper

func main() {
	opts.AttachArgumentList([]*commander.Argument{
		&commander.Argument{
			Name: "list",
			Type: "StringArray[]",
		},
	})
	opts.Parse([]string{"my-command", "--list=one,two,three"})

	// list is a StringArray[]
	if opts.ErrorForTypedOpt("list") == nil {
		log.Println(opts.TypedOpt("list"))
		myList := opts.TypedOpt("list").([]string)
		if len(myList) > 0 {
			fmt.Printf("My list: %v\n", myList)
		}
	}

	// Never defined, always shoud be an empty string
	if opts.TypedOpt("no-key").(string) != "" {
		panic("Something went wrong!")
	}

}
Output:

My list: [one two three]

type CommandRegistry

type CommandRegistry struct {
	Commands map[string]*CommandWrapper
	Helper   *CommandHelper
	Depth    int
	// contains filtered or unexported fields
}

CommandRegistry will handle all CLI request and find the route to the proper Command

func NewCommandRegistry

func NewCommandRegistry() *CommandRegistry

NewCommandRegistry is a simple "constructor"-like function that initializes Commands map

func (*CommandRegistry) CommandHelp

func (c *CommandRegistry) CommandHelp(name string)

CommandHelp prints more detailed help for a specific Command

func (*CommandRegistry) Execute

func (c *CommandRegistry) Execute()

Execute finds the proper command, handle errors from the command and print Help if the given command it unknown or print the Command specific help if something went wrong or the user asked for it.

func (*CommandRegistry) Help

func (c *CommandRegistry) Help()

Help lists all available commands to the user

func (*CommandRegistry) Register

func (c *CommandRegistry) Register(f NewCommandFunc)

Register is a function that adds your command into the registry

type CommandWrapper added in v1.1.0

type CommandWrapper struct {
	// Help contains all information about the command
	Help *CommandDescriptor
	// Handler will be called when the user calls that specific command
	Handler CommandHandler
	// Validator will be executed before Execute on the Handler
	Validator ValidatorFunc
	// Arguments is a simple list of possible arguments with type definition
	Arguments []*Argument
}

CommandWrapper is a general wrapper for a command CommandRegistry will know what to do this a struct like this

type NewCommandFunc added in v1.1.0

type NewCommandFunc func(appName string) *CommandWrapper

NewCommandFunc is the expected type for CommandRegistry.Register

type ValidatorFunc added in v1.1.0

type ValidatorFunc func(opts *CommandHelper)

ValidatorFunc can pre-validate the command and it's arguments Just throw a panic if something is wrong

Jump to

Keyboard shortcuts

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