command

package
v0.0.0-...-604993b Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2023 License: ISC Imports: 15 Imported by: 0

Documentation

Overview

Package command generates (sub)commands from generic function signatures.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Command

type Command interface {
	// Name returns a human friendly name of the command,
	// which may be used to identify commands
	// as well as decorate user facing help-text.
	Name() string

	// Synopsis returns a single-line short string describing the command.
	Synopsis() string

	// Usage returns an arbitrarily long string explaining how to use the command.
	Usage() string

	// Subcommands returns a list of subcommands (if any).
	Subcommands() []Command

	// Execute executes the command, with or without any arguments.
	Execute(ctx context.Context, args ...string) error
}

Command is a decorated function ready to be executed.

func MakeFixedCommand

func MakeFixedCommand[
	ET ExecuteType[T],
	EC ExecuteMonadic[ET, T],
	T any,
](
	name, synopsis, usage string,
	executeFn EC, options ...Option,
) Command

MakeFixedCommand wraps a function which accepts either ExecuteType or, ExecuteType and variadic string parameters, which are passed to `executeFn` during command.Execute.

Example

MakeFixedCommand can be used to construct commands that expect a specific fixed type, and optionally, variadic arguments.

package main

import (
	"context"
	"flag"
	"fmt"
	"os"

	"github.com/djdv/go-filesystem-utils/internal/command"
)

// fixedType is the type our execute function
// expects to always receive when it's called.
// The type will already be populated with default
// values or values parsed from the arguments passed
// to [command.Execute].
type fixedType struct {
	someField int
}

// BindFlags initializes default values for our type
// and gives the [flag] package the ability to overwrite
// them when parsing flags.
func (ft *fixedType) BindFlags(flagSet *flag.FlagSet) {
	const (
		flagName    = "flag"
		flagUsage   = "an example flag"
		flagDefault = 1
	)
	flagSet.IntVar(&ft.someField, flagName, flagDefault, flagUsage)
}

// MakeFixedCommand can be used to construct
// commands that expect a specific fixed type,
// and optionally, variadic arguments.
func main() {
	var (
		cmd     = newFixedCommand()
		cmdArgs = newFixedArgsCommand()
		ctx     = context.TODO()
	)
	if err := cmd.Execute(ctx); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
	if err := cmd.Execute(ctx, "-flag=2"); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
	if err := cmdArgs.Execute(ctx, "-flag=3"); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
	if err := cmdArgs.Execute(ctx, "-flag=4", "a", "b", "c"); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
}

func newFixedCommand() command.Command {
	const (
		name     = "fixed"
		synopsis = "Prints a value."
		usage    = "Call the command with or" +
			" without flags"
	)
	return command.MakeFixedCommand[*fixedType](
		name, synopsis, usage,
		fixedExecute,
	)
}

func fixedExecute(ctx context.Context, settings *fixedType) error {
	fmt.Printf("settings.someField: %d\n", settings.someField)
	return nil
}

func newFixedArgsCommand() command.Command {
	const (
		name     = "fixed-args"
		synopsis = "Prints a value and arguments."
		usage    = "Call the command with or" +
			" without flags or arguments"
	)
	return command.MakeFixedCommand[*fixedType](
		name, synopsis, usage,
		fixedExecuteArgs,
	)
}

func fixedExecuteArgs(ctx context.Context, settings *fixedType, arguments ...string) error {
	if err := fixedExecute(ctx, settings); err != nil {
		return nil
	}
	if len(arguments) > 0 {
		fmt.Printf("arguments: %v\n", arguments)
	}
	return nil
}
Output:

settings.someField: 1
settings.someField: 2
settings.someField: 3
settings.someField: 4
arguments: [a b c]

func MakeNiladicCommand

func MakeNiladicCommand(
	name, synopsis, usage string,
	executeFn ExecuteNiladicFunc,
	options ...Option,
) Command

MakeNiladicCommand returns a command that wraps `executeFn`.

Example

MakeNiladicCommand can be used to construct basic commands that don't expect additional parameters to be passed to their execute function.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/djdv/go-filesystem-utils/internal/command"
)

// MakeNiladicCommand can be used to construct
// basic commands that don't expect additional
// parameters to be passed to their execute function.
func main() {
	var (
		cmd = newNiladicCommand()
		ctx = context.TODO()
	)
	if err := cmd.Execute(ctx); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
}

func newNiladicCommand() command.Command {
	const (
		name     = "niladic"
		synopsis = "Prints a message."
		usage    = "Call the command with no arguments"
	)
	return command.MakeNiladicCommand(
		name, synopsis, usage,
		niladicExecute,
	)
}

func niladicExecute(context.Context) error {
	fmt.Println("hello!")
	return nil
}
Output:

hello!

func MakeVariadicCommand

func MakeVariadicCommand[
	TS ~[]T,
	ET ExecuteType[TS],
	EC ExecuteVariadic[ET, TS, T],
	T any,
](
	name, synopsis, usage string,
	executeFn EC, options ...Option,
) Command

