clic

package module
v0.0.0-...-42b5467 Latest Latest
Warning

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

Go to latest
Published: Feb 20, 2025 License: MIT Imports: 11 Imported by: 1

README

clic GoDoc

go get github.com/daved/clic

Package clic provides streamlined POSIX-friendly CLI command parsing.

Usage

type Clic
    func New(h Handler, name string, subs ...*Clic) *Clic
    func NewFromFunc(f HandlerFunc, name string, subs ...*Clic) *Clic
    func (c *Clic) Flag(val any, names, usage string) *flagset.Flag
    func (c *Clic) Handle(ctx context.Context) error
    func (c *Clic) Operand(val any, req bool, name, desc string) *operandset.Operand
    func (c *Clic) Parse(args []string) error
    func (c *Clic) Recursively(fn func(*Clic))
    func (c *Clic) Usage() string
// see package docs for more
Setup
func main() {
    // error handling omitted to keep example focused

    var (
        info  = "default"
        value = "unset"
    )

    // Associate HandlerFunc with command name
    root := clic.NewFromFunc(printFunc(&info, &value), "myapp")

    // Associate flag and operand variables with relevant names
    root.Flag(&info, "i|info", "Set additional info.")
    root.Operand(&value, true, "first_operand", "Value to be printed.")

    // Parse the cli command as `myapp --info=flagval arrrg`
    cmd, _ := c.Parse([]string{"--info=flagval", "arrrg"})

    // Run the handler that Parse resolved to
    _ = cmd.Handle(context.Background())
}

More Info

CLI Argument Types

Three types of arguments are handled: commands (and subcommands), flags (and their values), and operands. Commands and subcommands can each define their own flags and operands. Arguments that are not subcommands or flag-related (no hyphen prefix, not a flag value), will be treated as operands.

Example argument layout:

command --flag=flag-value subcommand -f flag-value operand_a operand_b
Default Templating

cmd.Usage() value from the example above:

Usage:

  myapp [FLAGS] <first_operand>

Flags for myapp:

    -i, --info  =STRING    default: default
        Set additional info.
Custom Templating

The Tmpl type eases custom templating. Custom data can be attached to instances of Clic, FlagSet, Flag, OperandSet, and Operand via their Meta fields for use in templates. The default template construction function (NewUsageTmpl) can be used as a reference for custom templates.

Designed To Be...

POSIX-friendly:

  • Long and short flags should be supported in a reasonably normal manner
  • Use the term "Operand" rather than the ambiguous "Arg"
  • Flags should not be processed after operands
  • Usage output should be familiar and effective

Simple and powerful:

  • The API surface should be small and flexible
  • Flags and operands should not be auto-set (e.g. help flag set explicitly)
  • Advanced usage should look similar to basic usage
  • Users should be trusted to understand/use the type system (e.g. interfaces)
  • Users should be trusted to understand/use advanced control flow mechanisms
Maturable Architecture

Package docs contain suggestions for three stages of application growth.

Dependencies

Documentation

Overview

Package clic provides streamlined POSIX-friendly CLI command parsing.

CLI Argument Types

Three types of arguments are handled: commands (and subcommands), flags (and their values), and operands. Commands and subcommands can each define their own flags and operands. Arguments that are not subcommands or flag-related (no hyphen prefix, not a flag value) will be treated as operands.

Example argument layout:

command --flag=flag-value subcommand -f flag-value operand_a operand_b

Custom Templating

The Tmpl type eases custom templating. Custom data can be attached to instances of Clic, FlagSet, Flag, OperandSet, and Operand via their Meta fields for use in templates. The default template construction function (NewUsageTmpl) can be used as a reference for custom templates.

Designed To Be...

POSIX-friendly:

  • Long and short flags should be supported in a reasonably normal manner
  • Use the term "Operand" rather than the ambiguous "Arg"
  • Flags should not be processed after operands
  • Usage output should be familiar and effective

Simple and powerful:

  • The API surface should be small and flexible
  • Flags and operands should not be auto-set (e.g. help flag set explicitly)
  • Advanced usage should look similar to basic usage
  • Users should be trusted to understand/use the type system (e.g. interfaces)
  • Users should be trusted to understand/use advanced control flow mechanisms
