- suite retire снимает правило, конвенцию или тему: правило остаётся заглушкой с датой и причиной, имя уезжает в раздел выбывших, снятие с непогашенными ссылками отклоняется с перечнем мест - добавлена проверка META-30: короткое описание языка обязано называть все слова словаря, иначе читатель копии толкует их по памяти - retired-раздел манифеста больше не считается объявлением пути: снятый префикс означает, что файл ушёл вместе с ним
150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// Commands that change something run in two modes, and which one is meant is
|
|
// read off the command line: bare means interactive, any flag means automatic.
|
|
//
|
|
// The two modes address two different callers. A person types the command and
|
|
// is walked through the fields with a hint for each; an agent or a script
|
|
// passes every field at once and must never be blocked waiting on a terminal
|
|
// that is not there. Neither mode guesses: a field that is required and absent
|
|
// stops the run in both.
|
|
|
|
// Field is one thing a command asks for.
|
|
type Field struct {
|
|
// Flag is the name the field carries on the command line.
|
|
Flag string
|
|
// Ask is the question put to a person.
|
|
Ask string
|
|
// Hint is the one line explaining what belongs here and why.
|
|
Hint string
|
|
// Default is offered as the answer when the person just presses enter.
|
|
Default string
|
|
// Optional fields may stay empty.
|
|
Optional bool
|
|
// Options, when given, are the only answers accepted.
|
|
Options []string
|
|
// Check validates an answer; it is applied in both modes.
|
|
Check func(string) error
|
|
}
|
|
|
|
// errStop ends the dialogue on end of input.
|
|
var errStop = errors.New("input ended")
|
|
|
|
// dialogue asks the fields one by one, re-asking what did not pass validation.
|
|
type dialogue struct {
|
|
in *bufio.Reader
|
|
out io.Writer
|
|
}
|
|
|
|
func newDialogue(env Env) *dialogue {
|
|
return &dialogue{in: bufio.NewReader(env.In), out: env.Out}
|
|
}
|
|
|
|
// ask puts one question and returns the answer, looping until it validates.
|
|
func (d *dialogue) ask(f Field) (string, error) {
|
|
for {
|
|
if f.Hint != "" {
|
|
fmt.Fprintf(d.out, "\n%s\n", f.Hint)
|
|
}
|
|
prompt := f.Ask
|
|
switch {
|
|
case len(f.Options) > 0:
|
|
prompt += fmt.Sprintf(" (%s)", strings.Join(f.Options, " / "))
|
|
if f.Default != "" {
|
|
prompt += fmt.Sprintf(" [%s]", f.Default)
|
|
}
|
|
case f.Default != "":
|
|
prompt += fmt.Sprintf(" [%s]", f.Default)
|
|
case f.Optional:
|
|
prompt += " [may stay empty]"
|
|
}
|
|
fmt.Fprintf(d.out, "%s: ", prompt)
|
|
|
|
line, err := d.in.ReadString('\n')
|
|
answer := strings.TrimSpace(line)
|
|
if answer == "" && err != nil {
|
|
return "", errStop
|
|
}
|
|
if answer == "" {
|
|
answer = f.Default
|
|
}
|
|
if answer == "" && !f.Optional {
|
|
fmt.Fprintln(d.out, " the field is required")
|
|
continue
|
|
}
|
|
if err := validate(f, answer); err != nil {
|
|
fmt.Fprintf(d.out, " %s\n", err)
|
|
continue
|
|
}
|
|
return answer, nil
|
|
}
|
|
}
|
|
|
|
// confirm asks a yes-or-no question before something irreversible, defaulting
|
|
// to no: a retirement that went through because the reader pressed enter is the
|
|
// one mistake the model cannot undo.
|
|
func (d *dialogue) confirm(question string) (bool, error) {
|
|
fmt.Fprintf(d.out, "\n%s [y/N]: ", question)
|
|
line, err := d.in.ReadString('\n')
|
|
answer := strings.ToLower(strings.TrimSpace(line))
|
|
if answer == "" && err != nil {
|
|
return false, errStop
|
|
}
|
|
return answer == "y" || answer == "yes", nil
|
|
}
|
|
|
|
// validate applies a field's check to a value that is not empty.
|
|
func validate(f Field, value string) error {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
if len(f.Options) > 0 && !slices.Contains(f.Options, value) {
|
|
return fmt.Errorf("one of: %s", strings.Join(f.Options, ", "))
|
|
}
|
|
if f.Check == nil {
|
|
return nil
|
|
}
|
|
return f.Check(value)
|
|
}
|
|
|
|
// resolve settles the fields in automatic mode: what came on the command line
|
|
// is validated, what is required and missing is named. Every missing field is
|
|
// reported at once — being sent back one flag at a time is the worst way to
|
|
// learn what a command wants.
|
|
func resolve(fields []Field, given map[string]string) error {
|
|
var missing []string
|
|
var problems []string
|
|
for _, f := range fields {
|
|
value := given[f.Flag]
|
|
if value == "" {
|
|
value = f.Default
|
|
given[f.Flag] = value
|
|
}
|
|
if value == "" {
|
|
if !f.Optional {
|
|
missing = append(missing, "--"+f.Flag)
|
|
}
|
|
continue
|
|
}
|
|
if err := validate(f, value); err != nil {
|
|
problems = append(problems, fmt.Sprintf("--%s: %s", f.Flag, err))
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
problems = append(problems, "required and missing: "+strings.Join(missing, ", "))
|
|
}
|
|
if len(problems) > 0 {
|
|
return errors.New(strings.Join(problems, "\n"))
|
|
}
|
|
return nil
|
|
}
|