Files
convy/internal/cli/cli.go
T
av b6b0976c19 исправлены находки ревью проектной стороны
- манифест читается так, как записан: решётка внутри строки не открывает
  комментарий, скобка внутри комментария не закрывает массив, имя внутри
  комментария не становится подпиской; новый ключ встаёт после массива,
  а не внутрь него
- всё записываемое проходит через manifest.Quote — обратный слэш в пути
  делал файл, который инструмент сам не читает
- маркер локальной части переехал в doc и пропускает огороженные блоки:
  процитированный в примере маркер больше не считается границей, а копия
  без маркера не перезаписывается молча
- лишний позиционный аргумент отсекается: flag прекращал разбор и прятал
  флаги после себя, из-за чего pull, list и check игнорировали --for
- заведены тесты проверок копий, включая молчание на исправной копии
2026-07-27 21:16:29 +03:00

173 lines
5.1 KiB
Go

// Package cli lays out the commands of the tool.
//
// The depth of a command reflects how often it runs and whom it addresses:
// project commands run in every repository and often, tending a suite runs in
// one repository and rarely. That is why `check` stays at the top level and
// whatever makes no sense in a project goes under `suite`. There are no
// synonyms: `convy suite pull` is not introduced next to `convy pull`.
package cli
import (
"fmt"
"io"
"os"
"strings"
)
// ExitCode is the exit status of the process.
type ExitCode int
const (
// OK means the check passed and the work is done.
OK ExitCode = 0
// Failed means the check found errors.
Failed ExitCode = 1
// Usage means the command was typed wrong or run in the wrong context.
Usage ExitCode = 2
)
// Env is the environment of a run. It is pulled out so that commands can be
// tested without a process.
type Env struct {
Dir string
In io.Reader
Out io.Writer
Err io.Writer
// Interactive says whether input comes from a terminal. A command that
// asks questions refuses to start without one rather than blocking on an
// answer nobody is there to give.
Interactive bool
}
// Run parses the arguments and executes the command.
func Run(env Env, args []string) ExitCode {
if len(args) == 0 {
usage(env.Out)
return Usage
}
switch args[0] {
case "suite":
return runSuite(env, args[1:])
case "init":
return runInit(env, args[1:])
case "add":
return runAdd(env, args[1:])
case "pull":
return runPull(env, args[1:])
case "list":
return runList(env, args[1:])
case "check":
return runCheck(env, args[1:])
case "help", "-h", "--help":
usage(env.Out)
return OK
default:
fmt.Fprintf(env.Err, "unknown command %q\n\n", args[0])
usage(env.Err)
return Usage
}
}
func runSuite(env Env, args []string) ExitCode {
if len(args) == 0 {
fmt.Fprintln(env.Err, "convy suite: a subcommand is required — init, add, rule, retire, list or check")
return Usage
}
switch args[0] {
case "check":
return runSuiteCheck(env, args[1:])
case "init":
return runSuiteInit(env, args[1:])
case "add":
return runSuiteAdd(env, args[1:])
case "rule":
return runSuiteRule(env, args[1:])
case "retire":
return runSuiteRetire(env, args[1:])
case "list":
return runSuiteList(env, args[1:])
default:
fmt.Fprintf(env.Err, "unknown subcommand %q for convy suite\n", args[0])
return Usage
}
}
// usage prints the help grouped under headings: a flat list hides the levels.
func usage(w io.Writer) {
fmt.Fprint(w, `convy — tending development conventions.
In a project:
convy init wire up conventions: the source and the first component
convy add <topic> subscribe and assemble
convy pull reassemble what is subscribed
convy list what is wired up and what else the suite has
convy check check the form of what is here
In a suite:
convy suite init start a suite: a directory and a manifest
convy suite add add a convention: a file, a topic and a prefix
convy suite rule add a rule: the next number, the blocks in order
convy suite retire retire a rule, a convention or a topic — never reusing it
convy suite list what the suite holds, and what a component would take
convy suite check suite integrity: prefixes, topics, axes, links, form
The commands that change something run in two modes. Bare, they ask for every
field with a hint attached — that mode is for a person. With flags, they take
everything at once and ask nothing — that mode is for agents and scripts.
`)
}
// noStrayArgs turns down an argument the command has no place for. The flag
// package stops parsing at the first argument that is not a flag, so a stray one
// does not merely sit there unused — it hides every flag written after it, and
// the command then does something other than what was asked in silence.
func noStrayArgs(env Env, name string, rest []string) ExitCode {
if len(rest) == 0 {
return OK
}
fmt.Fprintf(env.Err, "%s takes no argument, and %q was given; a component is named by --for\n", name, rest[0])
return Usage
}
// split reads a comma-separated list off the command line. An axis of a
// component is a list — a component may sit on two stacks at once — and one
// flag repeated is worse to type than one flag with commas in it.
func split(value string) []string {
var out []string
for _, part := range strings.Split(value, ",") {
if part = strings.TrimSpace(part); part != "" {
out = append(out, part)
}
}
return out
}
// Main is the entry point of the process.
func Main() int {
dir, err := os.Getwd()
if err != nil {
fmt.Fprintln(os.Stderr, "cannot determine the current directory:", err)
return int(Usage)
}
env := Env{
Dir: dir,
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
Interactive: terminal(os.Stdin),
}
return int(Run(env, os.Args[1:]))
}
// terminal reports whether a file is a character device, which is as close as
// the standard library gets to asking whether a person is on the other end.
func terminal(f *os.File) bool {
info, err := f.Stat()
if err != nil {
return false
}
return info.Mode()&os.ModeCharDevice != 0
}