Example
package main

import (
	"context"
	"fmt"

	"github.com/daved/clic"
)

func main() {
	// error handling omitted to keep example focused

	var (
		info  = "default"
		value = "unset"
	)

	printFn := func(ctx context.Context) error {
		fmt.Printf("info flag = %s\nvalue operand = %v\n", info, value)
		return nil
	}

	// Associate HandlerFunc with command name
	root := clic.NewFromFunc(printFn, "myapp")

	// Associate flag and operand variables with relevant names
	root.Flag(&info, "i|info", "Set additional info.")
	root.Operand(&value, true, "first_operand", "Value to be printed.")

	// Parse the cli command as `myapp --info=flagval arrrg`
	cmd, _ := root.Parse([]string{"--info=flagval", "arrrg"})

	// Run the handler that Parse resolved to
	_ = cmd.Handle(context.Background())

	fmt.Println()
	fmt.Println(cmd.Usage())
}
Output:

info flag = flagval
value operand = arrrg

Usage:

  myapp [FLAGS] <first_operand>

Flags for myapp:

    -i, --info  =STRING    default: default
        Set additional info.
Example (Aliases)
// error handling omitted to keep example focused

// Associate HandlerFunc with command name and alias
root := clic.NewFromFunc(hello, "hello|aliased")

// Parse the cli command as `myapp aliased`, and run the handler
cmd, _ := root.Parse([]string{"aliased"})
_ = cmd.Handle(context.Background())
Output:

Hello, World
Example (AppArchitectureHiStructure)
package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"os"

	"github.com/daved/clic"
)

type PrintCfg struct {
	Info  string
	Value string
}

func NewPrintCfg() *PrintCfg {
	return &PrintCfg{
		Info:  "default",
		Value: "unset",
	}
}

// Print focuses solely on an action without any knowledge about how it is/was
// called from the command line. This is not meant to be received as a rigid
// prescription, rather, as a reference for how various complex needs might be
// addressed (obviously it's excessive for this trivial example).
type Print struct {
	out io.Writer
	cnf *PrintCfg
}

func NewPrint(out io.Writer, cnf *PrintCfg) *Print {
	return &Print{
		out: out,
		cnf: cnf,
	}
}

func (p *Print) Run(ctx context.Context) error {
	fmt.Fprintf(p.out, "info flag = %s\nvalue operand = %v\n", p.cnf.Info, p.cnf.Value)
	return nil
}

// HandlePriոt focuses solely on composing an action (and related configuration)
// into its place within the Clic tree. Apart from structuring and constructing
// types, there's no special handling or knowledge. This helps keep code
// uncluttered, and diffs focused (i.e. in relevant files).
type HandlePriոt struct {
	action *Print
	actCnf *PrintCfg
}

func NewHandlePriոt(out io.Writer) *HandlePriոt {
	tCnf := NewPrintCfg()

	return &HandlePriոt{
		action: NewPrint(out, tCnf),
		actCnf: tCnf,
	}
}

// AsClic constrains Clic construction to user-defined types. Setting up Clic
// instances in this way helps to clean up calling code (e.g. main funcs).
func (h *HandlePriոt) AsClic(name string, subs ...*clic.Clic) *clic.Clic {
	c := clic.New(h, name, subs...)

	c.Flag(&h.actCnf.Info, "i|info", "Set additional info.")
	c.Operand(&h.actCnf.Value, true, "first_operand", "Value to be printed.")

	return c
}

func (h *HandlePriոt) HandleCommand(ctx context.Context) error {
	return h.action.Run(ctx)
}

// The rest of HandleRoot is found in the LoStructure example file.
func (h *HandleRoot) AsClic(name string, subs ...*clic.Clic) *clic.Clic {
	return clic.New(h, name, subs...)
}

func main() {
	out := os.Stdout // emulate an interesting dependency

	// Set up command
	root := NewHandleRoot(out).AsClic( // construct root handler with deps
		"myapp",                             // set root cmd name
		NewHandlePriոt(out).AsClic("print"), // set subcmd as newly constructed print handler
	)

	// Parse the cli command as `myapp print --info=flagval arrrg`
	cmd, err := root.Parse(args[1:])
	if err != nil {
		log.Fatalln(err)
	}

	// Run the handler that Parse resolved to
	if err := cmd.Handle(context.Background()); err != nil {
		log.Fatalln(err)
	}

}
Output:

