- убраны комментарии из suite.toml и .conventions.toml: файл, который машина переписывает, комментарий через круг не проносит; объяснения ушли в README рядом, который suite init теперь заводит - удалена текстовая правка манифеста целиком — 520 строк ручного лексера TOML вместе со всем классом ошибок порчи данных - запись идёт из структур энкодером; ключ, которого инструмент не знает, запись останавливает, а не теряется молча - convy sync сверяет манифест и подводит под него раскладку файлов: чего не хватает — собирает, что осиротело — удаляет, копию с локальной частью не трогает никогда
176 lines
5.2 KiB
Go
176 lines
5.2 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 "sync":
|
|
return runSync(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, text and all
|
|
convy sync make the files follow the manifest, and say what is off
|
|
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
|
|
}
|