- suite init создаёт директорию и манифест со скелетом таблиц; suite add пишет файл конвенции и вправляет запись в suite.toml, сохраняя комментарии - без аргументов команды спрашивают поля с подсказками, с флагами берут всё сразу и не спрашивают ничего; без терминала пустой вызов отказывает - строка о версии языка генерируется из словаря набора, поэтому созданный файл проходит suite check без правок - починено разрешение extends: короткая форма бралась по суффиксу и могла указать на сам файл; теперь неоднозначность либо избегается при записи, либо сообщается ошибкой
123 lines
3.2 KiB
Go
123 lines
3.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"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
|
|
// 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 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
|
|
}
|
|
}
|
|
|
|
// validate applies a field's check to a value that is not empty.
|
|
func validate(f Field, value string) error {
|
|
if value == "" || 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
|
|
}
|