blob: cec3af1f034df60623ecc363fe61b617394bd512 [file] [log] [blame]
// 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
}