Files
convy/internal/cli/prompt.go
T
av b29b5b5e6f suite init и suite add: заведение набора и конвенции в двух режимах
- suite init создаёт директорию и манифест со скелетом таблиц; suite add пишет
  файл конвенции и вправляет запись в suite.toml, сохраняя комментарии
- без аргументов команды спрашивают поля с подсказками, с флагами берут всё
  сразу и не спрашивают ничего; без терминала пустой вызов отказывает
- строка о версии языка генерируется из словаря набора, поэтому созданный файл
  проходит suite check без правок
- починено разрешение extends: короткая форма бралась по суффиксу и могла
  указать на сам файл; теперь неоднозначность либо избегается при записи, либо
  сообщается ошибкой
2026-07-27 10:40:57 +03:00

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
}