acmd

package module
v0.11.2 Latest Latest
Warning

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

Go to latest
Published: Nov 28, 2023 License: MIT Imports: 10 Imported by: 20

README

acmd

build-img pkg-img reportcard-img coverage-img version-img

Simple, useful and opinionated CLI package in Go. For config loader see aconfig

Rationale

Popular CLI libraries (or better frameworks) have too large and unclear API, in most cases, you just want to define commands for your CLI application and run them without additional work. This package does this by providing a small API, good defaults and clear code.

Features

  • Simple API.
  • Dependency-free.
  • Clean and tested code.
  • Command aliases.
  • Auto suggesting command.
  • Builtin help and version commands.

Install

Go version 1.17+

go get github.com/cristalhq/acmd

Example

cmds := []acmd.Command{
	{
		Name:        "now",
		Description: "prints current time",
		ExecFunc: func(ctx context.Context, args []string) error {
			fmt.Printf("now: %s\n", now.Format("15:04:05"))
			return nil
		},
	},
	{
		Name:        "status",
		Description: "prints status of the system",
		ExecFunc: func(ctx context.Context, args []string) error {
			// do something with ctx :)
			return nil
		},
	},
}

// all the acmd.Config fields are optional
r := acmd.RunnerOf(cmds, acmd.Config{
	AppName:        "acmd-example",
	AppDescription: "Example of acmd package",
	Version:        "the best v0.x.y",
	// Context - if nil `signal.Notify` will be used
	// Args - if nil `os.Args[1:]` will be used
	// Usage - if nil default print will be used
})

if err := r.Run(); err != nil {
	r.Exit(err)
}

See examples: example_test.go.

Documentation

See these docs or GUIDE.md for more details.

License

MIT License.

Documentation

Overview

Package acmd is a simple, useful and opinionated CLI package in Go.

See: https://github.com/cristalhq/acmd

Example (Alias)
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/cristalhq/acmd"
)

var nopUsage = func(cfg acmd.Config, cmds []acmd.Command) {}

func main() {
	testOut := os.Stdout
	testArgs := []string{"someapp", "f"}

	cmds := []acmd.Command{
		{
			Name:  "foo",
			Alias: "f",
			ExecFunc: func(ctx context.Context, args []string) error {
				fmt.Fprint(testOut, "foo")
				return nil
			},
		},
		{
			Name:  "bar",
			Alias: "b",
			ExecFunc: func(ctx context.Context, args []string) error {
				fmt.Fprint(testOut, "bar")
				return nil
			},
		},
	}

	r := acmd.RunnerOf(cmds, acmd.Config{
		AppName:        "acmd-example",
		AppDescription: "Example of acmd package",
		Version:        "the best v0.x.y",
		Output:         testOut,
		Args:           testArgs,
		Usage:          nopUsage,
	})

	if err := r.Run(); err != nil {
		panic(err)
	}

}
Output:

foo
Example (Autosuggestion)
package main

import (
	"context"
	"os"

	"github.com/cristalhq/acmd"
)

var (
	nopFunc  = func(context.Context, []string) error { return nil }
	nopUsage = func(cfg acmd.Config, cmds []acmd.Command) {}
)

func main() {
	testOut := os.Stdout
	testArgs := []string{"someapp", "baz"}

	cmds := []acmd.Command{
		{Name: "foo", ExecFunc: nopFunc},
		{Name: "bar", ExecFunc: nopFunc},
	}

	r := acmd.RunnerOf(cmds, acmd.Config{
		AppName:        "acmd-example",
		AppDescription: "Example of acmd package",
		Version:        "the best v0.x.y",
		Output:         testOut,
		Args:           testArgs,
		Usage:          nopUsage,
	})

	if err := r.Run(); err == nil {
		panic("must fail with command not found")
	}

}
Output:

"baz" unknown command, did you mean "bar"?
Run "acmd-example help" for usage.
Example (ExecStruct)
package main

import (
	"context"
	"errors"
	"fmt"
	"io"

	"github.com/cristalhq/acmd"
)

var nopUsage = func(cfg acmd.Config, cmds []acmd.Command) {}

type myCommand struct {
	ErrToReturn error
}