info flag = flagval
value operand = arrrg
Example (AppArchitectureLoStructure)
package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"os"

	"github.com/daved/clic"
)

type HandleRoot struct {
	out io.Writer
}

func NewHandleRoot(out io.Writer) *HandleRoot {
	return &HandleRoot{
		out: out,
	}
}

func (s *HandleRoot) HandleCommand(ctx context.Context) error {
	fmt.Fprintln(s.out, "from root")
	return nil
}

type HandlePrint struct {
	out   io.Writer
	info  string
	value string
}

func NewHandlePrint(out io.Writer) *HandlePrint {
	return &HandlePrint{
		out:   out,
		info:  "default",
		value: "unset",
	}
}

func (s *HandlePrint) HandleCommand(ctx context.Context) error {
	fmt.Fprintf(s.out, "info flag = %s\nvalue operand = %v\n", s.info, s.value)
	return nil
}

func main() {
	out := os.Stdout // emulate an interesting dependency

	// Associate Handler with command name
	handlePrint := NewHandlePrint(out)
	print := clic.New(handlePrint, "print")

	// Associate "print" flag and operand variables with relevant names
	print.Flag(&handlePrint.info, "i|info", "Set additional info.")
	print.Operand(&handlePrint.value, true, "first_operand", "Value to be printed.")

	// Associate Handler with application name, adding "print" as a subcommand
	root := clic.New(NewHandleRoot(out), "myapp", print)

	// Parse the cli command as `myapp print --info=flagval arrrg`
	cmd, err := root.Parse(args[1:])
	if err != nil {
		log.Fatalln(err)
	}

	// Run the handler that Parse resolved to
	if err := cmd.Handle(context.Background()); err != nil {
		log.Fatalln(err)
	}

}
Output:

info flag = flagval
value operand = arrrg
Example (AppArchitectureNoStructure)
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/daved/clic"
)

var args = []string{"myapp", "print", "--info=flagval", "arrrg"}

func main() {
	var (
		info  = "default"
		value = "unset"
		out   = os.Stdout // emulate an interesting dependency
	)

	// Associate HandlerFunc with command name
	print := clic.NewFromFunc(
		func(ctx context.Context) error {
			fmt.Fprintf(out, "info flag = %s\nvalue operand = %v\n", info, value)
			return nil
		},
		"print",
	)

	// Associate "print" flag and operand variables with relevant names
	print.Flag(&info, "i|info", "Set additional info.")
	print.Operand(&value, true, "first_operand", "Value to be printed.")

	// Associate HandlerFunc with application name, adding "print" as a subcommand
	root := clic.NewFromFunc(
		func(ctx context.Context) error {
			fmt.Fprintln(out, "from root")
			return nil
		},
		"myapp", print,
	)

	// Parse the cli command as `myapp print --info=flagval arrrg`
	cmd, err := root.Parse(args[1:])
	if err != nil {
		log.Fatalln(err)
	}

	// Run the handler that Parse resolved to
	if err := cmd.Handle(context.Background()); err != nil {
		log.Fatalln(err)
	}

}
Output:

info flag = flagval
value operand = arrrg
Example (Categories)
// Associate HandlerFuncs with command names, setting cat and desc fields
hello := clic.NewFromFunc(hello, "hello")
hello.Category = "Salutations"
hello.Description = "Show hello world message"

goodbye := clic.NewFromFunc(goodbye, "goodbye")
goodbye.Category = "Salutations"
goodbye.Description = "Show goodbye message"

details := clic.NewFromFunc(details, "details")
details.Category = "Informational"
details.Description = "List details (os.Args)"

// Associate HandlerFunc with command name
root := clic.NewFromFunc(printRoot, "myapp", hello, goodbye, details)
root.SubRequired = true
// Set up subcommand category order
// Category names seperated from optional descriptions by "|"
root.SubCmdCatsSort = []string{"Salutations|Salutations-related", "Informational|All things info"}

