422 lines
11 KiB
Go
422 lines
11 KiB
Go
package cli
|
|
|
|
import "os"
|
|
import "io"
|
|
import "fmt"
|
|
import "sort"
|
|
import "errors"
|
|
import "strings"
|
|
import "strconv"
|
|
|
|
// Cli represents a command line interface. It is not tied to os.Args, and it is
|
|
// possible to create multiple Cli's to have so many sub-commands in one
|
|
// program.
|
|
type Cli struct {
|
|
Logger
|
|
|
|
// Description describes the application.
|
|
Description string
|
|
|
|
// The syntax description of the command. It appears in Usage after the
|
|
// name of the program (os.Args[0]). If not specified, "[OPTIONS]..."
|
|
// will be used instead.
|
|
Syntax string
|
|
|
|
// Flag is a set of flags to parse.
|
|
Flags []*Flag
|
|
|
|
// Args is a list of leftover arguments that were not parsed.
|
|
Args []string
|
|
|
|
// Sub contains sub-commands.
|
|
Sub map[string] *Cli
|
|
|
|
// Super contains the string of commands used to invoke this Cli.
|
|
// For example, a command invoked using
|
|
// <program> foo bar
|
|
// Would have Super set to
|
|
// "foo bar"
|
|
// This is automatically filled out by Cli.AddSub.
|
|
Super string
|
|
}
|
|
|
|
// New creates a new Cli from a command description and a set of flags. These
|
|
// flags should be created as variables before passing them to New(), so that
|
|
// their values can be extracted after Parse is called.
|
|
func New (description string, flags ...*Flag) *Cli {
|
|
return &Cli {
|
|
Description: description,
|
|
Flags: flags,
|
|
Sub: make(map[string] *Cli),
|
|
}
|
|
}
|
|
|
|
// AddSub adds a sub-command to this command. It automatically fills out the
|
|
// sub-command's Super field, and alters the prefix of its Logger to match.
|
|
func (this *Cli) AddSub (name string, sub *Cli) {
|
|
super := this.Super
|
|
if super != "" { super += " " }
|
|
super += name
|
|
sub.Super = super
|
|
sub.Prefix = os.Args[0] + " " + super
|
|
this.Sub[name] = sub
|
|
}
|
|
|
|
// Parse parses the given set of command line arguments according to the flags
|
|
// defined within the Cli. The first element of the argument slice is always
|
|
// dropped, because it is assumed that it just contains the command name. If the
|
|
// second element matches up with the name of a sub-command, parsing is
|
|
// deferred to that sub-command instead. The sub-command will be given all but
|
|
// the first element of args. This method will return the address of the
|
|
// command that did the parsing.
|
|
func (this *Cli) Parse (args []string) (*Cli, error) {
|
|
// try to find a sub-command
|
|
if this.Sub != nil && len(args) > 1 {
|
|
if sub, ok := this.Sub[args[1]]; ok {
|
|
return sub.Parse(args[1:])
|
|
}
|
|
}
|
|
|
|
// strip out program name
|
|
if len(args) > 0 { args = args[1:] }
|
|
|
|
next := func () string {
|
|
if len(args) < 1 { return "" }
|
|
args = args[1:]
|
|
if len(args) > 0 {
|
|
return args[0]
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
processFlag := func (flag *Flag, allowInput bool) error {
|
|
if flag.Validate == nil {
|
|
flag.Value = "true"
|
|
} else {
|
|
if len(args) < 1 || !allowInput {
|
|
return errors.New(
|
|
flag.String() +
|
|
" requires a value")
|
|
}
|
|
value := next()
|
|
err := flag.Validate(value)
|
|
if err != nil {
|
|
return errors.New(fmt.Sprint (
|
|
"bad value for ", flag.String(), ": ",
|
|
err))
|
|
}
|
|
flag.Value = value
|
|
}
|
|
if flag.Found != nil { flag.Found(this, flag.Value) }
|
|
return nil
|
|
}
|
|
|
|
// process args one by one
|
|
for ; len(args) > 0; next() {
|
|
arg := args[0]
|
|
|
|
switch {
|
|
case arg == "--":
|
|
// halt parsing
|
|
this.Args = append(this.Args, args[1:]...)
|
|
return nil, nil
|
|
|
|
case strings.HasPrefix(arg, "--"):
|
|
// long flag
|
|
flag, ok := this.LongFlag(arg[2:])
|
|
if !ok {
|
|
return nil, errors.New("unknown flag: " + arg)
|
|
}
|
|
err := processFlag(flag, true)
|
|
if err != nil { return nil, err }
|
|
|
|
case strings.HasPrefix(arg, "-") && len(arg) > 1:
|
|
// one or more short flags
|
|
arg := arg[1:]
|
|
for _, part := range arg {
|
|
flag, ok := this.ShortFlag(part)
|
|
if !ok {
|
|
return nil, errors.New (
|
|
"unknown flag: -" +
|
|
string(part))
|
|
}
|
|
err := processFlag(flag, len(arg) == 1)
|
|
if err != nil { return nil, err }
|
|
}
|
|
|
|
default:
|
|
// not a flag
|
|
this.Args = append(this.Args, arg)
|
|
}
|
|
}
|
|
return this, nil
|
|
}
|
|
|
|
// ParseOrExit is like Parse, but if an error occurs while parsing the argument
|
|
// list, it prints out Usage and exits with error status 2.
|
|
func (this *Cli) ParseOrExit (args []string) *Cli {
|
|
command, err := this.Parse(args)
|
|
if err != nil {
|
|
fmt.Fprintf(this, "%s: %v\n\n", os.Args[0], err)
|
|
this.Usage()
|
|
os.Exit(2)
|
|
}
|
|
return command
|
|
}
|
|
|
|
// ShortFlag searches for and returns the flag with the given short form. If it
|
|
// was found, it returns true. If it was not found, it returns nil, false.
|
|
func (this *Cli) ShortFlag (short rune) (*Flag, bool) {
|
|
if short == 0 { return nil, false }
|
|
for index := len(this.Flags) - 1; index >= 0; index -- {
|
|
flag := this.Flags[index]
|
|
if flag.Short == short { return flag, true }
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// LongFlag searches for and returns the flag with the given long form. If it
|
|
// was found, it returns true. If it was not found, it returns nil, false.
|
|
func (this *Cli) LongFlag (long string) (*Flag, bool) {
|
|
if long == "" { return nil, false }
|
|
for index := len(this.Flags) - 1; index >= 0; index -- {
|
|
flag := this.Flags[index]
|
|
if flag.Long == long { return flag, true }
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// Usage prints out usage/help information.
|
|
func (this *Cli) Usage () {
|
|
// syntax
|
|
fmt.Fprint(this, "Usage:")
|
|
fmt.Fprint(this, " ", os.Args[0])
|
|
if this.Super != "" {
|
|
fmt.Fprint(this, " ", this.Super)
|
|
}
|
|
hasSubCommands := this.Sub != nil && len(this.Sub) > 0
|
|
if hasSubCommands {
|
|
fmt.Fprint(this, " [COMMAND]")
|
|
}
|
|
if this.Syntax == "" {
|
|
fmt.Fprint(this, " [OPTION]...")
|
|
} else {
|
|
fmt.Fprint(this, " ", this.Syntax)
|
|
}
|
|
fmt.Fprint(this, "\n\n")
|
|
|
|
// description
|
|
if this.Description != "" {
|
|
fmt.Fprintf(this, "%s\n\n", this.Description)
|
|
}
|
|
|
|
// fit the longest flag
|
|
longest := 0
|
|
for _, flag := range this.Flags {
|
|
if len(flag.Long) > longest {
|
|
longest = len(flag.Long)
|
|
}
|
|
}
|
|
format := fmt.Sprint("\t%-", longest + 8, "s%s\n")
|
|
|
|
// flags
|
|
for _, flag := range this.Flags {
|
|
shortLong := ""
|
|
if flag.Short == 0 {
|
|
shortLong += " "
|
|
} else {
|
|
shortLong += "-" + string(flag.Short)
|
|
}
|
|
if flag.Short != 0 && flag.Long != "" {
|
|
shortLong += ", "
|
|
}
|
|
if flag.Long != "" {
|
|
shortLong += "--" + flag.Long
|
|
}
|
|
|
|
fmt.Fprintf(this, format, shortLong, flag.Help)
|
|
}
|
|
|
|
// commands
|
|
if hasSubCommands {
|
|
fmt.Fprint(this, "\nCommands:\n\n")
|
|
names := sortMapKeys(this.Sub)
|
|
|
|
// fit the longest command
|
|
longest := 0
|
|
for _, name := range names {
|
|
if len(name) > longest {
|
|
longest = len(name)
|
|
}
|
|
}
|
|
format := fmt.Sprint("\t%-", longest + 2, "s%s\n")
|
|
|
|
for _, name := range names {
|
|
fmt.Fprintf(this, format, name, this.Sub[name].Description)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Logger prints messages to an output writer.
|
|
type Logger struct {
|
|
// Writer specifies the writer to output to. If it is nil, the Logger
|
|
// will output to os.Stderr. If you wish to silence the output, set it
|
|
// to io.Discard.
|
|
io.Writer
|
|
|
|
// Debug determines whether or not debug messages will be logged.
|
|
Debug bool
|
|
|
|
// Prefix is printed before all messages. If this is an empty string,
|
|
// os.Args[0] will be used.
|
|
Prefix string
|
|
}
|
|
|
|
// Println logs a normal message.
|
|
func (this *Logger) Println (v ...any) {
|
|
fmt.Fprintf(this, "%s: %s", this.prefix(), fmt.Sprintln(v...))
|
|
}
|
|
|
|
// Errorln logs an error message.
|
|
func (this *Logger) Errorln (v ...any) {
|
|
fmt.Fprintf(this, "%s: %s", this.prefix(), fmt.Sprintln(v...))
|
|
}
|
|
|
|
// Warnln logs a warning.
|
|
func (this *Logger) Warnln (v ...any) {
|
|
fmt.Fprintf(this, "%s: warning: %s", this.prefix(), fmt.Sprintln(v...))
|
|
}
|
|
|
|
// Debugln logs debugging information. It will only print the message if the
|
|
// Debug field is set to true.
|
|
func (this *Logger) Debugln (v ...any) {
|
|
if !this.Debug { return }
|
|
fmt.Fprintf(this, "%s: debug: %s", this.prefix(), fmt.Sprintln(v...))
|
|
}
|
|
|
|
func (this *Logger) prefix () string {
|
|
if this.Prefix == "" {
|
|
return os.Args[0]
|
|
} else {
|
|
return this.Prefix
|
|
}
|
|
}
|
|
|
|
// Write writes to the Logger's writer. If it is nil, it writes to os.Stderr.
|
|
func (this *Logger) Write (data []byte) (n int, err error) {
|
|
if this.Writer == nil {
|
|
return os.Stderr.Write(data)
|
|
} else {
|
|
return this.Writer.Write(data)
|
|
}
|
|
}
|
|
|
|
// Flag is a command line option.
|
|
type Flag struct {
|
|
Short rune // The short form of the flag (-l)
|
|
Long string // Long form of the flag (--long-form)
|
|
Help string // Help text to display by the flag
|
|
|
|
// Validate is an optional function that is called when an input is
|
|
// passed to the flag. It checks whether or not the input is valid, and
|
|
// returns an error if it isn't. If this function is nil, the flag is
|
|
// assumed to have no input.
|
|
Validate func (string) error
|
|
|
|
// Value contains the input given to the flag, and is filled out when
|
|
// the flags are parsed. If this is a non-input flag (if Validate is
|
|
// nil), this will be set to true. If the flag was not specified, Value
|
|
// will be unchanged.
|
|
Value string
|
|
|
|
// Found is an optional function that is called each time the flag is
|
|
// found, and has a valid argument (if applicable). The value is passed
|
|
// to the function, as well as the command that it was found in.
|
|
Found func (*Cli, string)
|
|
}
|
|
|
|
// String returns --<LongForm> if specified, and if not returns -<ShortForm>
|
|
func (this *Flag) String () string {
|
|
if this.Long == "" {
|
|
return fmt.Sprintf("-%c", this.Short)
|
|
} else {
|
|
return fmt.Sprintf("--%s", this.Long)
|
|
}
|
|
}
|
|
|
|
// NewFlag creates a new flag that does not take in a value.
|
|
func NewFlag (short rune, long string, help string) *Flag {
|
|
return &Flag {
|
|
Short: short,
|
|
Long: long,
|
|
Help: help,
|
|
}
|
|
}
|
|
|
|
// NewInputFlag creates a new flag that does take in a value. This function will
|
|
// panic if the given validation function is nil.
|
|
func NewInputFlag (short rune, long string, help string, defaul string, validate func (string) error) *Flag {
|
|
if validate == nil {
|
|
panic("validate must be non-nil for a flag to take in a value")
|
|
}
|
|
return &Flag {
|
|
Short: short,
|
|
Long: long,
|
|
Help: help,
|
|
Value: defaul,
|
|
Validate: validate,
|
|
}
|
|
}
|
|
|
|
// NewHelp creates a new help flag activated by --help or -h. It shows help
|
|
// information for the command that it is a part of, and causes the program to
|
|
// exit with a status of 2.
|
|
func NewHelp () *Flag {
|
|
flag := NewFlag('h', "help", "Display usage information and exit")
|
|
flag.Found = func (command *Cli, value string) {
|
|
command.Usage()
|
|
os.Exit(2)
|
|
}
|
|
return flag
|
|
}
|
|
|
|
// ValString is a validation function that always returns nil, accepting any
|
|
// string.
|
|
func ValString (value string) error {
|
|
return nil
|
|
}
|
|
|
|
// ValInt is a validation function that returns an error if the value is not an
|
|
// integer.
|
|
func ValInt (value string) error {
|
|
_, err := strconv.Atoi(value)
|
|
return err
|
|
}
|
|
|
|
// NewValSet returns a validation function that accepts a set of strings.
|
|
func NewValSet (allowed ...string) func (value string) error {
|
|
return func (value string) error {
|
|
allowedFmt := ""
|
|
for index, test := range allowed {
|
|
if test == value { return nil }
|
|
|
|
if index > 0 { allowedFmt += ", " }
|
|
allowedFmt += test
|
|
}
|
|
return errors.New (
|
|
"value must be one of (" + allowedFmt + ")")
|
|
}
|
|
}
|
|
|
|
func sortMapKeys[T any] (unsorted map[string] T) []string {
|
|
keys := make([]string, len(unsorted))
|
|
index := 0
|
|
for key := range unsorted {
|
|
keys[index] = key
|
|
index ++
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|