func (mc *myCommand) ExecCommand(ctx context.Context, args []string) error {
	return mc.ErrToReturn
}

func main() {
	myErr := errors.New("everything is ok")
	myCmd := &myCommand{ErrToReturn: myErr}

	cmds := []acmd.Command{
		{
			Name:        "what",
			Description: "does something",

			// ExecFunc:    myCmd.ExecCommand,
			// NOTE: line below is literally line above
			Exec: myCmd,
		},
	}

	r := acmd.RunnerOf(cmds, acmd.Config{
		AppName:         "acmd-example",
		AppDescription:  "Example of acmd package",
		PostDescription: "Best place to add examples",
		Output:          io.Discard,
		Args:            []string{"someapp", "what"},
		Usage:           nopUsage,
	})

	err := r.Run()
	if !errors.Is(err, myErr) {
		panic(fmt.Sprintf("\ngot : %+v\nwant: %+v\n", err, myErr))
	}

}
Output:

Example (NestedCommands)
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/cristalhq/acmd"
)

var nopFunc = func(context.Context, []string) error { return nil }

func main() {
	testOut := os.Stdout
	testArgs := []string{"someapp", "foo", "qux"}

	cmds := []acmd.Command{
		{
			Name: "foo",
			Subcommands: []acmd.Command{
				{Name: "bar", ExecFunc: nopFunc},
				{Name: "baz", ExecFunc: nopFunc},
				{
					Name: "qux",
					ExecFunc: func(ctx context.Context, args []string) error {
						fmt.Fprint(testOut, "qux")
						return nil
					},
				},
			},
		},
		{Name: "boom", ExecFunc: nopFunc},
	}

	r := acmd.RunnerOf(cmds, acmd.Config{
		AppName:        "acmd-example",
		AppDescription: "Example of acmd package",
		Version:        "the best v0.x.y",
		Output:         testOut,
		Args:           testArgs,
	})

	if err := r.Run(); err != nil {
		panic(err)
	}

}
Output:

qux
Example (PropagateFlags)
package main

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

	"github.com/cristalhq/acmd"
)

func main() {
	testOut := os.Stdout
	testArgs := []string{"someapp", "foo", "-dir=test-dir", "--verbose"}
	buf := &bytes.Buffer{}

	cmds := []acmd.Command{
		{
			Name: "foo", ExecFunc: func(ctx context.Context, args []string) error {
				var cfg generalFlags
				if err := cfg.Flags().Parse(args); err != nil {
					return err
				}
				if cfg.IsVerbose {
					fmt.Fprintf(buf, "TODO: dir %q, is verbose = %v\n", cfg.Dir, cfg.IsVerbose)
				}
				return nil
			},
		},
		{
			Name: "bar", ExecFunc: func(ctx context.Context, args []string) error {
				var cfg commandFlags
				if err := cfg.Flags().Parse(args); err != nil {
					return err
				}
				if cfg.IsVerbose {
					fmt.Fprintf(buf, "TODO: dir %q\n", cfg.Dir)
				}
				return nil
			},
		},
	}

	r := acmd.RunnerOf(cmds, acmd.Config{
		AppName:        "acmd-example",
		AppDescription: "Example of acmd package",
		Version:        "the best v0.x.y",
		Output:         testOut,
		Args:           testArgs,
	})

	if err := r.Run(); err != nil {
		panic(err)
	}
	fmt.Println(buf.String())

}

type generalFlags struct {
	IsVerbose bool
	Dir       string
}

func (c *generalFlags) Flags() *flag.FlagSet {
	fs := flag.NewFlagSet("", flag.ContinueOnError)
	fs.BoolVar(&c.IsVerbose, "verbose", false, "should app be verbose")
	fs.StringVar(&c.Dir, "dir", ".", "directory to process")
	return fs
}

type commandFlags struct {
	generalFlags
	File string
}

func (c *commandFlags) Flags() *flag.FlagSet {
	fs := c.generalFlags.Flags()
	fs.StringVar(&c.File, "file", "input.txt", "file to process")
	return fs
}
Output:

TODO: dir "test-dir", is verbose = true
Example (VerboseHelp)
package main

