Repository navigation
Expand file tree
/
Copy pathrunner.go
More file actions
165 lines (153 loc) · 3.8 KB
/
Copy pathrunner.go
File metadata and controls
165 lines (153 loc) · 3.8 KB
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
package huhx
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
)
// envTruthy reports whether an environment variable's value should be
// treated as true. Accepts "1", "true"/"TRUE" (and friends via
// strconv.ParseBool), plus "yes"/"YES" as an explicit extension.
func envTruthy(name string) bool {
v := os.Getenv(name)
if v == "" {
return false
}
if strings.EqualFold(v, "yes") {
return true
}
b, err := strconv.ParseBool(v)
return err == nil && b
}
// Runner drives a Form either interactively (delegating to huh) or
// non-interactively (walking groups and injecting answers from configured
// sources).
type Runner struct {
form *Form
mode Mode
envPrefix string
answers map[string]any
answerFile string
cobraCmd *cobra.Command
}
// New returns a Runner. Default mode is AutoDetect.
func New(form *Form, opts ...Option) *Runner {
r := &Runner{form: form, mode: AutoDetect}
for _, opt := range opts {
opt(r)
}
return r
}
// Run executes the form. In interactive mode it delegates to huh.Form.Run().
// In non-interactive mode it walks groups and resolves each field's answer
// from the configured sources, returning a descriptive error if any
// required answer is missing or any validator fails.
func (r *Runner) Run() error {
if r.isNonInteractive() {
return r.runNonInteractive()
}
return r.form.inner.Run()
}
// isNonInteractive reports whether the runner should skip the bubble tea
// loop and resolve answers from configured sources instead.
func (r *Runner) isNonInteractive() bool {
switch r.mode {
case Always:
return true
case Never:
return false
}
if envTruthy("NON_INTERACTIVE") || envTruthy("CI") {
return true
}
if !isatty.IsTerminal(os.Stdin.Fd()) && !isatty.IsCygwinTerminal(os.Stdin.Fd()) {
return true
}
if r.cobraCmd != nil {
if f := r.cobraCmd.Flags().Lookup("non-interactive"); f != nil && f.Changed && f.Value.String() == "true" {
return true
}
}
return false
}
func (r *Runner) runNonInteractive() error {
fileAns, err := loadAnswerFile(r.answerFile)
if err != nil {
return err
}
cliAns, err := cobraAnswerPairs(r.cobraCmd)
if err != nil {
return err
}
var missing []string
for gi, g := range r.form.groups {
if g.hide != nil && g.hide() {
continue
}
for fi, f := range g.fields {
if f.key() == "" {
if f.required() {
return fmt.Errorf("required field %d in group %d has no Key() set; call .Key(\"...\") on it to enable non-interactive mode", fi+1, gi+1)
}
continue
}
ans, ok := r.resolve(f.key(), fileAns, cliAns)
if !ok {
if f.required() {
missing = append(missing, f.key())
}
continue
}
if err := f.set(ans); err != nil {
return fmt.Errorf("field %q: %w", f.key(), err)
}
}
}
if len(missing) > 0 {
return r.missingErr(missing)
}
return nil
}
func (r *Runner) resolve(key string, fileAns, cliAns map[string]string) (string, bool) {
if v, ok := r.answers[key]; ok && v != nil {
return fmt.Sprintf("%v", v), true
}
if r.cobraCmd != nil {
if f := r.cobraCmd.Flags().Lookup(key); f != nil && f.Changed {
return f.Value.String(), true
}
}
if v, ok := cliAns[key]; ok {
return v, true
}
if v, ok := fileAns[key]; ok {
return v, true
}
if r.envPrefix != "" {
if v, ok := os.LookupEnv(envKey(r.envPrefix, key)); ok {
return v, true
}
}
return "", false
}
func (r *Runner) missingErr(missing []string) error {
width := 0
for _, k := range missing {
if w := len(k) + 2; w > width {
width = w
}
}
var b strings.Builder
b.WriteString("missing required answers for:\n")
for _, k := range missing {
fmt.Fprintf(&b, " %-*s", width, "--"+k)
if env := envKey(r.envPrefix, k); env != "" {
fmt.Fprintf(&b, " (env: %s)", env)
}
b.WriteByte('\n')
}
return errors.New(strings.TrimRight(b.String(), "\n"))
}