218 lines
5.1 KiB
Go
218 lines
5.1 KiB
Go
package kong
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// TokenType is the type of a token.
|
|
type TokenType int
|
|
|
|
// Token types.
|
|
const (
|
|
UntypedToken TokenType = iota
|
|
EOLToken
|
|
FlagToken // --<flag>
|
|
FlagValueToken // =<value>
|
|
ShortFlagToken // -<short>[<tail]
|
|
ShortFlagTailToken // <tail>
|
|
PositionalArgumentToken // <arg>
|
|
)
|
|
|
|
func (t TokenType) String() string {
|
|
switch t {
|
|
case UntypedToken:
|
|
return "untyped"
|
|
case EOLToken:
|
|
return "<EOL>"
|
|
case FlagToken: // --<flag>
|
|
return "long flag"
|
|
case FlagValueToken: // =<value>
|
|
return "flag value"
|
|
case ShortFlagToken: // -<short>[<tail]
|
|
return "short flag"
|
|
case ShortFlagTailToken: // <tail>
|
|
return "short flag remainder"
|
|
case PositionalArgumentToken: // <arg>
|
|
return "positional argument"
|
|
}
|
|
panic("unsupported type")
|
|
}
|
|
|
|
// Token created by Scanner.
|
|
type Token struct {
|
|
Value interface{}
|
|
Type TokenType
|
|
}
|
|
|
|
func (t Token) String() string {
|
|
switch t.Type {
|
|
case FlagToken:
|
|
return fmt.Sprintf("--%v", t.Value)
|
|
|
|
case ShortFlagToken:
|
|
return fmt.Sprintf("-%v", t.Value)
|
|
|
|
case EOLToken:
|
|
return "EOL"
|
|
|
|
default:
|
|
return fmt.Sprintf("%v", t.Value)
|
|
}
|
|
}
|
|
|
|
// IsEOL returns true if this Token is past the end of the line.
|
|
func (t Token) IsEOL() bool {
|
|
return t.Type == EOLToken
|
|
}
|
|
|
|
// IsAny returns true if the token's type is any of those provided.
|
|
func (t TokenType) IsAny(types ...TokenType) bool {
|
|
for _, typ := range types {
|
|
if t == typ {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// InferredType tries to infer the type of a token.
|
|
func (t Token) InferredType() TokenType {
|
|
if t.Type != UntypedToken {
|
|
return t.Type
|
|
}
|
|
if v, ok := t.Value.(string); ok {
|
|
if strings.HasPrefix(v, "--") { // nolint: gocritic
|
|
return FlagToken
|
|
} else if v == "-" {
|
|
return PositionalArgumentToken
|
|
} else if strings.HasPrefix(v, "-") {
|
|
return ShortFlagToken
|
|
}
|
|
}
|
|
return t.Type
|
|
}
|
|
|
|
// IsValue returns true if token is usable as a parseable value.
|
|
//
|
|
// A parseable value is either a value typed token, or an untyped token NOT starting with a hyphen.
|
|
func (t Token) IsValue() bool {
|
|
tt := t.InferredType()
|
|
return tt.IsAny(FlagValueToken, ShortFlagTailToken, PositionalArgumentToken) ||
|
|
(tt == UntypedToken && !strings.HasPrefix(t.String(), "-"))
|
|
}
|
|
|
|
// Scanner is a stack-based scanner over command-line tokens.
|
|
//
|
|
// Initially all tokens are untyped. As the parser consumes tokens it assigns types, splits tokens, and pushes them back
|
|
// onto the stream.
|
|
//
|
|
// For example, the token "--foo=bar" will be split into the following by the parser:
|
|
//
|
|
// [{FlagToken, "foo"}, {FlagValueToken, "bar"}]
|
|
type Scanner struct {
|
|
args []Token
|
|
}
|
|
|
|
// Scan creates a new Scanner from args with untyped tokens.
|
|
func Scan(args ...string) *Scanner {
|
|
s := &Scanner{}
|
|
for _, arg := range args {
|
|
s.args = append(s.args, Token{Value: arg})
|
|
}
|
|
return s
|
|
}
|
|
|
|
// ScanFromTokens creates a new Scanner from a slice of tokens.
|
|
func ScanFromTokens(tokens ...Token) *Scanner {
|
|
return &Scanner{args: tokens}
|
|
}
|
|
|
|
// Len returns the number of input arguments.
|
|
func (s *Scanner) Len() int {
|
|
return len(s.args)
|
|
}
|
|
|
|
// Pop the front token off the Scanner.
|
|
func (s *Scanner) Pop() Token {
|
|
if len(s.args) == 0 {
|
|
return Token{Type: EOLToken}
|
|
}
|
|
arg := s.args[0]
|
|
s.args = s.args[1:]
|
|
return arg
|
|
}
|
|
|
|
type expectedError struct {
|
|
context string
|
|
token Token
|
|
}
|
|
|
|
func (e *expectedError) Error() string {
|
|
return fmt.Sprintf("expected %s value but got %q (%s)", e.context, e.token, e.token.InferredType())
|
|
}
|
|
|
|
// PopValue pops a value token, or returns an error.
|
|
//
|
|
// "context" is used to assist the user if the value can not be popped, eg. "expected <context> value but got <type>"
|
|
func (s *Scanner) PopValue(context string) (Token, error) {
|
|
t := s.Pop()
|
|
if !t.IsValue() {
|
|
return t, &expectedError{context, t}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// PopValueInto pops a value token into target or returns an error.
|
|
//
|
|
// "context" is used to assist the user if the value can not be popped, eg. "expected <context> value but got <type>"
|
|
func (s *Scanner) PopValueInto(context string, target interface{}) error {
|
|
t, err := s.PopValue(context)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return jsonTranscode(t.Value, target)
|
|
}
|
|
|
|
// PopWhile predicate returns true.
|
|
func (s *Scanner) PopWhile(predicate func(Token) bool) (values []Token) {
|
|
for predicate(s.Peek()) {
|
|
values = append(values, s.Pop())
|
|
}
|
|
return
|
|
}
|
|
|
|
// PopUntil predicate returns true.
|
|
func (s *Scanner) PopUntil(predicate func(Token) bool) (values []Token) {
|
|
for !predicate(s.Peek()) {
|
|
values = append(values, s.Pop())
|
|
}
|
|
return
|
|
}
|
|
|
|
// Peek at the next Token or return an EOLToken.
|
|
func (s *Scanner) Peek() Token {
|
|
if len(s.args) == 0 {
|
|
return Token{Type: EOLToken}
|
|
}
|
|
return s.args[0]
|
|
}
|
|
|
|
// Push an untyped Token onto the front of the Scanner.
|
|
func (s *Scanner) Push(arg interface{}) *Scanner {
|
|
s.PushToken(Token{Value: arg})
|
|
return s
|
|
}
|
|
|
|
// PushTyped pushes a typed token onto the front of the Scanner.
|
|
func (s *Scanner) PushTyped(arg interface{}, typ TokenType) *Scanner {
|
|
s.PushToken(Token{Value: arg, Type: typ})
|
|
return s
|
|
}
|
|
|
|
// PushToken pushes a preconstructed Token onto the front of the Scanner.
|
|
func (s *Scanner) PushToken(token Token) *Scanner {
|
|
s.args = append([]Token{token}, s.args...)
|
|
return s
|
|
}
|