import (
	"context"
	"flag"
	"os"

	"github.com/cristalhq/acmd"
)

var nopFunc = func(context.Context, []string) error { return nil }

func main() {
	testOut := os.Stdout
	testArgs := []string{"someapp", "help"}

	cmds := []acmd.Command{
		{
			Name:        "now",
			Description: "prints current time",
			ExecFunc:    nopFunc,
		},
		{
			Name:        "status",
			Description: "prints status of the system",
			ExecFunc:    nopFunc,
		},
		{
			Name:     "boom",
			ExecFunc: nopFunc,
			FlagSet:  &generalFlags{},
		},
		{
			Name: "time", Subcommands: []acmd.Command{
				{Name: "next", ExecFunc: nopFunc, Description: "next time subcommand"},
				{Name: "curr", ExecFunc: nopFunc, Description: "curr time subcommand"},
			},
		},
	}

	r := acmd.RunnerOf(cmds, acmd.Config{
		AppName:         "acmd-example",
		AppDescription:  "Example of acmd package",
		PostDescription: "Best place to add examples.",
		Version:         "the best v0.x.y",
		Output:          testOut,
		Args:            testArgs,
		VerboseHelp:     !true, // TODO(cristaloleg): fix this
	})

	if err := r.Run(); err != nil {
		panic(err)
	}

}

type generalFlags struct {
	IsVerbose bool
	Dir       string
}

func (c *generalFlags) Flags() *flag.FlagSet {
	fs := flag.NewFlagSet("", flag.ContinueOnError)
	fs.BoolVar(&c.IsVerbose, "verbose", false, "should app be verbose")
	fs.StringVar(&c.Dir, "dir", ".", "directory to process")
	return fs
}
Output:

Example of acmd package

Usage:

    acmd-example <command> [arguments...]

The commands are:

    boom                <no description>
    help                shows help message
    now                 prints current time
    status              prints status of the system
    time curr           curr time subcommand
    time next           next time subcommand
    version             shows version of the application

Best place to add examples.

Version: the best v0.x.y
Example (Version)
package main

import (
	"context"
	"os"

	"github.com/cristalhq/acmd"
)

var (
	nopFunc  = func(context.Context, []string) error { return nil }
	nopUsage = func(cfg acmd.Config, cmds []acmd.Command) {}
)

func main() {
	testOut := os.Stdout
	testArgs := []string{"someapp", "version"}

	cmds := []acmd.Command{
		{Name: "foo", ExecFunc: nopFunc},
		{Name: "bar", ExecFunc: nopFunc},
	}

	r := acmd.RunnerOf(cmds, acmd.Config{
		AppName:        "acmd-example",
		AppDescription: "Example of acmd package",
		Version:        "the best v0.x.y",
		Output:         testOut,
		Args:           testArgs,
		Usage:          nopUsage,
	})

	if err := r.Run(); err != nil {
		panic(err)
	}

}
Output:

acmd-example version: the best v0.x.y

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNoArgs = errors.New("no args provided")

Functions

func AutocompleteFor added in v0.5.4

func AutocompleteFor(cmds []Command) (string, error)

func HasHelpFlag added in v0.5.1

func HasHelpFlag(flags []string) bool

HasHelpFlag reports whether help flag is presented in args.

Types

type Command

type Command struct {
	// Name of the command, ex: `init`
	Name string

	// Alias is an optional short second name, ex: `i`.
	Alias string

	// Description of the command.
	Description string

	// ExecFunc represents the command function.
	// Use Exec if you have struct implementing this function.
	ExecFunc func(ctx context.Context, args []string) error

	// Exec represents the command function.
	// Will be used only if ExecFunc is nil.
	Exec Exec

	// Subcommands of the command.
	Subcommands []Command

	// IsHidden reports whether command should not be show in help. Default false.
	IsHidden bool

	// FlagSet is an optional field where you can provide command's flags.
	// Is used for autocomplete. Works best with https://github.com/cristalhq/flagx
	FlagSet FlagsGetter
}

Command specifies a sub-command for a program's command-line interface.

type Config