MakeVariadicCommand wraps a function which accepts either variaic ExecuteType or, a slice of string parameters and variadic ExecuteType which are passed to `executeFn` during command.Execute.

Example

MakeVariadicCommand can be used to construct commands that expect a variable amount of parameters.

package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"strconv"

	"github.com/djdv/go-filesystem-utils/internal/command"
)

type (
	// settings is the type our execute function
	// will construct on its own.
	settings struct {
		someField int
	}
	// Execute expects to receive a list of `option`s
	// that it will apply to the [settings] struct.
	option func(*settings) error
	// options holds individual [option] values
	// and also satisfies the [command.ExecuteType] constraint.
	options []option
)

const variadicFlagDefault = 1

// BindFlags gives the [flag] package the ability to
// append options to our list during flag parsing.
func (ol *options) BindFlags(flagSet *flag.FlagSet) {
	const (
		flagName  = "flag"
		flagUsage = "an example flag"
	)
	flagSet.Func(flagName, flagUsage, func(parameter string) error {
		parsedValue, err := strconv.Atoi(parameter)
		if err != nil {
			return err
		}
		*ol = append(*ol, func(settings *settings) error {
			settings.someField = parsedValue
			return nil
		})
		return nil
	})
}

// MakeVariadicCommand can be used to construct
// commands that expect a variable amount of parameters.
func main() {
	var (
		cmd     = newVariadicCommand()
		cmdArgs = newVariadicArgsCommand()
		ctx     = context.TODO()
	)
	if err := cmd.Execute(ctx); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
	if err := cmd.Execute(ctx, "-flag=2"); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
	if err := cmdArgs.Execute(ctx, "-flag=3"); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
	if err := cmdArgs.Execute(ctx, "-flag=4", "a", "b", "c"); err != nil {
		fmt.Fprint(os.Stderr, err)
		return
	}
}

func newVariadicCommand() command.Command {
	const (
		name     = "variadic"
		synopsis = "Prints a value."
		usage    = "Call the command with or" +
			" without flags"
	)
	return command.MakeVariadicCommand[options](
		name, synopsis, usage,
		variadicExecute,
	)
}

func variadicExecute(ctx context.Context, options ...option) error {
	settings := settings{
		someField: variadicFlagDefault,
	}
	for _, apply := range options {
		if err := apply(&settings); err != nil {
			return err
		}
	}
	fmt.Printf("settings.someField: %d\n", settings.someField)
	return nil
}

func newVariadicArgsCommand() command.Command {
	const (
		name     = "variadic-args"
		synopsis = "Prints a value and arguments."
		usage    = "Call the command with or" +
			" without flags or arguments"
	)
	return command.MakeVariadicCommand[options](
		name, synopsis, usage,
		variadicExecuteArgs,
	)
}

func variadicExecuteArgs(ctx context.Context, arguments []string, options ...option) error {
	if err := variadicExecute(ctx, options...); err != nil {
		return nil
	}
	if len(arguments) > 0 {
		fmt.Printf("arguments: %v\n", arguments)
	}
	return nil
}
Output:

settings.someField: 1
settings.someField: 2
settings.someField: 3
settings.someField: 4
arguments: [a b c]

func SubcommandGroup

func SubcommandGroup(name, synopsis string, subcommands []Command, options ...Option) Command

SubcommandGroup returns a command that only defers to subcommands. Trying to execute the command itself will return UsageError.

Example

Subcommand groups can be useful for defining a section of related commands under a single named group.

package main

import (
	"context"
	"os"

	"github.com/djdv/go-filesystem-utils/internal/command"
)

// Subcommand groups can be useful for defining
// a section of related commands under a single
// named group.
func main() {
	var (
		cmd = newSubcommands()
		ctx = context.TODO()
	)
	// NOTE: text rendering is disabled
	// for `go test`'s output comparison.
	// Normally this can be omitted.
	const (
		helpFlag   = "-help"
		renderFlag = "-video-terminal=false"
	)
	cmd.Execute(ctx, helpFlag, renderFlag)
	cmd.Execute(ctx, "alphabets", helpFlag, renderFlag)
	cmd.Execute(ctx, "numerals", helpFlag, renderFlag)
}

func newSubcommands() command.Command {
	var (
		noopFn      = func(context.Context) error { return nil }
		makeCommand = func(name string) command.Command {
			var (
				synopsis = name + " synopsis"
				usage    = name + " usage"
			)
			return command.MakeNiladicCommand(
				name, synopsis, usage, noopFn,
			)
		}
		// Printer output defaults to [os.Stderr].
		// We set it here only because `go test`
		// compares against [os.Stdout].
		output     = os.Stdout
		cmdOptions = []command.Option{
			command.WithUsageOutput(output),
		}
	)
	return command.SubcommandGroup(
		"main", "Top level group",
		[]command.Command{
			command.SubcommandGroup(
				"alphabets", "Letter group.",
				[]command.Command{
					makeCommand("a"),
					makeCommand("b"),
					makeCommand("c"),
				},
				cmdOptions...,
			),
			command.SubcommandGroup(
				"numerals", "Number group.",
				[]command.Command{
					makeCommand("1"),
					makeCommand("2"),
					makeCommand("3"),
				},
				cmdOptions...,
			),
		},
		cmdOptions...,
	)
}
Output:

