| // Copyright 2012 Jesse van den Kieboom. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package flags |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "os" |
| "path" |
| "sort" |
| "strings" |
| "unicode/utf8" |
| ) |
| |
| // A Parser provides command line option parsing. It can contain several |
| // option groups each with their own set of options. |
| type Parser struct { |
| Commander |
| |
| // The option groups available to the parser |
| Groups []*Group |
| GroupsMap map[string]*Group |
| |
| // The parser application name |
| ApplicationName string |
| |
| // The application short description |
| Description string |
| |
| // The usage (e.g. [OPTIONS] <filename>) |
| Usage string |
| |
| Options Options |
| |
| currentCommand *Group |
| currentCommandString []string |
| } |
| |
| // Parser options |
| type Options uint |
| |
| const ( |
| // No options |
| None Options = 0 |
| |
| // Add a default Help Options group to the parser containing -h and |
| // --help options. When either -h or --help is specified on the command |
| // line, a pretty formatted help message will be printed to os.Stderr. |
| // The parser will return ErrHelp. |
| HelpFlag = 1 << iota |
| |
| // Pass all arguments after a double dash, --, as remaining command line |
| // arguments (i.e. they will not be parsed for flags) |
| PassDoubleDash |
| |
| // Ignore any unknown options and pass them as remaining command line |
| // arguments |
| IgnoreUnknown |
| |
| // Print any errors which occured during parsing to os.Stderr |
| PrintErrors |
| |
| // Pass all arguments after the first non option. This is equivalent |
| // to strict POSIX processing |
| PassAfterNonOption |
| |
| // A convenient default set of options |
| Default = HelpFlag | PrintErrors | PassDoubleDash |
| ) |
| |
| // Parse is a convenience function to parse command line options with default |
| // settings. The provided data is a pointer to a struct representing the |
| // default option group (named "Application Options"). For more control, use |
| // flags.NewParser. |
| func Parse(data interface{}) ([]string, error) { |
| return NewParser(data, Default).Parse() |
| } |
| |
| // ParseArgs is a convenience function to parse command line options with default |
| // settings. The provided data is a pointer to a struct representing the |
| // default option group (named "Application Options"). The args argument is |
| // the list of command line arguments to parse. If you just want to parse the |
| // default program command line arguments (i.e. os.Args), then use flags.Parse |
| // instead. For more control, use flags.NewParser. |
| func ParseArgs(data interface{}, args []string) ([]string, error) { |
| return NewParser(data, Default).ParseArgs(args) |
| } |
| |
| // ParseIni is a convenience function to parse command line options with default |
| // settings from an ini file. The provided data is a pointer to a struct |
| // representing the default option group (named "Application Options"). For |
| // more control, use flags.NewParser. |
| func ParseIni(filename string, data interface{}) error { |
| return NewParser(data, Default).ParseIniFile(filename) |
| } |
| |
| // NewParser creates a new parser. It uses os.Args[0] as the application |
| // name and then calls Parser.NewNamedParser (see Parser.NewNamedParser for |
| // more details). The provided data is a pointer to a struct representing the |
| // default option group (named "Application Options"), or nil if the default |
| // group should not be added. The options parameter specifies a set of options |
| // for the parser. |
| func NewParser(data interface{}, options Options) *Parser { |
| if data == nil { |
| return NewNamedParser(path.Base(os.Args[0]), options) |
| } |
| |
| return NewNamedParser(path.Base(os.Args[0]), options, NewGroup("Application Options", data)) |
| } |
| |
| // NewNamedParser creates a new parser. The appname is used to display the |
| // executable name in the builtin help message. An initial set of option groups |
| // can be specified when constructing a parser, but you can also add additional |
| // option groups later (see Parser.AddGroup). |
| func NewNamedParser(appname string, options Options, groups ...*Group) *Parser { |
| ret := &Parser{ |
| Commander: Commander{ |
| Commands: make(map[string]*Group), |
| }, |
| |
| ApplicationName: appname, |
| Groups: groups, |
| GroupsMap: make(map[string]*Group), |
| Options: options, |
| Usage: "[OPTIONS]", |
| } |
| |
| ret.EachGroup(func(index int, grp *Group) { |
| ret.GroupsMap[strings.ToLower(grp.Name)] = grp |
| }) |
| |
| return ret |
| } |
| |
| // AddGroup adds a new group to the parser with the given name and data. The |
| // data needs to be a pointer to a struct from which the fields indicate which |
| // options are in the group. |
| func (p *Parser) AddGroup(name string, data interface{}) *Parser { |
| group := NewGroup(name, data) |
| |
| p.Groups = append(p.Groups, group) |
| |
| group.each(0, func(index int, grp *Group) { |
| p.GroupsMap[strings.ToLower(group.Name)] = grp |
| }) |
| |
| return p |
| } |
| |
| // AddCommand adds a new command to the parser with the given name and data. The |
| // data needs to be a pointer to a struct from which the fields indicate which |
| // options are in the command. |
| func (p *Parser) AddCommand(command string, description string, longDescription string, data interface{}) *Parser { |
| p.AddGroup(description, data) |
| |
| group := p.Groups[len(p.Groups)-1] |
| group.IsCommand = true |
| group.LongDescription = longDescription |
| |
| p.Commands[command] = group |
| |
| return p |
| } |
| |
| // Parse parses the command line arguments from os.Args using Parser.ParseArgs. |
| // For more detailed information see ParseArgs. |
| func (p *Parser) Parse() ([]string, error) { |
| return p.ParseArgs(os.Args[1:]) |
| } |
| |
| // ParseIniFile parses flags from an ini formatted file. See ParseIni for more |
| // information on the ini file foramt. The returned errors can be of the type |
| // flags.Error or flags.IniError. |
| func (p *Parser) ParseIniFile(filename string) error { |
| p.storeDefaults() |
| |
| ini, err := readIniFromFile(filename) |
| |
| if err != nil { |
| return err |
| } |
| |
| return p.parseIni(ini) |
| } |
| |
| func (p *Parser) EachGroup(cb func(int, *Group)) { |
| if p.currentCommand != nil { |
| p.currentCommand.each(0, cb) |
| } else { |
| p.eachTopLevelGroup(cb) |
| } |
| } |
| |
| // ParseIni parses flags from an ini format. You can use ParseIniFile as a |
| // convenience function to parse from a filename instead of a general |
| // io.Reader. |
| // |
| // The format of the ini file is as follows: |
| // |
| // [Option group name] |
| // option = value |
| // |
| // Each section in the ini file represents an option group in the flags parser. |
| // The default flags parser option group (i.e. when using flags.Parse) is |
| // named 'Application Options'. The ini option name is matched in the following |
| // order: |
| // |
| // 1. Compared to the ini-name tag on the option struct field (if present) |
| // 2. Compared to the struct field name |
| // 3. Compared to the option long name (if present) |
| // 4. Compared to the option short name (if present) |
| // |
| // The returned errors can be of the type flags.Error or |
| // flags.IniError. |
| func (p *Parser) ParseIni(reader io.Reader) error { |
| p.storeDefaults() |
| |
| ini, err := readIni(reader, "") |
| |
| if err != nil { |
| return err |
| } |
| |
| return p.parseIni(ini) |
| } |
| |
| // WriteIniToFile writes the flags as ini format into a file. See WriteIni |
| // for more information. The returned error occurs when the specified file |
| // could not be opened for writing. |
| func (p *Parser) WriteIniToFile(filename string, options IniOptions) error { |
| file, err := os.Create(filename) |
| |
| if err != nil { |
| return err |
| } |
| |
| defer file.Close() |
| p.WriteIni(file, options) |
| |
| return nil |
| } |
| |
| // WriteIni writes the current values of all the flags to an ini format. |
| // See ParseIni for more information on the ini file format. You typically |
| // call this only after settings have been parsed since the default values of each |
| // option are stored just before parsing the flags (this is only relevant when |
| // IniIncludeDefaults is _not_ set in options). |
| func (p *Parser) WriteIni(writer io.Writer, options IniOptions) { |
| writeIni(p, writer, options) |
| } |
| |
| func (p *Parser) levenshtein(s string, t string) int { |
| if len(s) == 0 { |
| return len(t) |
| } |
| |
| if len(t) == 0 { |
| return len(s) |
| } |
| |
| var l1, l2, l3 int |
| |
| if len(s) == 1 { |
| l1 = len(t) + 1 |
| } else { |
| l1 = p.levenshtein(s[1:len(s)-1], t) + 1 |
| } |
| |
| if len(t) == 1 { |
| l2 = len(s) + 1 |
| } else { |
| l2 = p.levenshtein(t[1:len(t)-1], s) + 1 |
| } |
| |
| l3 = p.levenshtein(s[1:len(s)], t[1:len(t)]) |
| |
| if s[0] != t[0] { |
| l3 += 1 |
| } |
| |
| if l2 < l1 { |
| l1 = l2 |
| } |
| |
| if l1 < l3 { |
| return l1 |
| } |
| |
| return l3 |
| } |
| |
| func (p *Parser) closest(cmd string, commands []string) (string, int) { |
| if len(commands) == 0 { |
| return "", 0 |
| } |
| |
| mincmd := -1 |
| mindist := -1 |
| |
| for i, c := range commands { |
| l := p.levenshtein(cmd, c) |
| |
| if mincmd < 0 || l < mindist { |
| mindist = l |
| mincmd = i |
| } |
| } |
| |
| return commands[mincmd], mindist |
| } |
| |
| func argumentIsOption(arg string) bool { |
| return len(arg) > 0 && arg[0] == '-' |
| } |
| |
| // ParseArgs parses the command line arguments according to the option groups that |
| // were added to the parser. On successful parsing of the arguments, the |
| // remaining, non-option, arguments (if any) are returned. The returned error |
| // indicates a parsing error and can be used with PrintError to display |
| // contextual information on where the error occurred exactly. |
| // |
| // When the common help group has been added (AddHelp) and either -h or --help |
| // was specified in the command line arguments, a help message will be |
| // automatically printed. Furthermore, the special error type ErrHelp is returned. |
| // It is up to the caller to exit the program if so desired. |
| func (p *Parser) ParseArgs(args []string) ([]string, error) { |
| ret := make([]string, 0, len(args)) |
| i := 0 |
| |
| p.storeDefaults() |
| |
| if (p.Options & HelpFlag) != None { |
| var help struct { |
| ShowHelp func() error `short:"h" long:"help" description:"Show this help message"` |
| } |
| |
| help.ShowHelp = func() error { |
| var b bytes.Buffer |
| p.WriteHelp(&b) |
| return newError(ErrHelp, b.String()) |
| } |
| |
| helpgrp := NewGroup("Help Options", &help) |
| |
| // Append the help group to toplevel |
| p.Groups = append([]*Group{helpgrp}, p.Groups...) |
| |
| // Also append the help group to every command |
| p.Commander.EachCommand(func(command string, grp *Group) { |
| grp.EmbeddedGroups = append([]*Group{helpgrp}, grp.EmbeddedGroups...) |
| }) |
| |
| p.Options &^= HelpFlag |
| } |
| |
| required := make(map[*Option]struct{}) |
| commands := make(map[string]*Group) |
| |
| for command, subgroup := range p.Commands { |
| commands[command] = subgroup |
| } |
| |
| // Mark required arguments in a map |
| for _, group := range p.Groups { |
| for _, option := range group.Options { |
| if option.Required { |
| required[option] = struct{}{} |
| } |
| } |
| |
| // Initial set of commands |
| for command, subgroup := range group.Commands { |
| commands[command] = subgroup |
| } |
| } |
| |
| for i < len(args) { |
| arg := args[i] |
| i++ |
| |
| // When PassDoubleDash is set and we encounter a --, then |
| // simply append all the rest as arguments and break out |
| if (p.Options&PassDoubleDash) != None && arg == "--" { |
| ret = append(ret, args[i:]...) |
| break |
| } |
| |
| // If the argument is not an option then |
| // 1) Check for subcommand |
| // 2) Append it to the rest if subcommand is not found |
| if !argumentIsOption(arg) { |
| if cmdgroup := commands[arg]; cmdgroup != nil { |
| // Set current 'root' group |
| p.currentCommand = cmdgroup |
| |
| p.currentCommandString = append(p.currentCommandString, |
| arg) |
| |
| commands = cmdgroup.Commands |
| } else { |
| if (p.Options & PassAfterNonOption) != None { |
| ret = append(ret, args[(i-1):]...) |
| break |
| } else { |
| ret = append(ret, arg) |
| } |
| } |
| |
| continue |
| } |
| |
| pos := strings.Index(arg, "=") |
| var argument *string |
| |
| if pos >= 0 { |
| rest := arg[pos+1:] |
| argument = &rest |
| arg = arg[:pos] |
| } |
| |
| var err error |
| var option *Option |
| |
| if strings.HasPrefix(arg, "--") { |
| err, i, option = p.parseLong(args, arg[2:], argument, i) |
| } else { |
| short := arg[1:] |
| |
| for j, c := range short { |
| clen := utf8.RuneLen(c) |
| islast := (j+clen == len(short)) |
| |
| if !islast && argument == nil { |
| rr := short[j+clen:] |
| next, _ := utf8.DecodeRuneInString(rr) |
| info, _ := p.getShort(c) |
| |
| if info != nil && info.canArgument() { |
| if snext, _ := p.getShort(next); snext == nil { |
| // Consider the next stuff as an argument |
| argument = &rr |
| islast = true |
| } |
| } |
| } |
| |
| err, i, option = p.parseShort(args, c, islast, argument, i) |
| |
| if err != nil || islast { |
| break |
| } |
| } |
| } |
| |
| if err != nil { |
| ignoreUnknown := (p.Options & IgnoreUnknown) != None |
| |
| parseErr, ok := err.(*Error) |
| if !ok { |
| parseErr = newError(ErrUnknown, err.Error()) |
| } |
| |
| if ignoreUnknown { |
| ret = append(ret, arg) |
| } |
| |
| if !(parseErr.Type == ErrUnknownFlag && ignoreUnknown) && (p.Options&PrintErrors) != None { |
| if parseErr.Type == ErrHelp { |
| fmt.Fprintln(os.Stderr, err) |
| } else { |
| fmt.Fprintf(os.Stderr, "Flags error: %s\n", err.Error()) |
| } |
| return nil, wrapError(err) |
| } |
| } else { |
| delete(required, option) |
| } |
| } |
| |
| if len(required) > 0 { |
| names := make([]string, 0, len(required)) |
| |
| for k, _ := range required { |
| names = append(names, "`"+k.String()+"'") |
| } |
| |
| var msg string |
| |
| if len(names) == 1 { |
| msg = fmt.Sprintf("the required flag %s was not specified", names[0]) |
| } else { |
| msg = fmt.Sprintf("the required flags %s and %s were not specified", |
| strings.Join(names[:len(names)-1], ", "), names[len(names)-1]) |
| } |
| |
| err := newError(ErrRequired, msg) |
| |
| if (p.Options & PrintErrors) != None { |
| fmt.Fprintln(os.Stderr, err) |
| } |
| |
| return nil, err |
| } |
| |
| if p.currentCommand != nil { |
| // Execute group |
| cmd, ok := p.currentCommand.data.(Command) |
| |
| if ok { |
| err := cmd.Execute(ret) |
| |
| if err != nil && (p.Options&PrintErrors) != None { |
| fmt.Fprintln(os.Stderr, err) |
| } |
| |
| return nil, err |
| } |
| } else if len(commands) != 0 { |
| cmdnames := make([]string, 0, len(commands)) |
| |
| for k, _ := range commands { |
| cmdnames = append(cmdnames, k) |
| } |
| |
| sort.Strings(cmdnames) |
| var msg string |
| |
| if len(ret) != 0 { |
| c, l := p.closest(ret[0], cmdnames) |
| msg = fmt.Sprintf("Unknown command `%s'", ret[0]) |
| |
| if float32(l)/float32(len(c)) < 0.5 { |
| msg = fmt.Sprintf("%s, did you mean `%s'?", msg, c) |
| } else { |
| msg = fmt.Sprintf("%s. Please specify one command of: %s", |
| msg, |
| strings.Join(cmdnames, ", ")) |
| } |
| } else { |
| msg = fmt.Sprintf("Please specify one command of: %s", strings.Join(cmdnames, ", ")) |
| } |
| |
| err := newError(ErrRequired, msg) |
| |
| if (p.Options & PrintErrors) != None { |
| fmt.Fprintln(os.Stderr, msg) |
| } |
| |
| return nil, err |
| } |
| |
| return ret, nil |
| } |