type Config struct {
	// AppName is an optional name for the app, if empty os.Args[0] will be used.
	AppName string

	// AppDescription is an optional description. default is empty.
	AppDescription string

	// PostDescription of the command. Is shown after a help.
	PostDescription string

	// Version of the application.
	Version string

	// Output is a destination where result will be printed.
	// Exported for testing purpose only, if nil os.Stdout is used.
	Output io.Writer

	// Context for commands, if nil context based on os.Interrupt and syscall.SIGTERM will be used.
	Context context.Context

	// Args passed to the executable, if nil os.Args[1:] will be used.
	Args []string

	// Usage of the application, if nil default will be used.
	Usage func(cfg Config, cmds []Command)

	// VerboseHelp if "./app help -v" is passed, default is false.
	VerboseHelp bool
	// contains filtered or unexported fields
}

Config for the runner.

type ErrCode added in v0.5.3

type ErrCode int

ErrCode is a number to be returned as an exit code.

func (ErrCode) Error added in v0.5.3

func (e ErrCode) Error() string

type Exec added in v0.8.0

type Exec interface {
	ExecCommand(ctx context.Context, args []string) error
}

Exec represents a command to run.

type FlagsGetter added in v0.9.1

type FlagsGetter interface {
	Flags() *flag.FlagSet
}

FlagsGetter returns flags for the command. See examples.

type Runner

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

Runner of the sub-commands.

Example
package main

import (
	"context"
	"flag"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/cristalhq/acmd"
)

var nopUsage = func(cfg acmd.Config, cmds []acmd.Command) {}

func main() {
	testOut := os.Stdout
	testArgs := []string{"someapp", "now", "--times", "3"}

	const format = "15:04:05"
	now, _ := time.Parse(format, "10:20:30")

	cmds := []acmd.Command{
		{
			Name:        "now",
			Description: "prints current time",
			ExecFunc: func(ctx context.Context, args []string) error {
				fs := flag.NewFlagSet("some name for help", flag.ContinueOnError)
				times := fs.Int("times", 1, "how many times to print time")
				if err := fs.Parse(args); err != nil {
					return err
				}

				for i := 0; i < *times; i++ {
					fmt.Printf("now: %s\n", now.Format(format))
				}
				return nil
			},
			FlagSet: &commandFlags{},
		},
		{
			Name:        "status",
			Description: "prints status of the system",
			ExecFunc: func(ctx context.Context, args []string) error {
				req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.githubstatus.com/", http.NoBody)
				resp, err := http.DefaultClient.Do(req)
				if err != nil {
					return err
				}
				defer resp.Body.Close()
				// TODO: parse response, I don't know
				return nil
			},
		},
	}

	r := acmd.RunnerOf(cmds, acmd.Config{
		AppName:         "acmd-example",
		AppDescription:  "Example of acmd package",
		PostDescription: "Best place to add examples",
		Version:         "the best v0.x.y",
		Output:          testOut,
		Args:            testArgs,
		Usage:           nopUsage,
	})

	if err := r.Run(); err != nil {
		panic(err)
	}

}

type generalFlags struct {
	IsVerbose bool
	Dir       string
}

func (c *generalFlags) Flags() *flag.FlagSet {
	fs := flag.NewFlagSet("", flag.ContinueOnError)
	fs.BoolVar(&c.IsVerbose, "verbose", false, "should app be verbose")
	fs.StringVar(&c.Dir, "dir", ".", "directory to process")
	return fs
}

type commandFlags struct {
	generalFlags
	File string
}

func (c *commandFlags) Flags() *flag.FlagSet {
	fs := c.generalFlags.Flags()
	fs.StringVar(&c.File, "file", "input.txt", "file to process")
	return fs
}
Output:

now: 10:20:30
now: 10:20:30
now: 10:20:30

func RunnerOf

func RunnerOf(cmds []Command, cfg Config) *Runner

RunnerOf creates a Runner.

func (*Runner) Exit added in v0.5.3

func (r *Runner) Exit(err error)

Exit the application depending on the error. If err is nil, so successful/no error exit is done: os.Exit(0) If err is of type ErrCode: code from the error is returned: os.Exit(code) Otherwise: os.Exit(1).

func (*Runner) Run

func (r *Runner) Run() error

Run commands.

Jump to

Keyboard shortcuts

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