// Parse the cli command as `myapp`; will return error from lack of subcommand
cmd, err := root.Parse([]string{})
if err != nil {
	fmt.Println(cmd.Usage())
	fmt.Println(err)
}
Output:

Usage:

  myapp {hello|goodbye|details}

Subcommands for myapp:

  Salutations        Salutations-related
    hello              Show hello world message
    goodbye            Show goodbye message

  Informational      All things info
    details            List details (os.Args)

cli command: parse: subcommand required
Example (HelpAndVersionFlags)
// some error handling omitted to keep example focused

errHelpRequested := errors.New("help requested")
errVersionRequested := errors.New("version requested")

// Associate HandlerFunc with command name, and set flags
root := clic.NewFromFunc(printRoot, "myapp")
root.Flag(errHelpRequested, "help", "Print usage and quit")
root.Flag(errVersionRequested, "version", "Print version and quit")

// Parse the cli command as `myapp --version`
cmd, err := root.Parse([]string{"--version"})
if err != nil {
	switch {
	case errors.Is(err, errHelpRequested):
		fmt.Println(cmd.Usage())
		return

	case errors.Is(err, errVersionRequested):
		fmt.Println("version: v1.2.3")
		return

	default:
		fmt.Println(cmd.Usage())
		fmt.Println(err)
		return // likely as non-zero using os.Exit(n)
	}
}
Output:

version: v1.2.3
Example (RecursivelyWrappingHandlers)
// error handling omitted to keep example focused

// Associate HandlerFuncs with command names
hello := clic.NewFromFunc(hello, "hello")
goodbye := clic.NewFromFunc(goodbye, "goodbye")
details := clic.NewFromFunc(details, "details")

root := clic.NewFromFunc(printRoot, "myapp", hello, goodbye, details)
root.SubRequired = true

root.Recursively(func(c *clic.Clic) {
	next := c.Handler.HandleCommand
	c.Handler = clic.HandlerFunc(func(ctx context.Context) error {
		fmt.Println("before")

		if err := next(ctx); err != nil {
			return err
		}

		fmt.Println("after")
		return nil
	})
})

// Parse the cli command as `myapp hello`, and run the handler
cmd, _ := root.Parse([]string{"hello"})
_ = cmd.Handle(context.Background())

// Parse the cli command as `myapp goodbye`, and run the handler
cmd, _ = root.Parse([]string{"goodbye"})
_ = cmd.Handle(context.Background())
Output:

before
Hello, World
after
before
Goodbye
after
Example (UserFriendlyErrorMessages)
var info string

// Associate HandlerFunc with command name, and set info flag
root := clic.NewFromFunc(printRoot, "myapp")
root.Flag(&info, "info", "Set info")

// Parse the cli command as `myapp --force-err`
cmd, err := root.Parse([]string{"--force-err"})
if err != nil {
	fmt.Println(cmd.Usage())
	fmt.Println(clic.UserFriendlyError(err))
	return // likely as non-zero using os.Exit(n)
}
Output:

Usage:

  myapp [FLAGS]

Flags for myapp:

    --info  =STRING
        Set info

Unrecognized flag "force-err"
Example (Verbosity)
// error handling omitted to keep example focused

var verbosity []bool

// Associate HandlerFunc with command name, and set verbosity flag
root := clic.NewFromFunc(hello, "myapp")
root.Flag(&verbosity, "v", "Set verbosity. Can be set multiple times.")

// Parse the cli command as `myapp -vvv`, and run the handler
cmd, _ := root.Parse([]string{"-vvv"})
_ = cmd.Handle(context.Background())

fmt.Printf("verbosity: length=%d value=%v\n", len(verbosity), verbosity)
fmt.Println()
fmt.Println(cmd.Usage())
Output:

Hello, World
verbosity: length=3 value=[true true true]

Usage:

  myapp [FLAGS]