Must be called with a subcommand.

Usage:
	main subcommand [flags]
Flags:
  -help
    	prints out this help text
  -video-terminal
    	render text for video terminals
    	(default: true)
Subcommands:
  alphabets - Letter group.
  numerals  - Number group.

Must be called with a subcommand.

Usage:
	alphabets subcommand [flags]
Flags:
  -help
    	prints out this help text
  -video-terminal
    	render text for video terminals
    	(default: true)
Subcommands:
  a - a synopsis
  b - b synopsis
  c - c synopsis

Must be called with a subcommand.

Usage:
	numerals subcommand [flags]
Flags:
  -help
    	prints out this help text
  -video-terminal
    	render text for video terminals
    	(default: true)
Subcommands:
  1 - 1 synopsis
  2 - 2 synopsis
  3 - 3 synopsis

type ExecuteArgumentsVariadicFunc

type ExecuteArgumentsVariadicFunc[
	ET ExecuteType[ST],
	ST ~[]VT,
	VT any,
] interface {
	func(context.Context, []string, ...VT) error
}

ExecuteArgumentsVariadicFunc is a variant of ExecuteVariadicFunc that also accepts arguments.

type ExecuteDyadicFunc

type ExecuteDyadicFunc[
	ET ExecuteType[T],
	T any,
] interface {
	func(context.Context, ET, ...string) error
}

ExecuteDyadicFunc is a variant of ExecuteMonadicFunc that also accepts variadic arguments.

type ExecuteMonadic

type ExecuteMonadic[
	ET ExecuteType[T],
	T any,
] interface {
	ExecuteMonadicFunc[ET, T] |
		ExecuteDyadicFunc[ET, T]
}

ExecuteMonadic permits functions with these signatures.

type ExecuteMonadicFunc

type ExecuteMonadicFunc[
	ET ExecuteType[T],
	T any,
] interface {
	func(context.Context, ET) error
}

ExecuteMonadicFunc is a variant of ExecuteNiladicFunc that also accepts an ExecuteType.

type ExecuteNiladicFunc

type ExecuteNiladicFunc func(context.Context) error

ExecuteNiladicFunc may be used as a command's Execute function.

type ExecuteType

type ExecuteType[T any] interface {
	*T
	FlagBinder
}

ExecuteType is a constraint that permits any reference type that can bind its value(s) to flags.

type ExecuteVariadic

type ExecuteVariadic[
	ET ExecuteType[ST],
	ST ~[]VT,
	VT any,
] interface {
	ExecuteVariadicFunc[ET, ST, VT] |
		ExecuteArgumentsVariadicFunc[ET, ST, VT]
}

ExecuteVariadic permits functions with these signatures.

type ExecuteVariadicFunc

type ExecuteVariadicFunc[
	ET ExecuteType[ST],
	ST ~[]VT,
	VT any,
] interface {
	func(context.Context, ...VT) error
}

ExecuteVariadicFunc is a variant of ExecuteNiladicFunc that also accepts a variadic ExecuteType.

type FlagBinder

type FlagBinder interface {
	BindFlags(*flag.FlagSet)
}

A FlagBinder should call relevant flag.FlagSet methods to bind each of it's variable references with the FlagSet. E.g. a struct would pass references of its fields to `FlagSet.Var(&structABC.fieldXYZ, ...)`.

type Option

type Option func(*commandCommon)

Option is a functional option. One can be returned by the various constructors before being passed to [MakeCommand].

func WithSubcommands

func WithSubcommands(subcommands ...Command) Option

WithSubcommands provides a command with subcommands. Subcommands will be called if the supercommand receives arguments that match the subcommand name.

func WithUsageOutput

func WithUsageOutput(output io.Writer) Option

WithUsageOutput sets the writer that is written to when [Command.Execute] receives a request for help, or returns UsageError.

type UsageError

type UsageError struct{ Err error }

UsageError may be returned by commands to signal that its usage string should be presented to the caller.

func (UsageError) Error

func (ue UsageError) Error() string

func (UsageError) Unwrap

func (ue UsageError) Unwrap() error

Unwrap implements the errors.Unwrap interface.

type ValueNamer

type ValueNamer interface {
	Name() string
}

ValueNamer may be implemented by a flag.Value to specify the name of its parameter type, but is only used if the name is absent in the usage string.

Jump to

Keyboard shortcuts

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