ask

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2020 License: MIT Imports: 9 Imported by: 36

README

Ask

Ask is a small CLI building package for Go, which enables you to define commands as datat-types, without requiring full initialization upfront. This makes it suitable for shell applications, and CLIs with dynamic commands or just too many to load at once.

It has minimal dependencies: only github.com/spf13/pflag for excellent and familiar flag parsing (Cobra CLI is powered with these flags).

Warning: this is a new experimental package, built to improve the Rumor shell.

Usage

// load a command struct
cmd, err := Load(MyCommandStruct{})

// Execute a command
cmd, isHelp, err := cmd.Execute(context.Background(), "some", "args", "--here", "use", "a", "shell", "parser")

Thanks to pflag, all basic types, slices (well, work in progress), and some misc network types are supported for flags.

You can also implement the pflag.Value interface for custom flag parsing.

To define a command, implement Command and/or RouteCommand:

func (c *Command) Run(ctx context.Context, args ...string) error { ... }

func (c *MyHubCommand) Get(ctx context.Context, args ...string) (cmd interface{}, remaining []string, err error) { ... }

For additional help information, a command can also implement Help:

func (c Connect) Help() string { ... }

Example

package main

import (
    "context"
    "fmt"
    "net"
    "strings"
    "testing"
)

type ActorState struct {
    HostData string
}

type Peer struct {
    State *ActorState
}

func (c *Peer) Get(ctx context.Context, args ...string) (cmd interface{}, remaining []string, err error) {
    switch args[0] {
    case "connect":
        return &Connect{Parent: c, State: c.State}, args[1:], nil
    default:
        return nil, args, NotRecognizedErr
    }
}

type Connect struct {
    Parent *Peer
    State  *ActorState
    Addr   net.IP `ask:"--addr" help:"address to connect to"`
    Port   uint16 `ask:"--port" help:"port to use for connection"`
    Tag    string `ask:"--tag" help:"tag to give to peer"`
    Data   uint8  `ask:"<data>" help:"some number"`
    PeerID string `ask:"<id>" help:"libp2p ID of the peer, if no address is specified, the peer is looked up in the peerstore"`
    More   string `ask:"[more]" help:"optional"`
}

func (c Connect) Help() string {
    return "connect to a peer"
}

func (c *Connect) Run(ctx context.Context, args ...string) error {
    c.State.HostData = fmt.Sprintf("addr: %s:%d #%s $%d %s ~ %s, remaining: %s",
        c.Addr.String(), c.Port, c.Tag, c.Data, c.PeerID, c.More, strings.Join(args, ", "))
    return nil
}

func main() {
    state := ActorState{
        HostData: "old value",
    }
    defaultPeer := Peer{State: &state}
    cmd, err := Load(&defaultPeer)
    if err != nil {
        t.Fatal(err)
    }

    // Execute returns the final command that is executed,
    // to get the subcommands in case usage needs to be printed, or other result data is required.
    cmd, isHelp, err := cmd.Execute(context.Background(),
        strings.Split("connect --addr 1.2.3.4 --port=4000 --tag=123hey 42 someid optionalhere extra more", " ")...)
    // handle err
    if err == nil {
        panic(err)
    }
    if isHelp {
        // print usage if the user asks --help
        fmt.Println(cmd.Usage("connect"))
    }

    // use resulting state change
    // state.HostData == "1.2.3.4:4000 #123hey $42 someid ~ optionalhere, remaining: extra, more"
}

License

MIT, see LICENSE file.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var InvalidCmdTypeErr = errors.New("command type is not supported")
View Source
var NotRecognizedErr = errors.New("command was not recognized")

Functions

This section is empty.

Types

type Command

type Command interface {
	// Run the command, with context and remaining unrecognized args
	Run(ctx context.Context, args ...string) error
}

type CommandDescription

type CommandDescription struct {
	Help     string
	FlagsSet *pflag.FlagSet
	// Flags that can be passed as positional required args
	RequiredArgs []string
	// Flags that can be passed as positional optional args
	OptionalArgs []string
	// Command to run, may be nil if nothing has to run
	Command
	// Sub-command routing, can create commands (or other sub-commands) to access, may be nil if no sub-commands
	CommandRoute
}

An interface{} can be loaded as a command-description to execute it. See Load()

func Load

func Load(val interface{}) (*CommandDescription, error)

Load takes a structure instance that defines a command through its type, and the default values by determining them from the actual type.

func LoadReflect

func LoadReflect(val reflect.Value) (*CommandDescription, error)

LoadReflect is the same as Load, but directly using reflection to handle the value.

func (*CommandDescription) Execute

func (descr *CommandDescription) Execute(ctx context.Context, args ...string) (final *CommandDescription, isHelp bool, err error)

Runs the command, with given context and arguments. The final sub-command that actually runs is returned, and may be nil in case of an error. The "isHelp" will be true if help information was requested for the command (through `help`, `--help` or `-h`) To add inputs/outputs such as STDOUT to a command, they can be added as field in the command struct definition, and the command can pass them on to sub-commands. Similarly logging and other misc. data can be passed around. The execute parameters are kept minimal.

func (*CommandDescription) Load

func (descr *CommandDescription) Load(val interface{}) error

Load adds more flags/args/meta to the command description. It recursively goes into the field if it's tagged with `ask:"."`, or if it's an embedded field. (recurse depth-first) It skips the field explicitly if it's tagged with `ask:"-"` (used to ignore embedded fields) Multiple target values can be loaded if they do not conflict, the first Command and CommandRoute found will be used. The flags will be set over all loaded values.

func (*CommandDescription) LoadField

func (descr *CommandDescription) LoadField(f reflect.StructField, val reflect.Value) (requiredArg, optionalArg string, err error)

Check the struct field, and add flag for it if asked for

func (*CommandDescription) LoadReflect

func (descr *CommandDescription) LoadReflect(val reflect.Value) error

LoadReflect is the same as Load, but directly using reflection to handle the value.

func (*CommandDescription) Usage

func (descr *CommandDescription) Usage(name string) string

Usage prints the help information and the usage of all flags.

type CommandRoute

type CommandRoute interface {
	// Get a subcommand, which can be a Command or CommandRoute
	// The remaining arguments are passed to the subcommand on execution
	// The command that is returned will be loaded with `Load` before it runs
	Get(ctx context.Context, args ...string) (cmd interface{}, remaining []string, err error)
}

type Help

type Help interface {
	// Help explains how a command is used.
	Help() string
}

Optionally specify how to get help information (usage of flags is added to this when called through CommandDescription.Usage() )

Jump to

Keyboard shortcuts

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