forked from alecthomas/kong
-
Notifications
You must be signed in to change notification settings - Fork 0
/
scanner.go
222 lines (193 loc) · 5.29 KB
/
scanner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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
}
// ScanAsType creates a new Scanner from args with the given type.
func ScanAsType(ttype TokenType, args ...string) *Scanner {
s := &Scanner{}
for _, arg := range args {
s.args = append(s.args, Token{Value: arg, Type: ttype})
}
return s
}
// Scan creates a new Scanner from args with untyped tokens.
func Scan(args ...string) *Scanner {
return ScanAsType(UntypedToken, args...)
}
// 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
}