github.com/alecthomas/kong@v0.9.1-0.20240410131203-2ab5733f1179/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 }