Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,12 @@ argv.UsageLine([]string{"mise"}, mise.Root, mise.HelpText)
`Arguments`, `Flags` and `Global flags`, with the columns lined up and the
inherited globals worked out the way the parser resolves them.

**All 211 of mise's usage lines and all 211 of its pages match usage-lib's byte
for byte**, which is the test that keeps both honest. usage-lib builds the line from a spec through a
`argv.LongHelp` renders `--help`: the same content through a wider layout, with
help wrapped into a column, the long form of each description preferred, and each
annotation on its own line.

**All 211 usage lines, all 211 `-h` pages and all 211 `--help` pages match
usage-lib byte for byte**, which is the test that keeps them honest. usage-lib builds the line from a spec through a
template over a runtime model; this builds it from static tables. Reimplemented
rules drift, so both are run over mise's real spec and compared — the same check
`benches/gate/tests/help.rs` makes for usage-argv, against the same reference.
Expand Down Expand Up @@ -187,9 +191,6 @@ claim is measured at real scale rather than against a fixture with four flags:
- **Typed values.** Binding collects text. Something still has to turn `"8"` into
an `int` and `"1m"` into a `time.Duration`, and report the ones that will not
convert.
- **The long page.** `-h` is done; `--help` wraps long descriptions and switches
to a two-line layout for entries that have a longer form, which `ShortHelp`
does not do.
- **Errors worth reading.** `Error()` returns `unknown flag: --wat`, which names
the problem and helps nobody fix it. usage-argv renders these through miette
with the offending token underlined.
Expand Down
8 changes: 5 additions & 3 deletions go/argv/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ type Help struct {
Env string
Default []string
// BeforeHelp and AfterHelp bracket this command's page, overriding the
// spec-wide text.
BeforeHelp string
AfterHelp string
// spec-wide text. The long variants are preferred by `--help`.
BeforeHelp string
AfterHelp string
BeforeLongHelp string
AfterLongHelp string
// Examples are worked invocations, printed last.
Examples []Example
}
Expand Down
59 changes: 50 additions & 9 deletions go/argv/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package argv
import (
"sort"
"strings"
"unicode"
)

// The page `-h` prints.
Expand All @@ -26,15 +27,23 @@ type HelpSpec struct {
// About is the root's description, which the root's page uses in place of the
// command's own.
About string
// BeforeHelp and AfterHelp bracket every page that does not override them.
BeforeHelp string
AfterHelp string
// LongAbout is what `--help` prefers over About.
LongAbout string
// BeforeHelp and AfterHelp bracket every page that does not override them,
// and the long variants are what `--help` prefers.
BeforeHelp string
AfterHelp string
BeforeLongHelp string
AfterLongHelp string
}

// Example is one worked invocation, as a page prints it.
type Example struct {
Header string
Code string
// Help introduces the line on the long page, printed above the command
// rather than beside it.
Help string
Comment thread
cursor[bot] marked this conversation as resolved.
}

// shortCol is the width the short-flag column is padded to, so that `-J, --json`
Expand Down Expand Up @@ -80,7 +89,10 @@ func ShortHelp(spec HelpSpec, path []string, chain []*Command, help HelpTable) s
} else if meta != nil {
about = meta.Short
}
if about != "" {
// Trimmed as the long page trims it, and for the same reason: the blank line
// under a description belongs to the renderer, so one already in the text is a
// second one.
if about := trimEnd(about); about != "" {
out.WriteString(about + "\n\n")
}

Expand Down Expand Up @@ -161,7 +173,7 @@ func ShortHelp(spec HelpSpec, path []string, chain []*Command, help HelpTable) s
func(int) string { return "" },
func(w *strings.Builder, i int) { entry(w, inherited[i]) })

examplesSection(&out, meta)
examplesSection(&out, pageExamples(chain, help, meta))

after := spec.AfterHelp
if meta != nil && meta.AfterHelp != "" {
Expand Down Expand Up @@ -201,7 +213,7 @@ func commandsSection(out *strings.Builder, path []string, cmd *Command, help Hel
out.WriteString("\nCommands:\n")

// Sorted by the rendered usage rather than by name, as usage-lib sorts them.
sort.SliceStable(lines, func(i, j int) bool { return lines[i].usage < lines[j].usage })
sortLines(lines, func(i int) string { return lines[i].usage })

for _, l := range lines {
out.WriteString(" " + l.usage)
Expand Down Expand Up @@ -256,12 +268,30 @@ func groupsSection(out *strings.Builder, defaultTitle string, n int,
}
}

func examplesSection(out *strings.Builder, meta *Help) {
if meta == nil || len(meta.Examples) == 0 {
// pageExamples is a command's own examples, or the root's where it has none.
//
// The same fallback `BeforeHelp` and `AfterHelp` get, and for the same reason: a
// CLI writing examples once at the top means them to appear. mise declares none
// at its root, so the 211-page parity test cannot see this either way — it is
// checked against the reference's rule rather than against the fixture.
func pageExamples(chain []*Command, help HelpTable, meta *Help) []Example {
if meta != nil && len(meta.Examples) > 0 {
return meta.Examples
}
if len(chain) > 0 {
if root := help.Lookup(chain[0].Key); root != nil {
return root.Examples
}
}
return nil
}

func examplesSection(out *strings.Builder, examples []Example) {
if len(examples) == 0 {
return
}
out.WriteString("\nExamples:\n")
for _, e := range meta.Examples {
for _, e := range examples {
if e.Header != "" {
out.WriteString(" " + e.Header + ":\n")
}
Expand Down Expand Up @@ -325,6 +355,17 @@ func pad(s string, col int) string {

func width(s string) int { return len([]rune(s)) }

// trimEnd drops trailing whitespace, which is what `str::trim_end` does on the
// two sides this is ported from.
func trimEnd(s string) string { return strings.TrimRightFunc(s, unicode.IsSpace) }

// sortLines orders a section's entries by their rendered usage, which is how
// usage-lib orders them — for a command with no flags or arguments that agrees
// with sorting by name, and where it differs this is what a reader sees.
func sortLines[T any](lines []T, key func(int) string) {
sort.SliceStable(lines, func(i, j int) bool { return key(i) < key(j) })
}

func min(a, b int) int {
if a < b {
return a
Expand Down
Loading