Initial commit
This commit is contained in:
commit
20c6ac26c5
418
cli.go
Normal file
418
cli.go
Normal file
@ -0,0 +1,418 @@
|
||||
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 {
|
||||
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 _, flag := range this.Flags {
|
||||
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 _, flag := range this.Flags {
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user