// Licensed under terms of MIT license (see LICENSE-MIT)
|
|
// Copyright (c) 2013 Keith Batten, kbatten@gmail.com
|
|
|
|
/*
|
|
Package docopt parses command-line arguments based on a help message.
|
|
|
|
⚠ Use the alias “docopt-go”:
|
|
import "github.com/docopt/docopt-go"
|
|
or
|
|
$ go get github.com/docopt/docopt-go
|
|
*/
|
|
package docopt
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
/*
|
|
Parse `argv` based on the command-line interface described in `doc`.
|
|
|
|
Given a conventional command-line help message, docopt creates a parser and
|
|
processes the arguments. See
|
|
https://github.com/docopt/docopt#help-message-format for a description of the
|
|
help message format. If `argv` is `nil`, `os.Args[1:]` is used.
|
|
|
|
docopt returns a map of option names to the values parsed from `argv`, and an
|
|
error or `nil`.
|
|
|
|
Set `help` to `false` to disable automatic help messages on `-h` or `--help`.
|
|
If `version` is a non-empty string, it will be printed when `--version` is
|
|
specified. Set `optionsFirst` to `true` to require that options always come
|
|
before positional arguments; otherwise they can overlap.
|
|
|
|
By default, docopt calls `os.Exit(0)` if it handled a built-in option such as
|
|
`-h` or `--version`. If the user errored with a wrong command or options,
|
|
docopt exits with a return code of 1. To stop docopt from calling `os.Exit()`
|
|
and to handle your own return codes, pass an optional last parameter of `false`
|
|
for `exit`.
|
|
*/
|
|
func Parse(doc string, argv []string, help bool, version string,
|
|
optionsFirst bool, exit ...bool) (map[string]interface{}, error) {
|
|
// if "false" was the (optional) last arg, don't call os.Exit()
|
|
exitOk := true
|
|
if len(exit) > 0 {
|
|
exitOk = exit[0]
|
|
}
|
|
args, output, err := parse(doc, argv, help, version, optionsFirst)
|
|
if _, ok := err.(*UserError); ok {
|
|
// the user gave us bad input
|
|
fmt.Fprintln(os.Stderr, output)
|
|
if exitOk {
|
|
os.Exit(1)
|
|
}
|
|
} else if len(output) > 0 && err == nil {
|
|
// the user asked for help or `--version`
|
|
fmt.Println(output)
|
|
if exitOk {
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
return args, err
|
|
}
|
|
|
|
// parse and return a map of args, output and all errors
|
|
func parse(doc string, argv []string, help bool, version string, optionsFirst bool) (args map[string]interface{}, output string, err error) {
|
|
if argv == nil && len(os.Args) > 1 {
|
|
argv = os.Args[1:]
|
|
}
|
|
|
|
usageSections := parseSection("usage:", doc)
|
|
|
|
if len(usageSections) == 0 {
|
|
err = newLanguageError("\"usage:\" (case-insensitive) not found.")
|
|
return
|
|
}
|
|
if len(usageSections) > 1 {
|
|
err = newLanguageError("More than one \"usage:\" (case-insensitive).")
|
|
return
|
|
}
|
|
usage := usageSections[0]
|
|
|
|
options := parseDefaults(doc)
|
|
formal, err := formalUsage(usage)
|
|
if err != nil {
|
|
output = handleError(err, usage)
|
|
return
|
|
}
|
|
|
|
pat, err := parsePattern(formal, &options)
|
|
if err != nil {
|
|
output = handleError(err, usage)
|
|
return
|
|
}
|
|
|
|
patternArgv, err := parseArgv(newTokenList(argv, errorUser), &options, optionsFirst)
|
|
if err != nil {
|
|
output = handleError(err, usage)
|
|
return
|
|
}
|
|
patFlat, err := pat.flat(patternOption)
|
|
if err != nil {
|
|
output = handleError(err, usage)
|
|
return
|
|
}
|
|
patternOptions := patFlat.unique()
|
|
|
|
patFlat, err = pat.flat(patternOptionSSHORTCUT)
|
|
if err != nil {
|
|
output = handleError(err, usage)
|
|
return
|
|
}
|
|
for _, optionsShortcut := range patFlat {
|
|
docOptions := parseDefaults(doc)
|
|
optionsShortcut.children = docOptions.unique().diff(patternOptions)
|
|
}
|
|
|
|
if output = extras(help, version, patternArgv, doc); len(output) > 0 {
|
|
return
|
|
}
|
|
|
|
err = pat.fix()
|
|
if err != nil {
|
|
output = handleError(err, usage)
|
|
return
|
|
}
|
|
matched, left, collected := pat.match(&patternArgv, nil)
|
|
if matched && len(*left) == 0 {
|
|
patFlat, err = pat.flat(patternDefault)
|
|
if err != nil {
|
|
output = handleError(err, usage)
|
|
return
|
|
}
|
|
args = append(patFlat, *collected...).dictionary()
|
|
return
|
|
}
|
|
|
|
err = newUserError("")
|
|
output = handleError(err, usage)
|
|
return
|
|
}
|
|
|
|
func handleError(err error, usage string) string {
|
|
if _, ok := err.(*UserError); ok {
|
|
return strings.TrimSpace(fmt.Sprintf("%s\n%s", err, usage))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseSection(name, source string) []string {
|
|
p := regexp.MustCompile(`(?im)^([^\n]*` + name + `[^\n]*\n?(?:[ \t].*?(?:\n|$))*)`)
|
|
s := p.FindAllString(source, -1)
|
|
if s == nil {
|
|
s = []string{}
|
|
}
|
|
for i, v := range s {
|
|
s[i] = strings.TrimSpace(v)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func parseDefaults(doc string) patternList {
|
|
defaults := patternList{}
|
|
p := regexp.MustCompile(`\n[ \t]*(-\S+?)`)
|
|
for _, s := range parseSection("options:", doc) {
|
|
// FIXME corner case "bla: options: --foo"
|
|
_, _, s = stringPartition(s, ":") // get rid of "options:"
|
|
split := p.Split("\n"+s, -1)[1:]
|
|
match := p.FindAllStringSubmatch("\n"+s, -1)
|
|
for i := range split {
|
|
optionDescription := match[i][1] + split[i]
|
|
if strings.HasPrefix(optionDescription, "-") {
|
|
defaults = append(defaults, parseOption(optionDescription))
|
|
}
|
|
}
|
|
}
|
|
return defaults
|
|
}
|
|
|
|
func parsePattern(source string, options *patternList) (*pattern, error) {
|
|
tokens := tokenListFromPattern(source)
|
|
result, err := parseExpr(tokens, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tokens.current() != nil {
|
|
return nil, tokens.errorFunc("unexpected ending: %s" + strings.Join(tokens.tokens, " "))
|
|
}
|
|
return newRequired(result...), nil
|
|
}
|
|
|
|
func parseArgv(tokens *tokenList, options *patternList, optionsFirst bool) (patternList, error) {
|
|
/*
|
|
Parse command-line argument vector.
|
|
|
|
If options_first:
|
|
argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ;
|
|
else:
|
|
argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ;
|
|
*/
|
|
parsed := patternList{}
|
|
for tokens.current() != nil {
|
|
if tokens.current().eq("--") {
|
|
for _, v := range tokens.tokens {
|
|
parsed = append(parsed, newArgument("", v))
|
|
}
|
|
return parsed, nil
|
|
} else if tokens.current().hasPrefix("--") {
|
|
pl, err := parseLong(tokens, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parsed = append(parsed, pl...)
|
|
} else if tokens.current().hasPrefix("-") && !tokens.current().eq("-") {
|
|
ps, err := parseShorts(tokens, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parsed = append(parsed, ps...)
|
|
} else if optionsFirst {
|
|
for _, v := range tokens.tokens {
|
|
parsed = append(parsed, newArgument("", v))
|
|
}
|
|
return parsed, nil
|
|
} else {
|
|
parsed = append(parsed, newArgument("", tokens.move().String()))
|
|
}
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func parseOption(optionDescription string) *pattern {
|
|
optionDescription = strings.TrimSpace(optionDescription)
|
|
options, _, description := stringPartition(optionDescription, " ")
|
|
options = strings.Replace(options, ",", " ", -1)
|
|
options = strings.Replace(options, "=", " ", -1)
|
|
|
|
short := ""
|
|
long := ""
|
|
argcount := 0
|
|
var value interface{}
|
|
value = false
|
|
|
|
reDefault := regexp.MustCompile(`(?i)\[default: (.*)\]`)
|
|
for _, s := range strings.Fields(options) {
|
|
if strings.HasPrefix(s, "--") {
|
|
long = s
|
|
} else if strings.HasPrefix(s, "-") {
|
|
short = s
|
|
} else {
|
|
argcount = 1
|
|
}
|
|
if argcount > 0 {
|
|
matched := reDefault.FindAllStringSubmatch(description, -1)
|
|
if len(matched) > 0 {
|
|
value = matched[0][1]
|
|
} else {
|
|
value = nil
|
|
}
|
|
}
|
|
}
|
|
return newOption(short, long, argcount, value)
|
|
}
|
|
|
|
func parseExpr(tokens *tokenList, options *patternList) (patternList, error) {
|
|
// expr ::= seq ( '|' seq )* ;
|
|
seq, err := parseSeq(tokens, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !tokens.current().eq("|") {
|
|
return seq, nil
|
|
}
|
|
var result patternList
|
|
if len(seq) > 1 {
|
|
result = patternList{newRequired(seq...)}
|
|
} else {
|
|
result = seq
|
|
}
|
|
for tokens.current().eq("|") {
|
|
tokens.move()
|
|
seq, err = parseSeq(tokens, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(seq) > 1 {
|
|
result = append(result, newRequired(seq...))
|
|
} else {
|
|
result = append(result, seq...)
|
|
}
|
|
}
|
|
if len(result) > 1 {
|
|
return patternList{newEither(result...)}, nil
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func parseSeq(tokens *tokenList, options *patternList) (patternList, error) {
|
|
// seq ::= ( atom [ '...' ] )* ;
|
|
result := patternList{}
|
|
for !tokens.current().match(true, "]", ")", "|") {
|
|
atom, err := parseAtom(tokens, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tokens.current().eq("...") {
|
|
atom = patternList{newOneOrMore(atom...)}
|
|
tokens.move()
|
|
}
|
|
result = append(result, atom...)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func parseAtom(tokens *tokenList, options *patternList) (patternList, error) {
|
|
// atom ::= '(' expr ')' | '[' expr ']' | 'options' | long | shorts | argument | command ;
|
|
tok := tokens.current()
|
|
result := patternList{}
|
|
if tokens.current().match(false, "(", "[") {
|
|
tokens.move()
|
|
var matching string
|
|
pl, err := parseExpr(tokens, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tok.eq("(") {
|
|
matching = ")"
|
|
result = patternList{newRequired(pl...)}
|
|
} else if tok.eq("[") {
|
|
matching = "]"
|
|
result = patternList{newOptional(pl...)}
|
|
}
|
|
moved := tokens.move()
|
|
if !moved.eq(matching) {
|
|
return nil, tokens.errorFunc("unmatched '%s', expected: '%s' got: '%s'", tok, matching, moved)
|
|
}
|
|
return result, nil
|
|
} else if tok.eq("options") {
|
|
tokens.move()
|
|
return patternList{newOptionsShortcut()}, nil
|
|
} else if tok.hasPrefix("--") && !tok.eq("--") {
|
|
return parseLong(tokens, options)
|
|
} else if tok.hasPrefix("-") && !tok.eq("-") && !tok.eq("--") {
|
|
return parseShorts(tokens, options)
|
|
} else if tok.hasPrefix("<") && tok.hasSuffix(">") || tok.isUpper() {
|
|
return patternList{newArgument(tokens.move().String(), nil)}, nil
|
|
}
|
|
return patternList{newCommand(tokens.move().String(), false)}, nil
|
|
}
|
|
|
|
func parseLong(tokens *tokenList, options *patternList) (patternList, error) {
|
|
// long ::= '--' chars [ ( ' ' | '=' ) chars ] ;
|
|
long, eq, v := stringPartition(tokens.move().String(), "=")
|
|
var value interface{}
|
|
var opt *pattern
|
|
if eq == "" && v == "" {
|
|
value = nil
|
|
} else {
|
|
value = v
|
|
}
|
|
|
|
if !strings.HasPrefix(long, "--") {
|
|
return nil, newError("long option '%s' doesn't start with --", long)
|
|
}
|
|
similar := patternList{}
|
|
for _, o := range *options {
|
|
if o.long == long {
|
|
similar = append(similar, o)
|
|
}
|
|
}
|
|
if tokens.err == errorUser && len(similar) == 0 { // if no exact match
|
|
similar = patternList{}
|
|
for _, o := range *options {
|
|
if strings.HasPrefix(o.long, long) {
|
|
similar = append(similar, o)
|
|
}
|
|
}
|
|
}
|
|
if len(similar) > 1 { // might be simply specified ambiguously 2+ times?
|
|
similarLong := make([]string, len(similar))
|
|
for i, s := range similar {
|
|
similarLong[i] = s.long
|
|
}
|
|
return nil, tokens.errorFunc("%s is not a unique prefix: %s?", long, strings.Join(similarLong, ", "))
|
|
} else if len(similar) < 1 {
|
|
argcount := 0
|
|
if eq == "=" {
|
|
argcount = 1
|
|
}
|
|
opt = newOption("", long, argcount, false)
|
|
*options = append(*options, opt)
|
|
if tokens.err == errorUser {
|
|
var val interface{}
|
|
if argcount > 0 {
|
|
val = value
|
|
} else {
|
|
val = true
|
|
}
|
|
opt = newOption("", long, argcount, val)
|
|
}
|
|
} else {
|
|
opt = newOption(similar[0].short, similar[0].long, similar[0].argcount, similar[0].value)
|
|
if opt.argcount == 0 {
|
|
if value != nil {
|
|
return nil, tokens.errorFunc("%s must not have an argument", opt.long)
|
|
}
|
|
} else {
|
|
if value == nil {
|
|
if tokens.current().match(true, "--") {
|
|
return nil, tokens.errorFunc("%s requires argument", opt.long)
|
|
}
|
|
moved := tokens.move()
|
|
if moved != nil {
|
|
value = moved.String() // only set as string if not nil
|
|
}
|
|
}
|
|
}
|
|
if tokens.err == errorUser {
|
|
if value != nil {
|
|
opt.value = value
|
|
} else {
|
|
opt.value = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return patternList{opt}, nil
|
|
}
|
|
|
|
func parseShorts(tokens *tokenList, options *patternList) (patternList, error) {
|
|
// shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;
|
|
tok := tokens.move()
|
|
if !tok.hasPrefix("-") || tok.hasPrefix("--") {
|
|
return nil, newError("short option '%s' doesn't start with -", tok)
|
|
}
|
|
left := strings.TrimLeft(tok.String(), "-")
|
|
parsed := patternList{}
|
|
for left != "" {
|
|
var opt *pattern
|
|
short := "-" + left[0:1]
|
|
left = left[1:]
|
|
similar := patternList{}
|
|
for _, o := range *options {
|
|
if o.short == short {
|
|
similar = append(similar, o)
|
|
}
|
|
}
|
|
if len(similar) > 1 {
|
|
return nil, tokens.errorFunc("%s is specified ambiguously %d times", short, len(similar))
|
|
} else if len(similar) < 1 {
|
|
opt = newOption(short, "", 0, false)
|
|
*options = append(*options, opt)
|
|
if tokens.err == errorUser {
|
|
opt = newOption(short, "", 0, true)
|
|
}
|
|
} else { // why copying is necessary here?
|
|
opt = newOption(short, similar[0].long, similar[0].argcount, similar[0].value)
|
|
var value interface{}
|
|
if opt.argcount > 0 {
|
|
if left == "" {
|
|
if tokens.current().match(true, "--") {
|
|
return nil, tokens.errorFunc("%s requires argument", short)
|
|
}
|
|
value = tokens.move().String()
|
|
} else {
|
|
value = left
|
|
left = ""
|
|
}
|
|
}
|
|
if tokens.err == errorUser {
|
|
if value != nil {
|
|
opt.value = value
|
|
} else {
|
|
opt.value = true
|
|
}
|
|
}
|
|
}
|
|
parsed = append(parsed, opt)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func newTokenList(source []string, err errorType) *tokenList {
|
|
errorFunc := newError
|
|
if err == errorUser {
|
|
errorFunc = newUserError
|
|
} else if err == errorLanguage {
|
|
errorFunc = newLanguageError
|
|
}
|
|
return &tokenList{source, errorFunc, err}
|
|
}
|
|
|
|
func tokenListFromString(source string) *tokenList {
|
|
return newTokenList(strings.Fields(source), errorUser)
|
|
}
|
|
|
|
func tokenListFromPattern(source string) *tokenList {
|
|
p := regexp.MustCompile(`([\[\]\(\)\|]|\.\.\.)`)
|
|
source = p.ReplaceAllString(source, ` $1 `)
|
|
p = regexp.MustCompile(`\s+|(\S*<.*?>)`)
|
|
split := p.Split(source, -1)
|
|
match := p.FindAllStringSubmatch(source, -1)
|
|
var result []string
|
|
l := len(split)
|
|
for i := 0; i < l; i++ {
|
|
if len(split[i]) > 0 {
|
|
result = append(result, split[i])
|
|
}
|
|
if i < l-1 && len(match[i][1]) > 0 {
|
|
result = append(result, match[i][1])
|
|
}
|
|
}
|
|
return newTokenList(result, errorLanguage)
|
|
}
|
|
|
|
func formalUsage(section string) (string, error) {
|
|
_, _, section = stringPartition(section, ":") // drop "usage:"
|
|
pu := strings.Fields(section)
|
|
|
|
if len(pu) == 0 {
|
|
return "", newLanguageError("no fields found in usage (perhaps a spacing error).")
|
|
}
|
|
|
|
result := "( "
|
|
for _, s := range pu[1:] {
|
|
if s == pu[0] {
|
|
result += ") | ( "
|
|
} else {
|
|
result += s + " "
|
|
}
|
|
}
|
|
result += ")"
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func extras(help bool, version string, options patternList, doc string) string {
|
|
if help {
|
|
for _, o := range options {
|
|
if (o.name == "-h" || o.name == "--help") && o.value == true {
|
|
return strings.Trim(doc, "\n")
|
|
}
|
|
}
|
|
}
|
|
if version != "" {
|
|
for _, o := range options {
|
|
if (o.name == "--version") && o.value == true {
|
|
return version
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type errorType int
|
|
|
|
const (
|
|
errorUser errorType = iota
|
|
errorLanguage
|
|
)
|
|
|
|
func (e errorType) String() string {
|
|
switch e {
|
|
case errorUser:
|
|
return "errorUser"
|
|
case errorLanguage:
|
|
return "errorLanguage"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// UserError records an error with program arguments.
|
|
type UserError struct {
|
|
msg string
|
|
Usage string
|
|
}
|
|
|
|
func (e UserError) Error() string {
|
|
return e.msg
|
|
}
|
|
func newUserError(msg string, f ...interface{}) error {
|
|
return &UserError{fmt.Sprintf(msg, f...), ""}
|
|
}
|
|
|
|
// LanguageError records an error with the doc string.
|
|
type LanguageError struct {
|
|
msg string
|
|
}
|
|
|
|
func (e LanguageError) Error() string {
|
|
return e.msg
|
|
}
|
|
func newLanguageError(msg string, f ...interface{}) error {
|
|
return &LanguageError{fmt.Sprintf(msg, f...)}
|
|
}
|
|
|
|
var newError = fmt.Errorf
|
|
|
|
type tokenList struct {
|
|
tokens []string
|
|
errorFunc func(string, ...interface{}) error
|
|
err errorType
|
|
}
|
|
type token string
|
|
|
|
func (t *token) eq(s string) bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
return string(*t) == s
|
|
}
|
|
func (t *token) match(matchNil bool, tokenStrings ...string) bool {
|
|
if t == nil && matchNil {
|
|
return true
|
|
} else if t == nil && !matchNil {
|
|
return false
|
|
}
|
|
|
|
for _, tok := range tokenStrings {
|
|
if tok == string(*t) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
func (t *token) hasPrefix(prefix string) bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(string(*t), prefix)
|
|
}
|
|
func (t *token) hasSuffix(suffix string) bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
return strings.HasSuffix(string(*t), suffix)
|
|
}
|
|
func (t *token) isUpper() bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
return isStringUppercase(string(*t))
|
|
}
|
|
func (t *token) String() string {
|
|
if t == nil {
|
|
return ""
|
|
}
|
|
return string(*t)
|
|
}
|
|
|
|
func (tl *tokenList) current() *token {
|
|
if len(tl.tokens) > 0 {
|
|
return (*token)(&(tl.tokens[0]))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (tl *tokenList) length() int {
|
|
return len(tl.tokens)
|
|
}
|
|
|
|
func (tl *tokenList) move() *token {
|
|
if len(tl.tokens) > 0 {
|
|
t := tl.tokens[0]
|
|
tl.tokens = tl.tokens[1:]
|
|
return (*token)(&t)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type patternType uint
|
|
|
|
const (
|
|
// leaf
|
|
patternArgument patternType = 1 << iota
|
|
patternCommand
|
|
patternOption
|
|
|
|
// branch
|
|
patternRequired
|
|
patternOptionAL
|
|
patternOptionSSHORTCUT // Marker/placeholder for [options] shortcut.
|
|
patternOneOrMore
|
|
patternEither
|
|
|
|
patternLeaf = patternArgument +
|
|
patternCommand +
|
|
patternOption
|
|
patternBranch = patternRequired +
|
|
patternOptionAL +
|
|
patternOptionSSHORTCUT +
|
|
patternOneOrMore +
|
|
patternEither
|
|
patternAll = patternLeaf + patternBranch
|
|
patternDefault = 0
|
|
)
|
|
|
|
func (pt patternType) String() string {
|
|
switch pt {
|
|
case patternArgument:
|
|
return "argument"
|
|
case patternCommand:
|
|
return "command"
|
|
case patternOption:
|
|
return "option"
|
|
case patternRequired:
|
|
return "required"
|
|
case patternOptionAL:
|
|
return "optional"
|
|
case patternOptionSSHORTCUT:
|
|
return "optionsshortcut"
|
|
case patternOneOrMore:
|
|
return "oneormore"
|
|
case patternEither:
|
|
return "either"
|
|
case patternLeaf:
|
|
return "leaf"
|
|
case patternBranch:
|
|
return "branch"
|
|
case patternAll:
|
|
return "all"
|
|
case patternDefault:
|
|
return "default"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type pattern struct {
|
|
t patternType
|
|
|
|
children patternList
|
|
|
|
name string
|
|
value interface{}
|
|
|
|
short string
|
|
long string
|
|
argcount int
|
|
}
|
|
|
|
type patternList []*pattern
|
|
|
|
func newBranchPattern(t patternType, pl ...*pattern) *pattern {
|
|
var p pattern
|
|
p.t = t
|
|
p.children = make(patternList, len(pl))
|
|
copy(p.children, pl)
|
|
return &p
|
|
}
|
|
|
|
func newRequired(pl ...*pattern) *pattern {
|
|
return newBranchPattern(patternRequired, pl...)
|
|
}
|
|
|
|
func newEither(pl ...*pattern) *pattern {
|
|
return newBranchPattern(patternEither, pl...)
|
|
}
|
|
|
|
func newOneOrMore(pl ...*pattern) *pattern {
|
|
return newBranchPattern(patternOneOrMore, pl...)
|
|
}
|
|
|
|
func newOptional(pl ...*pattern) *pattern {
|
|
return newBranchPattern(patternOptionAL, pl...)
|
|
}
|
|
|
|
func newOptionsShortcut() *pattern {
|
|
var p pattern
|
|
p.t = patternOptionSSHORTCUT
|
|
return &p
|
|
}
|
|
|
|
func newLeafPattern(t patternType, name string, value interface{}) *pattern {
|
|
// default: value=nil
|
|
var p pattern
|
|
p.t = t
|
|
p.name = name
|
|
p.value = value
|
|
return &p
|
|
}
|
|
|
|
func newArgument(name string, value interface{}) *pattern {
|
|
// default: value=nil
|
|
return newLeafPattern(patternArgument, name, value)
|
|
}
|
|
|
|
func newCommand(name string, value interface{}) *pattern {
|
|
// default: value=false
|
|
var p pattern
|
|
p.t = patternCommand
|
|
p.name = name
|
|
p.value = value
|
|
return &p
|
|
}
|
|
|
|
func newOption(short, long string, argcount int, value interface{}) *pattern {
|
|
// default: "", "", 0, false
|
|
var p pattern
|
|
p.t = patternOption
|
|
p.short = short
|
|
p.long = long
|
|
if long != "" {
|
|
p.name = long
|
|
} else {
|
|
p.name = short
|
|
}
|
|
p.argcount = argcount
|
|
if value == false && argcount > 0 {
|
|
p.value = nil
|
|
} else {
|
|
p.value = value
|
|
}
|
|
return &p
|
|
}
|
|
|
|
func (p *pattern) flat(types patternType) (patternList, error) {
|
|
if p.t&patternLeaf != 0 {
|
|
if types == patternDefault {
|
|
types = patternAll
|
|
}
|
|
if p.t&types != 0 {
|
|
return patternList{p}, nil
|
|
}
|
|
return patternList{}, nil
|
|
}
|
|
|
|
if p.t&patternBranch != 0 {
|
|
if p.t&types != 0 {
|
|
return patternList{p}, nil
|
|
}
|
|
result := patternList{}
|
|
for _, child := range p.children {
|
|
childFlat, err := child.flat(types)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, childFlat...)
|
|
}
|
|
return result, nil
|
|
}
|
|
return nil, newError("unknown pattern type: %d, %d", p.t, types)
|
|
}
|
|
|
|
func (p *pattern) fix() error {
|
|
err := p.fixIdentities(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.fixRepeatingArguments()
|
|
return nil
|
|
}
|
|
|
|
func (p *pattern) fixIdentities(uniq patternList) error {
|
|
// Make pattern-tree tips point to same object if they are equal.
|
|
if p.t&patternBranch == 0 {
|
|
return nil
|
|
}
|
|
if uniq == nil {
|
|
pFlat, err := p.flat(patternDefault)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
uniq = pFlat.unique()
|
|
}
|
|
for i, child := range p.children {
|
|
if child.t&patternBranch == 0 {
|
|
ind, err := uniq.index(child)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.children[i] = uniq[ind]
|
|
} else {
|
|
err := child.fixIdentities(uniq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *pattern) fixRepeatingArguments() {
|
|
// Fix elements that should accumulate/increment values.
|
|
var either []patternList
|
|
|
|
for _, child := range p.transform().children {
|
|
either = append(either, child.children)
|
|
}
|
|
for _, cas := range either {
|
|
casMultiple := patternList{}
|
|
for _, e := range cas {
|
|
if cas.count(e) > 1 {
|
|
casMultiple = append(casMultiple, e)
|
|
}
|
|
}
|
|
for _, e := range casMultiple {
|
|
if e.t == patternArgument || e.t == patternOption && e.argcount > 0 {
|
|
switch e.value.(type) {
|
|
case string:
|
|
e.value = strings.Fields(e.value.(string))
|
|
case []string:
|
|
default:
|
|
e.value = []string{}
|
|
}
|
|
}
|
|
if e.t == patternCommand || e.t == patternOption && e.argcount == 0 {
|
|
e.value = 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *pattern) match(left *patternList, collected *patternList) (bool, *patternList, *patternList) {
|
|
if collected == nil {
|
|
collected = &patternList{}
|
|
}
|
|
if p.t&patternRequired != 0 {
|
|
l := left
|
|
c := collected
|
|
for _, p := range p.children {
|
|
var matched bool
|
|
matched, l, c = p.match(l, c)
|
|
if !matched {
|
|
return false, left, collected
|
|
}
|
|
}
|
|
return true, l, c
|
|
} else if p.t&patternOptionAL != 0 || p.t&patternOptionSSHORTCUT != 0 {
|
|
for _, p := range p.children {
|
|
_, left, collected = p.match(left, collected)
|
|
}
|
|
return true, left, collected
|
|
} else if p.t&patternOneOrMore != 0 {
|
|
if len(p.children) != 1 {
|
|
panic("OneOrMore.match(): assert len(p.children) == 1")
|
|
}
|
|
l := left
|
|
c := collected
|
|
var lAlt *patternList
|
|
matched := true
|
|
times := 0
|
|
for matched {
|
|
// could it be that something didn't match but changed l or c?
|
|
matched, l, c = p.children[0].match(l, c)
|
|
if matched {
|
|
times++
|
|
}
|
|
if lAlt == l {
|
|
break
|
|
}
|
|
lAlt = l
|
|
}
|
|
if times >= 1 {
|
|
return true, l, c
|
|
}
|
|
return false, left, collected
|
|
} else if p.t&patternEither != 0 {
|
|
type outcomeStruct struct {
|
|
matched bool
|
|
left *patternList
|
|
collected *patternList
|
|
length int
|
|
}
|
|
outcomes := []outcomeStruct{}
|
|
for _, p := range p.children {
|
|
matched, l, c := p.match(left, collected)
|
|
outcome := outcomeStruct{matched, l, c, len(*l)}
|
|
if matched {
|
|
outcomes = append(outcomes, outcome)
|
|
}
|
|
}
|
|
if len(outcomes) > 0 {
|
|
minLen := outcomes[0].length
|
|
minIndex := 0
|
|
for i, v := range outcomes {
|
|
if v.length < minLen {
|
|
minIndex = i
|
|
}
|
|
}
|
|
return outcomes[minIndex].matched, outcomes[minIndex].left, outcomes[minIndex].collected
|
|
}
|
|
return false, left, collected
|
|
} else if p.t&patternLeaf != 0 {
|
|
pos, match := p.singleMatch(left)
|
|
var increment interface{}
|
|
if match == nil {
|
|
return false, left, collected
|
|
}
|
|
leftAlt := make(patternList, len((*left)[:pos]), len((*left)[:pos])+len((*left)[pos+1:]))
|
|
copy(leftAlt, (*left)[:pos])
|
|
leftAlt = append(leftAlt, (*left)[pos+1:]...)
|
|
sameName := patternList{}
|
|
for _, a := range *collected {
|
|
if a.name == p.name {
|
|
sameName = append(sameName, a)
|
|
}
|
|
}
|
|
|
|
switch p.value.(type) {
|
|
case int, []string:
|
|
switch p.value.(type) {
|
|
case int:
|
|
increment = 1
|
|
case []string:
|
|
switch match.value.(type) {
|
|
case string:
|
|
increment = []string{match.value.(string)}
|
|
default:
|
|
increment = match.value
|
|
}
|
|
}
|
|
if len(sameName) == 0 {
|
|
match.value = increment
|
|
collectedMatch := make(patternList, len(*collected), len(*collected)+1)
|
|
copy(collectedMatch, *collected)
|
|
collectedMatch = append(collectedMatch, match)
|
|
return true, &leftAlt, &collectedMatch
|
|
}
|
|
switch sameName[0].value.(type) {
|
|
case int:
|
|
sameName[0].value = sameName[0].value.(int) + increment.(int)
|
|
case []string:
|
|
sameName[0].value = append(sameName[0].value.([]string), increment.([]string)...)
|
|
}
|
|
return true, &leftAlt, collected
|
|
}
|
|
collectedMatch := make(patternList, len(*collected), len(*collected)+1)
|
|
copy(collectedMatch, *collected)
|
|
collectedMatch = append(collectedMatch, match)
|
|
return true, &leftAlt, &collectedMatch
|
|
}
|
|
panic("unmatched type")
|
|
}
|
|
|
|
func (p *pattern) singleMatch(left *patternList) (int, *pattern) {
|
|
if p.t&patternArgument != 0 {
|
|
for n, pat := range *left {
|
|
if pat.t&patternArgument != 0 {
|
|
return n, newArgument(p.name, pat.value)
|
|
}
|
|
}
|
|
return -1, nil
|
|
} else if p.t&patternCommand != 0 {
|
|
for n, pat := range *left {
|
|
if pat.t&patternArgument != 0 {
|
|
if pat.value == p.name {
|
|
return n, newCommand(p.name, true)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return -1, nil
|
|
} else if p.t&patternOption != 0 {
|
|
for n, pat := range *left {
|
|
if p.name == pat.name {
|
|
return n, pat
|
|
}
|
|
}
|
|
return -1, nil
|
|
}
|
|
panic("unmatched type")
|
|
}
|
|
|
|
func (p *pattern) String() string {
|
|
if p.t&patternOption != 0 {
|
|
return fmt.Sprintf("%s(%s, %s, %d, %+v)", p.t, p.short, p.long, p.argcount, p.value)
|
|
} else if p.t&patternLeaf != 0 {
|
|
return fmt.Sprintf("%s(%s, %+v)", p.t, p.name, p.value)
|
|
} else if p.t&patternBranch != 0 {
|
|
result := ""
|
|
for i, child := range p.children {
|
|
if i > 0 {
|
|
result += ", "
|
|
}
|
|
result += child.String()
|
|
}
|
|
return fmt.Sprintf("%s(%s)", p.t, result)
|
|
}
|
|
panic("unmatched type")
|
|
}
|
|
|
|
func (p *pattern) transform() *pattern {
|
|
/*
|
|
Expand pattern into an (almost) equivalent one, but with single Either.
|
|
|
|
Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
|
|
Quirks: [-a] => (-a), (-a...) => (-a -a)
|
|
*/
|
|
result := []patternList{}
|
|
groups := []patternList{patternList{p}}
|
|
parents := patternRequired +
|
|
patternOptionAL +
|
|
patternOptionSSHORTCUT +
|
|
patternEither +
|
|
patternOneOrMore
|
|
for len(groups) > 0 {
|
|
children := groups[0]
|
|
groups = groups[1:]
|
|
var child *pattern
|
|
for _, c := range children {
|
|
if c.t&parents != 0 {
|
|
child = c
|
|
break
|
|
}
|
|
}
|
|
if child != nil {
|
|
children.remove(child)
|
|
if child.t&patternEither != 0 {
|
|
for _, c := range child.children {
|
|
r := patternList{}
|
|
r = append(r, c)
|
|
r = append(r, children...)
|
|
groups = append(groups, r)
|
|
}
|
|
} else if child.t&patternOneOrMore != 0 {
|
|
r := patternList{}
|
|
r = append(r, child.children.double()...)
|
|
r = append(r, children...)
|
|
groups = append(groups, r)
|
|
} else {
|
|
r := patternList{}
|
|
r = append(r, child.children...)
|
|
r = append(r, children...)
|
|
groups = append(groups, r)
|
|
}
|
|
} else {
|
|
result = append(result, children)
|
|
}
|
|
}
|
|
either := patternList{}
|
|
for _, e := range result {
|
|
either = append(either, newRequired(e...))
|
|
}
|
|
return newEither(either...)
|
|
}
|
|
|
|
func (p *pattern) eq(other *pattern) bool {
|
|
return reflect.DeepEqual(p, other)
|
|
}
|
|
|
|
func (pl patternList) unique() patternList {
|
|
table := make(map[string]bool)
|
|
result := patternList{}
|
|
for _, v := range pl {
|
|
if !table[v.String()] {
|
|
table[v.String()] = true
|
|
result = append(result, v)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (pl patternList) index(p *pattern) (int, error) {
|
|
for i, c := range pl {
|
|
if c.eq(p) {
|
|
return i, nil
|
|
}
|
|
}
|
|
return -1, newError("%s not in list", p)
|
|
}
|
|
|
|
func (pl patternList) count(p *pattern) int {
|
|
count := 0
|
|
for _, c := range pl {
|
|
if c.eq(p) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (pl patternList) diff(l patternList) patternList {
|
|
lAlt := make(patternList, len(l))
|
|
copy(lAlt, l)
|
|
result := make(patternList, 0, len(pl))
|
|
for _, v := range pl {
|
|
if v != nil {
|
|
match := false
|
|
for i, w := range lAlt {
|
|
if w.eq(v) {
|
|
match = true
|
|
lAlt[i] = nil
|
|
break
|
|
}
|
|
}
|
|
if match == false {
|
|
result = append(result, v)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (pl patternList) double() patternList {
|
|
l := len(pl)
|
|
result := make(patternList, l*2)
|
|
copy(result, pl)
|
|
copy(result[l:2*l], pl)
|
|
return result
|
|
}
|
|
|
|
func (pl *patternList) remove(p *pattern) {
|
|
(*pl) = pl.diff(patternList{p})
|
|
}
|
|
|
|
func (pl patternList) dictionary() map[string]interface{} {
|
|
dict := make(map[string]interface{})
|
|
for _, a := range pl {
|
|
dict[a.name] = a.value
|
|
}
|
|
return dict
|
|
}
|
|
|
|
func stringPartition(s, sep string) (string, string, string) {
|
|
sepPos := strings.Index(s, sep)
|
|
if sepPos == -1 { // no seperator found
|
|
return s, "", ""
|
|
}
|
|
split := strings.SplitN(s, sep, 2)
|
|
return split[0], sep, split[1]
|
|
}
|
|
|
|
// returns true if all cased characters in the string are uppercase
|
|
// and there are there is at least one cased charcter
|
|
func isStringUppercase(s string) bool {
|
|
if strings.ToUpper(s) != s {
|
|
return false
|
|
}
|
|
for _, c := range []rune(s) {
|
|
if unicode.IsUpper(c) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|