File size: 5,699 Bytes
e089492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
package shellconfig

import (
	"encoding/json"
	"fmt"
	"io"
	"maps"
	"strconv"
	"strings"
)

// flagKind is the value type a flag parses from the command line.
type flagKind int

const (
	flagString flagKind = iota
	flagBool
	// flagBoolTrue is a valueless boolean flag (e.g. --think) that stores
	// true when present, without consuming an argument.
	flagBoolTrue
	flagInt
	flagFloat
	// flagKeyValue consumes two args (KEY VALUE) and stores them as a map
	// entry, e.g. --env NAME VALUE.
	flagKeyValue
	// flagJSONObject parses the value as a JSON object (map), rejecting
	// arrays and scalars.
	flagJSONObject
	// flagJSONAny parses the value as arbitrary JSON.
	flagJSONAny
)

// flagOp is how a parsed flag value is written into the target map.
type flagOp int

const (
	// opSet assigns target[jsonKey] = value.
	opSet flagOp = iota
	// opAppend appends value to the []any at target[jsonKey].
	opAppend
	// opSetChild assigns childMap(target, child)[jsonKey] = value, e.g. a
	// single --env KEY VALUE entry under an "env" object.
	opSetChild
	// opMergeChild merges a JSON object into childMap(target, child), e.g.
	// --provider-options '{...}'.
	opMergeChild
)

// flagSpec declares one command-line flag: how it parses, where it writes,
// and an optional validator. A builtin's whole flag surface is a []flagSpec
// handed to applyFlags, which replaces the per-builtin parse loops.
type flagSpec struct {
	name    string // long flag including dashes, e.g. "--api-key"
	jsonKey string // destination key in the target map
	kind    flagKind
	op      flagOp
	child   string // child map name for opSetChild / opMergeChild

	// validate, if non-nil, checks the parsed value before it is stored.
	// It receives the value as string, bool, int64, float64, or
	// map[string]any depending on kind.
	validate func(any) error
}

// applyFlags parses args[start:] against specs and writes the results into
// target. cmd names the invoking command for error messages (e.g.
// "provider add"). An unrecognized flag is an error.
func applyFlags(specs []flagSpec, args []string, start int, target map[string]any, cmd string, stderr io.Writer) error {
	i := start
	for i < len(args) {
		spec, ok := findFlag(specs, args[i])
		if !ok {
			return usage(stderr, fmt.Sprintf("%s: unknown flag %s", cmd, args[i]))
		}

		val, next, err := parseFlagValue(spec, args, i)
		if err != nil {
			return usage(stderr, err.Error())
		}
		if spec.validate != nil {
			if err := spec.validate(val); err != nil {
				return usage(stderr, fmt.Sprintf("%s: %s", cmd, err))
			}
		}
		storeFlag(target, spec, val)
		i = next
	}
	return nil
}

func findFlag(specs []flagSpec, name string) (flagSpec, bool) {
	for _, s := range specs {
		if s.name == name {
			return s, true
		}
	}
	return flagSpec{}, false
}

// parseFlagValue reads the value(s) for spec starting at args[i] and returns
// the parsed value plus the index to resume from.
func parseFlagValue(spec flagSpec, args []string, i int) (any, int, error) {
	name := strings.TrimPrefix(spec.name, "--")

	switch spec.kind {
	case flagString:
		v, err := nextArg(args, i, name)
		if err != nil {
			return nil, 0, err
		}
		return v, i + 2, nil

	case flagBoolTrue:
		return true, i + 1, nil

	case flagBool:
		v, err := nextArg(args, i, name)
		if err != nil {
			return nil, 0, err
		}
		b, err := parseBool(v)
		if err != nil {
			return nil, 0, fmt.Errorf("%s: --%s expects true/false, got %q", args[0], name, v)
		}
		return b, i + 2, nil

	case flagInt:
		v, err := nextArg(args, i, name)
		if err != nil {
			return nil, 0, err
		}
		n, err := strconv.ParseInt(v, 10, 64)
		if err != nil {
			return nil, 0, fmt.Errorf("%s: --%s expects an integer, got %q", args[0], name, v)
		}
		return n, i + 2, nil

	case flagFloat:
		v, err := nextArg(args, i, name)
		if err != nil {
			return nil, 0, err
		}
		f, err := strconv.ParseFloat(v, 64)
		if err != nil {
			return nil, 0, fmt.Errorf("%s: --%s expects a number, got %q", args[0], name, v)
		}
		return f, i + 2, nil

	case flagKeyValue:
		if i+2 >= len(args) {
			return nil, 0, fmt.Errorf("%s: --%s requires a key and value", args[0], name)
		}
		return [2]string{args[i+1], args[i+2]}, i + 3, nil

	case flagJSONObject:
		v, err := nextArg(args, i, name)
		if err != nil {
			return nil, 0, err
		}
		var object map[string]any
		if err := json.Unmarshal([]byte(v), &object); err != nil || object == nil {
			return nil, 0, fmt.Errorf("%s: --%s expects a JSON object, got %q", args[0], name, v)
		}
		return object, i + 2, nil

	case flagJSONAny:
		v, err := nextArg(args, i, name)
		if err != nil {
			return nil, 0, err
		}
		var parsed any
		if err := json.Unmarshal([]byte(v), &parsed); err != nil {
			return nil, 0, fmt.Errorf("%s: --%s expects valid JSON, got %q: %s", args[0], name, v, err)
		}
		return parsed, i + 2, nil

	default:
		return nil, 0, fmt.Errorf("%s: --%s has unknown flag kind", args[0], name)
	}
}

// nextArg returns args[i+1], erroring if the flag is missing its value.
func nextArg(args []string, i int, flag string) (string, error) {
	if i+1 >= len(args) {
		return "", fmt.Errorf("%s: --%s requires a value", args[0], flag)
	}
	return args[i+1], nil
}

// storeFlag writes a parsed value into target according to spec.op.
func storeFlag(target map[string]any, spec flagSpec, val any) {
	switch spec.op {
	case opSet:
		target[spec.jsonKey] = val
	case opAppend:
		arr, _ := target[spec.jsonKey].([]any)
		target[spec.jsonKey] = append(arr, val)
	case opSetChild:
		if kv, ok := val.([2]string); ok {
			childMap(target, spec.child)[kv[0]] = kv[1]
		}
	case opMergeChild:
		if obj, ok := val.(map[string]any); ok {
			maps.Copy(childMap(target, spec.child), obj)
		}
	}
}