github.com/maresnic/mr-kong@v1.0.0/scanner.go (about)

     1  package kong
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  )
     7  
     8  // TokenType is the type of a token.
     9  type TokenType int
    10  
    11  // Token types.
    12  const (
    13  	UntypedToken TokenType = iota
    14  	EOLToken
    15  	FlagToken               // --<flag>
    16  	FlagValueToken          // =<value>
    17  	ShortFlagToken          // -<short>[<tail]
    18  	ShortFlagTailToken      // <tail>
    19  	PositionalArgumentToken // <arg>
    20  )
    21  
    22  func (t TokenType) String() string {
    23  	switch t {
    24  	case UntypedToken:
    25  		return "untyped"
    26  	case EOLToken:
    27  		return "<EOL>"
    28  	case FlagToken: // --<flag>
    29  		return "long flag"
    30  	case FlagValueToken: // =<value>
    31  		return "flag value"
    32  	case ShortFlagToken: // -<short>[<tail]
    33  		return "short flag"
    34  	case ShortFlagTailToken: // <tail>
    35  		return "short flag remainder"
    36  	case PositionalArgumentToken: // <arg>
    37  		return "positional argument"
    38  	}
    39  	panic("unsupported type")
    40  }
    41  
    42  // Token created by Scanner.
    43  type Token struct {
    44  	Value interface{}
    45  	Type  TokenType
    46  }
    47  
    48  func (t Token) String() string {
    49  	switch t.Type {
    50  	case FlagToken:
    51  		return fmt.Sprintf("--%v", t.Value)
    52  
    53  	case ShortFlagToken:
    54  		return fmt.Sprintf("-%v", t.Value)
    55  
    56  	case EOLToken:
    57  		return "EOL"
    58  
    59  	default:
    60  		return fmt.Sprintf("%v", t.Value)
    61  	}
    62  }
    63  
    64  // IsEOL returns true if this Token is past the end of the line.
    65  func (t Token) IsEOL() bool {
    66  	return t.Type == EOLToken
    67  }
    68  
    69  // IsAny returns true if the token's type is any of those provided.
    70  func (t TokenType) IsAny(types ...TokenType) bool {
    71  	for _, typ := range types {
    72  		if t == typ {
    73  			return true
    74  		}
    75  	}
    76  	return false
    77  }
    78  
    79  // InferredType tries to infer the type of a token.
    80  func (t Token) InferredType() TokenType {
    81  	if t.Type != UntypedToken {
    82  		return t.Type
    83  	}
    84  	if v, ok := t.Value.(string); ok {
    85  		if strings.HasPrefix(v, "--") { //nolint: gocritic
    86  			return FlagToken
    87  		} else if v == "-" {
    88  			return PositionalArgumentToken
    89  		} else if strings.HasPrefix(v, "-") {
    90  			return ShortFlagToken
    91  		}
    92  	}
    93  	return t.Type
    94  }
    95  
    96  // IsValue returns true if token is usable as a parseable value.
    97  //
    98  // A parseable value is either a value typed token, or an untyped token NOT starting with a hyphen.
    99  func (t Token) IsValue() bool {
   100  	tt := t.InferredType()
   101  	return tt.IsAny(FlagValueToken, ShortFlagTailToken, PositionalArgumentToken) ||
   102  		(tt == UntypedToken && !strings.HasPrefix(t.String(), "-"))
   103  }
   104  
   105  // Scanner is a stack-based scanner over command-line tokens.
   106  //
   107  // Initially all tokens are untyped. As the parser consumes tokens it assigns types, splits tokens, and pushes them back
   108  // onto the stream.
   109  //
   110  // For example, the token "--foo=bar" will be split into the following by the parser:
   111  //
   112  //	[{FlagToken, "foo"}, {FlagValueToken, "bar"}]
   113  type Scanner struct {
   114  	args []Token
   115  }
   116  
   117  // ScanAsType creates a new Scanner from args with the given type.
   118  func ScanAsType(ttype TokenType, args ...string) *Scanner {
   119  	s := &Scanner{}
   120  	for _, arg := range args {
   121  		s.args = append(s.args, Token{Value: arg, Type: ttype})
   122  	}
   123  	return s
   124  }
   125  
   126  // Scan creates a new Scanner from args with untyped tokens.
   127  func Scan(args ...string) *Scanner {
   128  	return ScanAsType(UntypedToken, args...)
   129  }
   130  
   131  // ScanFromTokens creates a new Scanner from a slice of tokens.
   132  func ScanFromTokens(tokens ...Token) *Scanner {
   133  	return &Scanner{args: tokens}
   134  }
   135  
   136  // Len returns the number of input arguments.
   137  func (s *Scanner) Len() int {
   138  	return len(s.args)
   139  }
   140  
   141  // Pop the front token off the Scanner.
   142  func (s *Scanner) Pop() Token {
   143  	if len(s.args) == 0 {
   144  		return Token{Type: EOLToken}
   145  	}
   146  	arg := s.args[0]
   147  	s.args = s.args[1:]
   148  	return arg
   149  }
   150  
   151  type expectedError struct {
   152  	context string
   153  	token   Token
   154  }
   155  
   156  func (e *expectedError) Error() string {
   157  	return fmt.Sprintf("expected %s value but got %q (%s)", e.context, e.token, e.token.InferredType())
   158  }
   159  
   160  // PopValue pops a value token, or returns an error.
   161  //
   162  // "context" is used to assist the user if the value can not be popped, eg. "expected <context> value but got <type>"
   163  func (s *Scanner) PopValue(context string) (Token, error) {
   164  	t := s.Pop()
   165  	if !t.IsValue() {
   166  		return t, &expectedError{context, t}
   167  	}
   168  	return t, nil
   169  }
   170  
   171  // PopValueInto pops a value token into target or returns an error.
   172  //
   173  // "context" is used to assist the user if the value can not be popped, eg. "expected <context> value but got <type>"
   174  func (s *Scanner) PopValueInto(context string, target interface{}) error {
   175  	t, err := s.PopValue(context)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	return jsonTranscode(t.Value, target)
   180  }
   181  
   182  // PopWhile predicate returns true.
   183  func (s *Scanner) PopWhile(predicate func(Token) bool) (values []Token) {
   184  	for predicate(s.Peek()) {
   185  		values = append(values, s.Pop())
   186  	}
   187  	return
   188  }
   189  
   190  // PopUntil predicate returns true.
   191  func (s *Scanner) PopUntil(predicate func(Token) bool) (values []Token) {
   192  	for !predicate(s.Peek()) {
   193  		values = append(values, s.Pop())
   194  	}
   195  	return
   196  }
   197  
   198  // Peek at the next Token or return an EOLToken.
   199  func (s *Scanner) Peek() Token {
   200  	if len(s.args) == 0 {
   201  		return Token{Type: EOLToken}
   202  	}
   203  	return s.args[0]
   204  }
   205  
   206  // Push an untyped Token onto the front of the Scanner.
   207  func (s *Scanner) Push(arg interface{}) *Scanner {
   208  	s.PushToken(Token{Value: arg})
   209  	return s
   210  }
   211  
   212  // PushTyped pushes a typed token onto the front of the Scanner.
   213  func (s *Scanner) PushTyped(arg interface{}, typ TokenType) *Scanner {
   214  	s.PushToken(Token{Value: arg, Type: typ})
   215  	return s
   216  }
   217  
   218  // PushToken pushes a preconstructed Token onto the front of the Scanner.
   219  func (s *Scanner) PushToken(token Token) *Scanner {
   220  	s.args = append([]Token{token}, s.args...)
   221  	return s
   222  }