suite init и suite add: заведение набора и конвенции в двух режимах
- suite init создаёт директорию и манифест со скелетом таблиц; suite add пишет файл конвенции и вправляет запись в suite.toml, сохраняя комментарии - без аргументов команды спрашивают поля с подсказками, с флагами берут всё сразу и не спрашивают ничего; без терминала пустой вызов отказывает - строка о версии языка генерируется из словаря набора, поэтому созданный файл проходит suite check без правок - починено разрешение extends: короткая форма бралась по суффиксу и могла указать на сам файл; теперь неоднозначность либо избегается при записи, либо сообщается ошибкой
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user