Flags for myapp:

    -v  =BOOL
        Set verbosity. Can be set multiple times.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	CauseParseSubCmdRequired   = ErrSubCmdRequired
	CauseParseFlagResolve      = &flagset.ResolveError{}
	CauseParseFlagUnrecognized = flagset.ErrFlagUnrecognized
	CauseParseOperandResolve   = &operandset.ResolveError{}
	CauseParseOperandRequired  = operandset.ErrOperandRequired
	CauseParseHydrateError     = &vtype.HydrateError{}    // from Flag and Operand Resolve
	CauseParseTypeUnsupported  = vtype.ErrTypeUnsupported // from Flag and Operand Resolve
)

Cause values are provided for documentation, and to allow callers to easily detect error conditions using a switch/case and errors.Is. If error inspection is required, use errors.As.

View Source
var ErrSubCmdRequired = errors.New("subcommand required")

ErrSubCmdRequired signals that a subcommand is required and not set.

Functions

func UserFriendlyError

func UserFriendlyError(err error) error

UserFriendlyError returns a new error containing a plain language message.

Types

type Clic

type Clic struct {
	Links

	// Templating
	Tmpl           *Tmpl // set to NewUsageTmpl by default
	Description    string
	HideUsage      bool
	Category       string
	SubCmdCatsSort []string
	Meta           map[string]any

	// Additional Configuration
	SubRequired bool

	// Reconfiguration (modify as needed)
	Handler Handler
	Aliases []string

	// Accessing (avoid modification)
	FlagSet    *flagset.FlagSet
	OperandSet *operandset.OperandSet
}

Clic manages a CLI command handler and related information.

func New

func New(h Handler, name string, subs ...*Clic) *Clic

New returns an instance of Clic.

func NewFromFunc

func NewFromFunc(f HandlerFunc, name string, subs ...*Clic) *Clic

NewFromFunc returns an instance of Clic. Any function that is compatible with HandlerFunc will be converted automatically.

func (*Clic) Flag

func (c *Clic) Flag(val any, names, usage string) *flagset.Flag

Flag adds a flag option. See flagset.FlagSet.Flag for more details like compatible value types.

func (*Clic) Handle

func (c *Clic) Handle(ctx context.Context) error

Handle calls the HandleCommand method on the set Handler.

func (*Clic) Operand

func (c *Clic) Operand(val any, req bool, name, desc string) *operandset.Operand

Operand adds an operand option. See operandset.OperandSet.Operand for more details like compatible value types.

func (*Clic) Parse

func (c *Clic) Parse(args []string) (*Clic, error)

Parse resolves arguments to the relevant *Clic instance.

func (*Clic) Recursively

func (c *Clic) Recursively(fn func(*Clic))

Recursively applies the provided function to the current Clic instance and all its subcommands recursively.

func (*Clic) Usage

func (c *Clic) Usage() string

Usage returns usage text. The default template construction function (NewUsageTmpl) can be used as a reference for custom templates which should be used to set the "Tmpl" field on Clic (likely using *Clic.Recursively).

type Handler

type Handler interface {
	HandleCommand(context.Context) error
}

Handler describes types that can be used to handle CLI command requests.

type HandlerFunc

type HandlerFunc func(context.Context) error

HandlerFunc converts compatible functions to a Handler implementation.

func (HandlerFunc) HandleCommand

func (f HandlerFunc) HandleCommand(ctx context.Context) error

HandleCommand implements Handler.

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

Links contains Clic instance relationships. The relationships are defined here to clean up the Clic type's docs.

func (Links) ParentCmd

func (l Links) ParentCmd() *Clic

ParentCmd returns the parent Clic instance associated with the Clic instance.

func (Links) SubCmds

func (l Links) SubCmds() []*Clic

SubCmds returns the child Clic instances associated with the Clic instance.

type Tmpl

type Tmpl struct {
	Text string
	FMap template.FuncMap
	Data any
}

Tmpl holds template configuration details.

func NewUsageTmpl

func NewUsageTmpl(c *Clic) *Tmpl

NewUsageTmpl returns the default template configuration. This can be used as an example of how to setup custom usage output templating.

func (*Tmpl) Execute

func (t *Tmpl) Execute() (string, error)

Execute parses the template text and funcmap, then executes it using the set data.

func (*Tmpl) String

func (t *Tmpl) String() string

String calls the Execute method returning either the validly executed template output or error message text.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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