Files
convy/internal/cli/suitelist.go
T
av 23d88c4048 проектные команды и ссылки на источник
- заведён internal/source: уровни ссылаются друг на друга путём на диске
  или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается
  заново и удаляется, кэша нет
- добавлены init, add, pull, list, check в проекте — манифест
  .conventions.toml, сборка копий по разу на компонент, маркер локальной
  части, READING.md рядом
- проверки формы развязаны с набором: принимают lang.Vocabulary, а язык
  копии узнаётся по строке о версии — манифеста рядом с ней нет
2026-07-27 20:42:18 +03:00

166 lines
4.5 KiB
Go

package cli
import (
"flag"
"fmt"
"io"
"sort"
"strings"
"git.vakhrushev.me/av/convy/internal/doc"
"git.vakhrushev.me/av/convy/internal/lang"
"git.vakhrushev.me/av/convy/internal/suite"
)
// suite list answers two questions. Bare, it says what the suite holds: topics,
// their layers, the prefixes taken. Given an axis, it says what a component
// would take — which is the read-only half of assembly, and the reason to build
// it before anything starts writing copies into other repositories.
func runSuiteList(env Env, args []string) ExitCode {
fs := flag.NewFlagSet("convy suite list", flag.ContinueOnError)
fs.SetOutput(env.Err)
root := fs.String("root", "", "root of the suite; by default it is looked up upwards")
topic := fs.String("topic", "", "show one topic only")
langAxis := fs.String("lang", "", "language of the component, to show what it would take")
stackAxis := fs.String("stack", "", "stack of the component, comma-separated, to show what it would take")
retired := fs.Bool("retired", false, "show the retired names instead of the live ones")
if err := fs.Parse(args); err != nil {
return Usage
}
dir, code := suiteRoot(env, *root)
if code != OK {
return code
}
s, err := suite.Load(dir)
if err != nil {
fmt.Fprintln(env.Err, err)
return Usage
}
if *retired {
listRetired(env.Out, s)
return OK
}
topics := s.Manifest.LiveTopics()
if *topic != "" {
if !s.Manifest.TopicLive(*topic) {
fmt.Fprintf(env.Err, "the suite declares no live topic %q\n", *topic)
return Usage
}
topics = []string{*topic}
}
// The path column is sized to the suite rather than guessed: a name that
// overruns a fixed width breaks every row below it.
width := 0
for _, d := range s.Docs {
width = max(width, len(d.Path))
}
selecting := *langAxis != "" || *stackAxis != ""
component := suite.Component{Lang: split(*langAxis), Stack: split(*stackAxis)}
for i, name := range topics {
if i > 0 {
fmt.Fprintln(env.Out)
}
listTopic(env.Out, s, name, component, selecting, width)
}
if !selecting {
fmt.Fprintf(env.Out, "\n%s, %s, language version %d (%s)\n",
plural(len(topics), "topic"), plural(len(s.Docs), "file"),
s.Manifest.Language.Version, s.Manifest.Language.Lang)
}
return OK
}
func listTopic(w io.Writer, s *suite.Suite, name string, c suite.Component, selecting bool, width int) {
fmt.Fprintf(w, "%s — %s\n", name, s.Manifest.Topics.Live[name])
if !selecting {
for _, d := range s.Layers(name) {
fmt.Fprintln(w, " "+layerLine(s, d, width))
}
return
}
taken, left := s.Assemble(name, c)
if len(taken) == 0 {
fmt.Fprintln(w, " nothing: the topic has no layer this component takes")
}
for _, d := range taken {
fmt.Fprintln(w, " "+layerLine(s, d, width))
}
for _, d := range left {
fmt.Fprintln(w, " · "+layerLine(s, d, width)+" left out")
}
}
// layerLine describes one layer: its prefix, where it lies, what axis it is on
// and how much of it there is.
func layerLine(s *suite.Suite, d *doc.Document, width int) string {
rules, retired := 0, 0
for _, r := range d.Rules {
rules++
if _, ok := r.Block(lang.Retired); ok {
retired++
}
}
count := plural(rules, "rule")
if retired > 0 {
count += fmt.Sprintf(", %d retired", retired)
}
return fmt.Sprintf("%-6s %-*s %-22s %s", s.Prefix(d), width, d.Path, suite.Axis(d), count)
}
func listRetired(w io.Writer, s *suite.Suite) {
sections := []struct {
title string
entries map[string]string
}{
{"topics", s.Manifest.Topics.Retired},
{"prefixes", s.Manifest.Prefixes.Retired},
}
empty := true
for _, section := range sections {
if len(section.entries) == 0 {
continue
}
empty = false
fmt.Fprintf(w, "%s\n", section.title)
for _, key := range sortedKeys(section.entries) {
fmt.Fprintf(w, " %-16s %s\n", key, section.entries[key])
}
}
if empty {
fmt.Fprintln(w, "nothing has been retired yet")
return
}
fmt.Fprintln(w, "\nnone of these names is ever handed out again")
}
// 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
}
func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}