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 // 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 -- if specified, and if not returns - 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 }