ask

package module
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2021 License: MIT Imports: 10 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).

In addition, some special array/slice types are supported:

  • []byte as hex-encoded string, case-insensitive, optional 0x prefix and padding
  • [N]byte, same as above, but an array
  • [][N]byte, a comma-separated list of elements, each formatted like the above.

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

Note: flags in between command parts, e.g. peer --foobar connect are not supported, but may be in the future.

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 CommandRoute:

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

Cmd(route string) (cmd interface{}, err error)

To hint at sub-command routes, implement CommandKnownRoutes:

Routes() []string

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

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

The help information, along usage info (flag set info + default values + sub commands list) can be retrieved from .Usage() after Load()-ing the command.

For default options that are not "" or 0 or other Go defaults, the Default() interface can be implemented on a command, to set its flag values during Load().

Flags being changed can be detected by declaring a separate bool type field, with changed:"flagnamehere" to reference the flag (without flag prefix like --) which may be changed or not.

Example

package main

import (
    "context"
    "fmt"
    "github.com/protolambda/ask"
    "net"
    "strings"
    "testing"
)

type ActorState struct {
    HostData string
}

type Peer struct {
    *ActorState
}

func (c *Peer) Cmd(route string) (cmd interface{}, err error) {
    switch route {
    case "connect":
        return &Connect{ActorState: c.ActorState}, nil
    default:
        return nil, ask.UnrecognizedErr
    }
}

func (c *Peer) Routes() []string {
    return []string{"connect"}
}

type Connect struct {
    // Do not embed the parent command, or Connect will be recognized as a command route.
    // If recursive commands are desired, the command route can return a nil command
    // if the command itself should be evaluated as a normal command instead.
    *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"`

	PortIsSet bool      `changed:"port"`
	AddrIsSet bool      `changed:"addr"`
}

func (c *Connect) Default() {
    c.Port = 9000
}

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

func (c *Connect) Run(ctx context.Context, args ...string) error {
    // c.PortIsSet == false
    // c.AddrIsSet == true
    c.HostData = fmt.Sprintf("%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{ActorState: &state}
    cmd, err := ask.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 --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())
    }

    // 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 UnrecognizedErr = errors.New("command was not recognized")

Functions

func AddFlag added in v0.0.2

func AddFlag(flags *pflag.FlagSet, typ reflect.Type, val reflect.Value, name string, shorthand string, help string) error

Types

type BytesHexFlag added in v0.0.2

type BytesHexFlag []byte

BytesHex exposes bytes as a flag, hex-encoded, optional whitespace padding, case insensitive, and optional 0x prefix.

func (*BytesHexFlag) Set added in v0.0.2

func (f *BytesHexFlag) Set(value string) error

func (BytesHexFlag) String added in v0.0.2

func (f BytesHexFlag) String() string

func (*BytesHexFlag) Type added in v0.0.2

func (f *BytesHexFlag) Type() string

type ChangedMarker added in v0.0.5

type ChangedMarker struct {
	// Dest is the boolean which is set to true if the flag was changed from default, false otherwise.
	Dest *bool
	// The name of the flag which is changed or not
	Name string
}

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 {
	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
	// ChangedMarkers tracks which flags are changed.
	// Define a field as 'MySettingChanged bool `changed:"my-setting"`' to e.g. track '--my-setting' being changed.
	ChangedMarkers []ChangedMarker
	// 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
	// Help Information as provided by the Help interface
	Help
}

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:"."` (recurse depth-first). Embedded fields are handled as regular fields unless explicitly squashed. It skips the field explicitly if it's tagged with `ask:"-"` 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() string

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

type CommandKnownRoutes added in v0.0.2

type CommandKnownRoutes interface {
	// Routes lists the sub-commands that can be asked from Get.
	Routes() []string
}

CommandKnownRoutes may be implemented by a CommandRoute to declare which routes are accessible, useful for e.g. help messages to give more information for each of the subcommands.

type CommandRoute

type CommandRoute interface {
	// Cmd gets a sub-command, which can be a Command or CommandRoute
	// The command that is returned will be loaded with `Load` before it runs or its subcommand is retrieved.
	// Return nil if the command route should be ignored, e.g. if this route is also a regular command with arguments.
	Cmd(route string) (cmd interface{}, 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() )

type InitDefault added in v0.0.2

type InitDefault interface {
	// Default the flags of a command.
	Default()
}

InitDefault can be implemented by a command to not rely on the parent command initializing the command correctly, and instead move the responsibility to the command itself. The default is initialized during Load, and a command may embed multiple sub-structures that implement Default.

Jump to

Keyboard shortcuts

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