help and docs

This commit is contained in:
Arpad Ryszka 2025-12-10 20:31:10 +01:00
parent b9391a862d
commit 60acf6e820
27 changed files with 2995 additions and 1257 deletions

View File

@ -44,7 +44,10 @@ docreflect_test.go: $(sources)
mkdir -p .build mkdir -p .build
.build/wand: $(sources) iniparser.gen.go docreflect.gen.go .build .build/wand: $(sources) iniparser.gen.go docreflect.gen.go .build
go build -o .build/wand -ldflags "-X main.version=$(shell date +%Y-%m-%d)-$(shell git rev-parse --short HEAD)" ./cmd/wand go build \
-o .build/wand \
-ldflags "-X main.version=$(git show -s --format=%cs HEAD)-$(shell git rev-parse --short HEAD)" \
./cmd/wand
install: .build/wand install: .build/wand
cp .build/wand ~/bin cp .build/wand ~/bin

View File

@ -18,11 +18,7 @@ func bindKeyVals(receiver reflect.Value, keyVals map[string][]string) bool {
} }
func bindOptions(receiver reflect.Value, shortForms []string, o []option) bool { func bindOptions(receiver reflect.Value, shortForms []string, o []option) bool {
ms := make(map[string]string) ms := shortFormsToLong(shortForms)
for i := 0; i < len(shortForms); i += 2 {
ms[shortForms[i]] = shortForms[i+1]
}
v := make(map[string][]any) v := make(map[string][]any)
for _, oi := range o { for _, oi := range o {
n := oi.name n := oi.name

View File

@ -225,8 +225,9 @@ func validateShortFormsTree(cmd Cmd) (map[string]string, map[string]string, erro
mf := mapFields(cmd.impl) mf := mapFields(cmd.impl)
_, helpDefined := mf["help"] _, helpDefined := mf["help"]
for i := 0; i < len(cmd.shortForms); i += 2 { l2s := longFormsToShort(cmd.shortForms)
s, l := cmd.shortForms[i], cmd.shortForms[i+1] for l, ss := range l2s {
for _, s := range ss {
if s == "h" && l == "help" && !helpDefined { if s == "h" && l == "help" && !helpDefined {
continue continue
} }
@ -262,6 +263,7 @@ func validateShortFormsTree(cmd Cmd) (map[string]string, map[string]string, erro
delete(unmapped, s) delete(unmapped, s)
mapped[s] = l mapped[s] = l
} }
}
return mapped, unmapped, nil return mapped, unmapped, nil
} }
@ -317,27 +319,10 @@ func boolOptions(cmd Cmd) []string {
n = append(n, "help") n = append(n, "help")
} }
sfm := make(map[string][]string)
for i := 0; i < len(cmd.shortForms); i += 2 {
s, l := cmd.shortForms[i], cmd.shortForms[i+1]
sfm[l] = append(sfm[l], s)
}
var sf []string var sf []string
l2s := longFormsToShort(cmd.shortForms)
for _, ni := range n { for _, ni := range n {
sf = append(sf, sfm[ni]...) sf = append(sf, l2s[ni]...)
}
var hasHelpShortForm bool
for i := 0; i < len(cmd.shortForms); i += 2 {
if cmd.shortForms[i] == "h" {
hasHelpShortForm = true
break
}
}
if !hasHelp && !hasHelpShortForm {
sf = append(sf, "h")
} }
return append(n, sf...) return append(n, sf...)

View File

@ -238,6 +238,33 @@ func TestCommand(t *testing.T) {
testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""), testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""),
) )
f3 := func(o struct{ Foo map[string]string }) string { return o.Foo["bar"] }
cmd = Command("foo", f3)
t.Run(
"free form",
testExec(testCase{impl: cmd, command: "foo --foo-bar baz"}, "option not supported"),
)
cmd = ShortForm(Command("foo", f3), "b", "foo-bar")
t.Run(
"short form for free form",
testExec(testCase{impl: cmd, command: "foo -b baz"}, "unmapped short form"),
)
f4 := func(o map[string]string) string { return o["foo"] }
cmd = ShortForm(Command("foo", f4), "f", "foo")
t.Run(
"short form for free form root",
testExec(testCase{impl: cmd, command: "foo -f baz"}, "unmapped short form"),
)
f5 := func(o struct{ Foo struct{ Bar string } }) string { return o.Foo.Bar }
cmd = ShortForm(Command("foo", f5), "b", "foo-bar")
t.Run(
"short form for child field",
testExec(testCase{impl: cmd, command: "foo -b baz"}, "", "baz"),
)
cmd = Command( cmd = Command(
"foo", "foo",
f0, f0,

View File

@ -273,17 +273,34 @@ func readArgs(boolOptions, args []string) commandLine {
return c return c
} }
func shortFormsToLong(sf []string) map[string]string {
s2l := make(map[string]string)
for i := 0; i < len(sf); i += 2 {
s2l[sf[i]] = sf[i+1]
}
return s2l
}
func longFormsToShort(sf []string) map[string][]string {
l2s := make(map[string][]string)
for i := 0; i < len(sf); i += 2 {
l2s[sf[i+1]] = append(
l2s[sf[i+1]],
sf[i],
)
}
return l2s
}
func hasHelpOption(cmd Cmd, o []option) bool { func hasHelpOption(cmd Cmd, o []option) bool {
var mf map[string][]bind.Field var mf map[string][]bind.Field
if cmd.impl != nil { if cmd.impl != nil {
mf = mapFields(cmd.impl) mf = mapFields(cmd.impl)
} }
sf := make(map[string]string) sf := shortFormsToLong(cmd.shortForms)
for i := 0; i < len(cmd.shortForms); i += 2 {
sf[cmd.shortForms[i]] = cmd.shortForms[i+1]
}
for _, oi := range o { for _, oi := range o {
if !oi.value.isBool { if !oi.value.isBool {
continue continue

View File

@ -2,6 +2,7 @@ package wand
import ( import (
"bytes" "bytes"
"code.squareroundforest.org/arpio/textedit"
"errors" "errors"
"fmt" "fmt"
"github.com/iancoleman/strcase" "github.com/iancoleman/strcase"
@ -62,27 +63,33 @@ func (f *file) Close() error {
} }
func unescapeConfig(s string) string { func unescapeConfig(s string) string {
var ( var b bytes.Buffer
u []rune w := textedit.New(
escaped bool &b,
textedit.Func(
func(r rune, escaped bool) ([]rune, bool) {
if escaped {
return []rune{r}, false
}
if r == '\\' {
return nil, true
}
return []rune{r}, false
},
func(escaped bool) []rune {
if escaped {
return []rune{'\\'}
}
return nil
},
),
) )
r := []rune(s) w.Write([]byte(s))
for _, ri := range r { return b.String()
if escaped {
u = append(u, ri)
continue
}
if ri == '\\' {
escaped = true
continue
}
u = append(u, ri)
}
return string(u)
} }
func unquoteConfig(s string) string { func unquoteConfig(s string) string {

View File

@ -1,23 +0,0 @@
package wand
import (
"code.squareroundforest.org/arpio/notation"
"os"
"testing"
)
func debug(a ...any) {
if !testing.Testing() {
return
}
notation.Fprintln(os.Stderr, a...)
}
func debugw(a ...any) {
if !testing.Testing() {
return
}
notation.Fprintlnw(os.Stderr, a...)
}

View File

@ -2,26 +2,44 @@ package wand
const envDocs = `Every command line option's value can also be provided as an environment variable. Environment variable const envDocs = `Every command line option's value can also be provided as an environment variable. Environment variable
names need to use snake casing like myapp_foo_bar_baz or MYAPP_FOO_BAR_BAZ, or other casing that doesn't include the '-' dash names need to use snake casing like myapp_foo_bar_baz or MYAPP_FOO_BAR_BAZ, or other casing that doesn't include the '-' dash
character, and they need to be prefixed with the name of the application, as in the base name of the command. When both the character, and they need to be prefixed with the name of the application, as in the base name of the command.
environment variable and the command line option is defined, the command line option overrides the environment variable.
Multiple values for the same environment variable can be defined by concatenating the values with the ':' separator When both the environment variable and the command line option is defined, the command line option overrides the environment
variable. Multiple values for the same environment variable can be defined by concatenating the values with the ':' separator
character. When overriding multiple values with command line options, all the environment values of the same field are character. When overriding multiple values with command line options, all the environment values of the same field are
dropped.` dropped.`
const configOptionDocs = `the config option allows to define zero or more configuration files at arbitrary path` const configOptionDocs = `the config option allows to define zero or more configuration files at arbitrary path`
const configDocs = `Every command line option's value can also be provided as an entry in a configuration file. Configuration const configDocs = `Every command line option's value can also be provided as an entry in a configuration file.
file entries can use keys with different casings, e.g. snake case foo_bar_baz, or kebab case foo-bar-baz. The keys of the
entries can use a limited set of characters: [a-zA-Z0-9_-], and the first character needs to be one of [a-zA-Z_]. Entry Configuration file entries can use keys with different casings, e.g. snake case foo_bar_baz, or kebab case foo-bar-baz. The
values can consist of any characters, except for newline, control characters, " (quote) and \ (backslash), or the values can keys of the entries can use a limited set of characters: [a-zA-Z0-9_-], and the first character needs to be one of [a-zA-Z_].
be quoted, in which case they can consist of any characters, spanning multiple lines, and only the " (quote) and \ (backslash) Entry values can consist of any characters, except for newline, control characters, " (quote) and \ (backslash), or the values
characters need to be escaped by the \ (backslash) character. Configuration files allow multiple entries with the same key, can be quoted, in which case they can consist of any characters, spanning multiple lines, and only the " (quote) and \
when if the associated command line option also allows multiple instances (marked with [*]). When an entry is defined (backslash) characters need to be escaped by the \ (backslash) character.
multiple configuration files, the effective value is overridden in the order of the definition of the possible config files
(see the listing order below). To discard values defined in the overridden config files without defining new ones, we can Configuration files allow multiple entries with the same key, when if the associated command line option also allows multiple
set entries with only the key, omitting the = key/value separator. Entries in the config files are overridden by the instances (marked with [*]). When an entry is defined multiple configuration files, the effective value is overridden in the
environment variables, when defined, and by the command line options when defined. Config files marked as optional don't order of the definition of the possible config files (see the listing order below). To discard values defined in the overridden
need to be present in the file system, but if they exist, then they must contain valid configuration syntax which is wand's config files without defining new ones, we can set entries with only the key, omitting the = key/value separator. Entries in
flavor of .ini files (https://code.squareroundforest.org/arpio/ini.treerack).` the config files are overridden by the environment variables, when defined, and by the command line options when defined.
Config files marked as optional don't need to be present in the file system, but if they exist, then they must contain valid
configuration syntax which is wand's flavor of .ini files
(https://code.squareroundforest.org/arpio/wand/src/branch/main/ini.treerack).`
const versionDocs = `Print the version of the current binary release.` const versionDocs = `Print the version of the current binary release.`
const (
docListOptions = `Options marked with [*] accept any number of instances.`
docBoolOptions = `Bool options can be used with implicit true values.`
docOptionValues = `Option values can be set both via = or just separated by space.`
)
const docOptionGrouping = `The short form of bool options can be combined. ` +
`The last short form does not need to be a bool option. E.g. -abc=42.`
const docSubcommandHelpFmt = `Show help for each subcommand by calling %s <subcommand> help or %s <subcommand> --help.`
const docSubcommandHint = `Show help for each subcommand by calling <command> help or <command> --help.`

View File

@ -2,9 +2,10 @@
Generated with https://code.squareroundforest.org/arpio/docreflect Generated with https://code.squareroundforest.org/arpio/docreflect
*/ */
package wand package wand
import "code.squareroundforest.org/arpio/docreflect" import "code.squareroundforest.org/arpio/docreflect"
func init() { func init() {
docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)")
@ -15,7 +16,10 @@ docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Cle
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.InlineImport", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.InlineImport", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, commandDir)") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, o, commandDir)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions.DateString", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions.Version", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "")

View File

@ -2,16 +2,21 @@
Generated with https://code.squareroundforest.org/arpio/docreflect Generated with https://code.squareroundforest.org/arpio/docreflect
*/ */
package wand package wand
import "code.squareroundforest.org/arpio/docreflect" import "code.squareroundforest.org/arpio/docreflect"
func init() { func init() {
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib", "") docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Bar", "\nfunc(out, a, b, c)") docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Bar", "\nfunc(out, a, b, c)")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Baz", "\nfunc(o)") docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Baz", "\nfunc(o)")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Foo", "Foo sums three numbers.\n\nfunc(a, b, c)") docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.CustomHelp", "\nfunc(o)")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Foo", "Foo sums three numbers.\nIt prints the sum to stdout.\n\nThe input numbers can be any integer.\n\nfunc(a, b, c)")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.OptionWithHelp", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.OptionWithHelp.Help", "Custom help.\n")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options", "") docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Duration", "") docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Bar", "Bars, any number.\n")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Foo", "") docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Duration", "Duration is another option.\n")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Time", "") docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Foo", "Foo is an option.\n")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Time", "Time is the third option here.\n")
} }

14
exec.go
View File

@ -6,6 +6,7 @@ import (
"io" "io"
"path/filepath" "path/filepath"
"strconv" "strconv"
"time"
) )
func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, conf Config, env, args []string) { func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, conf Config, env, args []string) {
@ -16,8 +17,14 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
return return
} }
cmd = insertHelp(cmd)
// build time documentation generator mode:
if getenv(env, "_wandgenerate") == "man" { if getenv(env, "_wandgenerate") == "man" {
if err := generateMan(stdout, cmd, conf); err != nil { dateString := getenv(env, "_wandgeneratedate")
date, _ := time.Parse(time.DateOnly, dateString)
version := getenv(env, "_wandgenerateversion")
if err := generateMan(stdout, cmd, conf, date, version); err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
exit(1) exit(1)
} }
@ -25,6 +32,7 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
return return
} }
// build time documentation generator mode:
if getenv(env, "_wandgenerate") == "markdown" { if getenv(env, "_wandgenerate") == "markdown" {
level, _ := strconv.Atoi(getenv(env, "_wandmarkdownlevel")) level, _ := strconv.Atoi(getenv(env, "_wandmarkdownlevel"))
if err := generateMarkdown(stdout, cmd, conf, level); err != nil { if err := generateMarkdown(stdout, cmd, conf, level); err != nil {
@ -35,13 +43,11 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
return return
} }
cmd = insertHelp(cmd)
// will need root command for the config and the env: // will need root command for the config and the env:
rootCmd := cmd rootCmd := cmd
cmd, fullCmd, args := selectCommand(cmd, args[1:]) cmd, fullCmd, args := selectCommand(cmd, args[1:])
if cmd.helpFor != nil { if cmd.helpFor != nil {
if err := showHelp(stdout, cmd, conf, fullCmd); err != nil { if err := showHelp(stdout, *cmd.helpFor, conf, fullCmd[:len(fullCmd)-1]); err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
exit(1) exit(1)
} }

View File

@ -118,3 +118,7 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) {
} }
} }
} }
func execTest(t *testing.T, test testCase, err string, expect ...string) {
testExec(test, err, expect...)(t)
}

202
format.go
View File

@ -1,202 +0,0 @@
package wand
import (
"fmt"
"io"
"sort"
"strings"
)
func printer(out io.Writer) (printf func(f string, args ...any), println func(args ...any), finish func() error) {
var err error
printf = func(f string, args ...any) {
if err != nil {
return
}
_, err = fmt.Fprintf(out, f, args...)
}
println = func(args ...any) {
if err != nil {
return
}
_, err = fmt.Fprintln(out, args...)
}
finish = func() error {
return err
}
return
}
func paragraphs(s string) string {
var (
paragraph []string
paragraphs [][]string
)
l := strings.Split(s, "\n")
for i := range l {
l[i] = strings.TrimSpace(l[i])
if l[i] == "" {
if len(paragraph) > 0 {
paragraphs, paragraph = append(paragraphs, paragraph), nil
}
continue
}
paragraph = append(paragraph, l[i])
}
if len(paragraph) > 0 {
paragraphs = append(paragraphs, paragraph)
}
var cparagraphs []string
for _, p := range paragraphs {
cparagraphs = append(cparagraphs, strings.Join(p, " "))
}
return strings.Join(cparagraphs, "\n\n")
}
func lines(s string) string {
p := paragraphs(s)
pp := strings.Split(p, "\n\n")
return strings.Join(pp, "\n")
}
func manParagraphs(s string) string {
p := paragraphs(s)
pp := strings.Split(p, "\n\n")
for i := range pp {
pp[i] = fmt.Sprintf(".PP\n%s", pp[i])
}
return strings.Join(pp, "\n")
}
func manLines(s string) string {
l := lines(s)
ll := strings.Split(l, "\n")
for i := range ll {
ll[i] = fmt.Sprintf(".br\n%s", ll[i])
}
return strings.Join(ll, "\n")
}
func escapeTeletype(s string) string {
r := []rune(s)
for i := range r {
if r[i] >= 0x00 && r[i] <= 0x1f && r[i] != '\n' && r[i] != '\t' {
r[i] = 0xb7
}
if r[i] >= 0x7f && r[i] <= 0x9f {
r[i] = 0xb7
}
}
return string(r)
}
func escapeRoff(s string) string {
var (
rr []rune
lastNewline bool
)
r := []rune(s)
for _, ri := range r {
switch ri {
case '\\', '-', '"':
rr = append(rr, '\\', ri)
case '.', '\'':
if lastNewline {
rr = append(rr, '\\')
}
rr = append(rr, ri)
case 0x2013:
rr = append(rr, []rune("\\(en")...)
case 0x2014:
rr = append(rr, []rune("\\(em")...)
case 0x201c:
rr = append(rr, []rune("\\(lq")...)
case 0x201d:
rr = append(rr, []rune("\\(rq")...)
case 0x2018:
rr = append(rr, []rune("\\(oq")...)
case 0x2019:
rr = append(rr, []rune("\\(cq")...)
default:
rr = append(rr, ri)
}
lastNewline = ri == '\n'
}
return string(rr)
}
func escapeMD(s string) string {
var (
rr []rune
lastDigit bool
)
r := []rune(s)
for _, ri := range r {
switch ri {
case '*', '_', '#', '-', '+', '[', ']', '`', '<', '>', '|', '\\':
rr = append(rr, '\\', ri)
case '.':
if lastDigit {
rr = append(rr, '\\')
}
rr = append(rr, ri)
default:
rr = append(rr, ri)
}
lastDigit = ri >= 0 && ri <= 9
}
return string(rr)
}
func prepareOptions(o []docOption) (names []string, descriptions map[string]string) {
for _, oi := range o {
ons := []string{fmt.Sprintf("--%s", oi.name)}
for _, sn := range oi.shortNames {
ons = append(ons, fmt.Sprintf("-%s", sn))
}
n := strings.Join(ons, ", ")
if oi.isBool {
n = fmt.Sprintf("%s [b]", n)
} else {
n = fmt.Sprintf("%s %s", n, oi.typ)
}
if oi.acceptsMultiple {
n = fmt.Sprintf("%s [*]", n)
}
names = append(names, n)
if descriptions == nil {
descriptions = make(map[string]string)
}
descriptions[n] = oi.description
}
sort.Strings(names)
return
}

View File

@ -1,239 +0,0 @@
package wand
import (
"fmt"
"github.com/iancoleman/strcase"
"io"
"sort"
"strings"
)
func formatHelp(out io.Writer, doc doc) error {
printf, println, finish := printer(out)
printf(escapeTeletype(doc.fullCommand))
println()
if doc.hasImplementation || doc.synopsis.hasSubcommands {
println()
printf("Synopsis")
println()
}
if doc.hasImplementation {
println()
printf(escapeTeletype(doc.synopsis.command))
if doc.synopsis.hasOptions {
printf(" [options...]")
}
for i := range doc.synopsis.arguments.names {
printf(
" [%s %s]",
doc.synopsis.arguments.names[i],
doc.synopsis.arguments.types[i],
)
}
if doc.synopsis.arguments.variadic {
printf("...")
min := doc.synopsis.arguments.minPositional
max := doc.synopsis.arguments.maxPositional
switch {
case min > 0 && max > 0:
printf("\nmin %d and max %d total positional arguments", min, max)
case min > 0:
printf("\nmin %d total positional arguments", min)
case max > 0:
printf("\nmax %d total positional arguments", max)
}
}
println()
}
if doc.synopsis.hasSubcommands {
println()
printf("%s <subcommand> [options or args...]", escapeTeletype(doc.synopsis.command))
println()
println()
printf("(For the details about the available subcommands, see the related section below.)")
println()
}
if doc.description != "" {
println()
printf(escapeTeletype(paragraphs(doc.description)))
println()
}
if len(doc.options) > 0 {
println()
printf("Options")
if doc.hasBoolOptions || doc.hasListOptions {
println()
if doc.hasBoolOptions {
println()
printf("[b]: booelan flag, true or false, or no argument means true")
}
if doc.hasListOptions {
println()
printf("[*]: accepts multiple instances of the same option")
}
}
println()
println()
names, od := prepareOptions(doc.options)
var max int
for _, n := range names {
if len(n) > max {
max = len(n)
}
}
for i := range names {
pad := strings.Join(make([]string, max-len(names[i])+1), " ")
names[i] = fmt.Sprintf("%s:%s", names[i], pad)
}
for _, n := range names {
printf(n)
if od[n] != "" {
printf(" %s", escapeTeletype(lines(od[n])))
}
println()
}
}
if len(doc.subcommands) > 0 {
println()
printf("Subcommands")
println()
println()
var names []string
cd := make(map[string]string)
for _, sc := range doc.subcommands {
name := sc.name
if sc.isDefault {
name = fmt.Sprintf("%s (default)", name)
}
d := sc.description
if sc.isHelp {
d = fmt.Sprintf("Show this help. %s", d)
}
if sc.hasHelpSubcommand {
d = fmt.Sprintf("%s - For help, see: %s %s help", d, escapeTeletype(doc.name), sc.name)
} else if sc.hasHelpOption {
d = fmt.Sprintf("%s - For help, see: %s %s --help", d, escapeTeletype(doc.name), sc.name)
}
cd[name] = d
}
sort.Strings(names)
var max int
for _, n := range names {
if len(n) > max {
max = len(n)
}
}
for i := range names {
pad := strings.Join(make([]string, max-len(names[i])+1), " ")
names[i] = fmt.Sprintf("%s%s", names[i], pad)
}
for _, n := range names {
printf(n)
if cd[n] != "" {
printf(": %s", escapeTeletype(paragraphs(cd[n])))
}
println()
}
}
if len(doc.options) > 0 && commandNameExpression.MatchString(doc.appName) {
println("Environment Variables")
println()
printf(escapeTeletype(paragraphs(envDocs)))
println()
println()
o := doc.options[0]
printf("Example environment variable:")
println()
println()
printf(strcase.ToSnake(fmt.Sprintf("%s-%s", doc.appName, o.name)))
printf("=")
if o.isBool {
printf("true")
} else {
printf("42")
}
println()
}
if len(doc.options) > 0 && len(doc.configFiles) > 0 {
println("Configuration Files")
println()
printf(escapeTeletype(paragraphs(configDocs)))
println()
println()
printf("Config files:")
println()
println()
for _, cf := range doc.configFiles {
if cf.fromOption {
printf("zero or more configuration files defined by the --config option")
println()
continue
}
if cf.fn != "" {
printf(cf.fn)
if cf.optional {
printf(" (optional)")
}
println()
continue
}
}
println()
o := doc.options[0]
printf("Example configuration entry:")
println()
println()
printf("# default for --")
printf(o.name)
printf(":")
println()
printf(strcase.ToSnake(o.name))
printf(" = ")
if o.isBool {
printf("true")
} else {
printf("42")
}
println()
println()
printf("Example for discarding an inherited entry:")
println()
println()
printf("# discarding an inherited entry:")
println()
printf(strcase.ToSnake(o.name))
println()
}
return finish()
}

View File

@ -1,204 +0,0 @@
package wand
import (
"fmt"
"github.com/iancoleman/strcase"
"io"
"strings"
"time"
)
func formatManCommand(printf func(string, ...any), println func(...any), doc doc) {
println(".SH Synopsis")
printf(".B %s", escapeRoff(doc.synopsis.command))
if doc.synopsis.hasOptions {
printf(" [options...]")
}
for i := range doc.synopsis.arguments.names {
printf(
" [%s %s]",
doc.synopsis.arguments.names[i],
doc.synopsis.arguments.types[i],
)
}
min := doc.synopsis.arguments.minPositional
max := doc.synopsis.arguments.maxPositional
if doc.synopsis.arguments.variadic {
println("...")
if min > 0 || max > 0 {
println(".PP")
}
switch {
case min > 0 && max > 0:
printf("min %d and max %d total positional arguments\n", min, max)
case min > 0:
printf("min %d total positional arguments\n", min)
case max > 0:
printf("max %d total positional arguments\n", max)
}
}
if doc.synopsis.hasSubcommands {
if min > 0 || max > 0 {
println(".PP")
}
for i, sc := range doc.subcommands {
if i > 0 {
println(".br")
}
printf("%s %s\n", escapeRoff(doc.name), sc.name)
}
}
if doc.description != "" {
println(".SH Description")
println(manParagraphs(escapeRoff(doc.description)))
}
if len(doc.options) > 0 {
println(".SH Options")
if doc.hasBoolOptions || doc.hasListOptions {
println(".PP")
}
if doc.hasBoolOptions {
println(".B [b]:")
println("booelan flag, true or false, or no argument means true")
}
if doc.hasListOptions {
if doc.hasBoolOptions {
println(".br")
}
println(".B [*]:")
println("accepts multiple instances of the same option")
}
names, descriptions := prepareOptions(doc.options)
for _, n := range names {
println(".TP")
printf(".B %s\n", escapeRoff(n))
if descriptions[n] != "" {
println(manLines(escapeRoff(descriptions[n])))
}
}
}
if len(doc.options) > 0 && commandNameExpression.MatchString(doc.appName) {
println(".SH Environment Variables")
println(manParagraphs(escapeRoff(envDocs)))
println(".PP Example environment variable:")
o := doc.options[0]
println(".TP")
printf(strcase.ToSnake(fmt.Sprintf("%s-%s", doc.appName, o.name)))
printf("=")
if o.isBool {
printf("true")
} else {
printf("42")
}
println()
}
if len(doc.options) > 0 && len(doc.configFiles) > 0 {
println(".SH Configuration Files")
println(manParagraphs(escapeRoff(configDocs)))
println(".PP Config files:")
for i, cf := range doc.configFiles {
if i > 0 {
println(".br")
}
if cf.fromOption {
println(escapeRoff("zero or more configuration files defined by the --config option"))
continue
}
if cf.fn != "" {
printf(escapeRoff(cf.fn))
if cf.optional {
printf(" (optional)")
}
println()
continue
}
}
println(".PP Example configuration entry:")
println(".PP")
o := doc.options[0]
printf(escapeRoff(fmt.Sprintf("# default for --%s:\n", o.name)))
println(".br")
printf(escapeRoff(strcase.ToSnake(o.name)))
printf(" = ")
if o.isBool {
printf("true")
} else {
printf("42")
}
println()
println(".PP Example for discarding an inherited entry:")
println(".PP")
println("# discarding an inherited entry:")
println(".br")
println(escapeRoff(strcase.ToSnake(o.name)))
}
}
func formatManMultiCommand(out io.Writer, doc doc) error {
printf, println, finish := printer(out)
printf(".TH %s 1 %s \"%s\"\n", escapeRoff(doc.appName), escapeRoff(doc.date.Format(time.DateOnly)), escapeRoff(doc.appName))
printf(".SH Name\n%s\n", escapeRoff(doc.appName))
println(".SH Provides several commands:")
println(".PP")
allCommands := allCommands(doc)
for i, c := range allCommands {
if i > 0 {
println(".br")
}
println(escapeRoff(c.fullCommand))
}
for _, c := range allCommands {
printf(".SH %s\n", escapeRoff(strings.ToUpper(c.fullCommand)))
formatManCommand(printf, println, c)
}
return finish()
}
func formatManSingleCommand(out io.Writer, doc doc) error {
printf, println, finish := printer(out)
printf(".TH %s 1 %s \"%s\"\n", escapeRoff(doc.appName), escapeRoff(doc.date.Format(time.DateOnly)), escapeRoff(doc.appName))
printf(".SH Name\n%s\n", escapeRoff(doc.appName))
formatManCommand(printf, println, doc)
return finish()
}
func formatMan(out io.Writer, doc doc) error {
var hasSubcommands bool
for _, sc := range doc.subcommands {
if !sc.isHelp && !sc.isVersion {
continue
}
hasSubcommands = true
break
}
if hasSubcommands {
return formatManMultiCommand(out, doc)
}
return formatManSingleCommand(out, doc)
}

View File

@ -1,201 +0,0 @@
package wand
import (
"fmt"
"github.com/iancoleman/strcase"
"io"
"strings"
)
func header(level int) string {
s := make([]string, level+2)
return strings.Join(s, "#")
}
func formatMarkdownCommand(printf func(string, ...any), println func(...any), doc doc, level int) {
printf("%s Synopsis\n\n", header(level))
println("```")
printf(doc.synopsis.command)
if doc.synopsis.hasOptions {
printf(" [options...]")
}
for i := range doc.synopsis.arguments.names {
printf(
" [%s %s]",
doc.synopsis.arguments.names[i],
doc.synopsis.arguments.types[i],
)
}
if doc.synopsis.arguments.variadic {
printf("...")
}
println()
println("```")
min := doc.synopsis.arguments.minPositional
max := doc.synopsis.arguments.maxPositional
if doc.synopsis.arguments.variadic {
if min > 0 || max > 0 {
println()
}
switch {
case min > 0 && max > 0:
printf("min %d and max %d total positional arguments\n", min, max)
case min > 0:
printf("min %d total positional arguments\n", min)
case max > 0:
printf("max %d total positional arguments\n", max)
}
}
if doc.synopsis.hasSubcommands {
println()
println("```")
for _, sc := range doc.subcommands {
printf("%s %s\n", escapeMD(doc.name), sc.name)
}
println("```")
}
if doc.description != "" {
printf("\n%s Description\n\n", header(level))
println(escapeMD(paragraphs(doc.description)))
}
if len(doc.options) > 0 {
printf("\n%s Options\n\n", header(level))
if doc.hasBoolOptions {
printf("- [b]: booelan flag, true or false, or no argument means true\n")
}
if doc.hasListOptions {
printf("- [*]: accepts multiple instances of the same option\n")
}
if doc.hasBoolOptions || doc.hasListOptions {
println()
}
names, descriptions := prepareOptions(doc.options)
for _, n := range names {
printf("- **%s**: %s\n", escapeMD(n), escapeMD(lines(descriptions[n])))
}
}
if len(doc.options) > 0 && commandNameExpression.MatchString(doc.appName) {
printf("\n.%s Environment Variables\n\n", header(level))
println(escapeMD(paragraphs(envDocs)))
println()
println("Example environment variable:")
println()
o := doc.options[0]
println("```")
printf(strcase.ToSnake(fmt.Sprintf("%s-%s", doc.appName, o.name)))
printf("=")
if o.isBool {
printf("true")
} else {
printf("42")
}
println()
println("```")
}
if len(doc.options) > 0 && len(doc.configFiles) > 0 {
printf("\n.Configuration Files\n\n")
println(escapeMD(paragraphs(configDocs)))
println()
println("Config files:")
println()
for _, cf := range doc.configFiles {
if cf.fromOption {
println("- zero or more configuration files defined by the --config option\n")
continue
}
if cf.fn != "" {
printf("- %s", cf.fn)
if cf.optional {
printf(" (optional)")
}
println()
continue
}
}
println()
println("Example configuration entry:")
println()
o := doc.options[0]
println("```")
printf("# default for --%s:\n", o.name)
printf(strcase.ToSnake(o.name))
printf(" = ")
if o.isBool {
printf("true")
} else {
printf("42")
}
println()
println("```")
println()
println("Example for discarding an inherited entry:")
println()
println("```")
println("# discarding an inherited entry:")
println(strcase.ToSnake(o.name))
println("```")
}
}
func formatMarkdownMultiCommand(out io.Writer, doc doc, level int) error {
printf, println, finish := printer(out)
printf("%s %s\n\n", header(level), escapeMD(doc.appName))
println("Provides several commands:")
println()
allCommands := allCommands(doc)
for _, c := range allCommands {
printf("- %s\n", escapeMD(c.fullCommand))
}
println()
for _, c := range allCommands {
printf("%s %s\n\n", header(level+1), escapeMD(c.fullCommand))
formatMarkdownCommand(printf, println, c, level+2)
}
return finish()
}
func formatMarkdownSingleCommand(out io.Writer, doc doc, level int) error {
printf, println, finish := printer(out)
printf("%s %s\n\n", header(level), doc.appName)
formatMarkdownCommand(printf, println, doc, level+1)
return finish()
}
func formatMarkdown(out io.Writer, doc doc, level int) error {
var hasSubcommands bool
for _, sc := range doc.subcommands {
if !sc.isHelp && !sc.isVersion {
continue
}
hasSubcommands = true
break
}
if hasSubcommands {
return formatMarkdownMultiCommand(out, doc, level)
}
return formatMarkdownSingleCommand(out, doc, level)
}

11
go.mod
View File

@ -1,13 +1,20 @@
module code.squareroundforest.org/arpio/wand module code.squareroundforest.org/arpio/wand
go 1.25.0 go 1.25.3
require ( require (
code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5 code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5
code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1 code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1
code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239 code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239
code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f
code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18
code.squareroundforest.org/arpio/treerack v0.0.0-20251031193114-4f1c219052ae code.squareroundforest.org/arpio/treerack v0.0.0-20251031193114-4f1c219052ae
github.com/iancoleman/strcase v0.3.0 github.com/iancoleman/strcase v0.3.0
golang.org/x/term v0.37.0
) )
require golang.org/x/mod v0.27.0 // indirect require (
code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sys v0.38.0 // indirect
)

18
go.sum
View File

@ -1,20 +1,22 @@
code.squareroundforest.org/arpio/bind v0.0.0-20250905213330-4591a086be1e h1:DkOYkD12OWMAczreQESVQF7b1KsyBQq4G700oGxNy08=
code.squareroundforest.org/arpio/bind v0.0.0-20250905213330-4591a086be1e/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI=
code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5 h1:SIgLIawD6Vv7rAvUobpVshLshdwFEJ0NOUrWpheS088= code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5 h1:SIgLIawD6Vv7rAvUobpVshLshdwFEJ0NOUrWpheS088=
code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI=
code.squareroundforest.org/arpio/docreflect v0.0.0-20250904132730-afd27063724e h1:f7wtGAmuTYH/VTn92sBTtKhs463q+DTtW2yKgst2kl8=
code.squareroundforest.org/arpio/docreflect v0.0.0-20250904132730-afd27063724e/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA=
code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1 h1:bJi41U5yGQykg6jVlD2AdWiznvx3Jg7ZpzEU85syOXw= code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1 h1:bJi41U5yGQykg6jVlD2AdWiznvx3Jg7ZpzEU85syOXw=
code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA= code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I= code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9 h1:b7voJlwe0jKH568X+O7b/JTAUrHLTSKNSSL+hhV2Q/Q=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9/go.mod h1:hq+2CENEd4bVSZnOdq38FUFOJJnF3OTQRv78qMGkNlE=
code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239 h1:JvLVMuvF2laxXkIZbHC1/0xtKyKndAwIHbIIWkHqTzc= code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239 h1:JvLVMuvF2laxXkIZbHC1/0xtKyKndAwIHbIIWkHqTzc=
code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610 h1:I0jebdyQQfqJcwq2lT/TkUPBU8secHa5xZ+VzOdYVsw= code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f h1:gomu8xTD953IkL3M528qVEuZ2z93C2I6Hr4vyIwE7kI=
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610/go.mod h1:9XhPcVt1Y1M609z02lHvEcp00dwPD9NUCoVxS2TpcH8= code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs=
code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18 h1:2aa62CYm9ld5SNoFxWzE2wUN0xjVWQ+xieoeFantdg4=
code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18/go.mod h1:+0G3gufMAP8SCEIrDT1D/DaVOSfjS8EwPTBs5vfxqQg=
code.squareroundforest.org/arpio/treerack v0.0.0-20251031193114-4f1c219052ae h1:D28IunhepRhRSp3U2z84e3WtxbYMRzi/FwEEZg54ULM= code.squareroundforest.org/arpio/treerack v0.0.0-20251031193114-4f1c219052ae h1:D28IunhepRhRSp3U2z84e3WtxbYMRzi/FwEEZg54ULM=
code.squareroundforest.org/arpio/treerack v0.0.0-20251031193114-4f1c219052ae/go.mod h1:9XhPcVt1Y1M609z02lHvEcp00dwPD9NUCoVxS2TpcH8= code.squareroundforest.org/arpio/treerack v0.0.0-20251031193114-4f1c219052ae/go.mod h1:9XhPcVt1Y1M609z02lHvEcp00dwPD9NUCoVxS2TpcH8=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=

1050
help.go

File diff suppressed because it is too large Load Diff

1832
help_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -51,15 +51,9 @@ func validateEnv(cmd Cmd, e env) error {
} }
func validateOptions(cmd Cmd, o []option, conf Config) error { func validateOptions(cmd Cmd, o []option, conf Config) error {
ms := make(map[string]string)
ml := make(map[string]string)
for i := 0; i < len(cmd.shortForms); i += 2 {
s, l := cmd.shortForms[i], cmd.shortForms[i+1]
ms[s] = l
ml[l] = s
}
var names []string var names []string
ms := shortFormsToLong(cmd.shortForms)
ml := longFormsToShort(cmd.shortForms)
mo := make(map[string][]option) mo := make(map[string][]option)
for _, oi := range o { for _, oi := range o {
n := oi.name n := oi.name
@ -81,7 +75,7 @@ func validateOptions(cmd Cmd, o []option, conf Config) error {
for _, n := range names { for _, n := range names {
os := mo[n] os := mo[n]
en := "--" + n en := "--" + n
if sn, ok := ml[n]; ok { for _, sn := range ml[n] {
en += ", -" + sn en += ", -" + sn
} }

View File

@ -6,12 +6,30 @@ import (
) )
type Options struct { type Options struct {
// Foo is an option.
Foo int Foo int
// Bars, any number.
Bar []string
// Duration is another option.
Duration time.Duration Duration time.Duration
// Time is the third option here.
Time time.Time Time time.Time
} }
type OptionWithHelp struct {
// Custom help.
Help bool
}
// Foo sums three numbers. // Foo sums three numbers.
// It prints the sum to stdout.
//
// The input numbers can be any integer.
func Foo(a, b, c int) int { func Foo(a, b, c int) int {
return a + b + c return a + b + c
} }
@ -23,3 +41,5 @@ func Bar(out io.Writer, a, b, c int) int {
func Baz(o Options) int { func Baz(o Options) int {
return o.Foo return o.Foo
} }
func CustomHelp(o OptionWithHelp) {}

View File

@ -1,10 +1,10 @@
package wand package wand
import ( import (
"bytes"
"io"
"testing" "testing"
"time" "time"
"io"
"bytes"
) )
func TestOutput(t *testing.T) { func TestOutput(t *testing.T) {

View File

@ -115,6 +115,15 @@ func isTime(t reflect.Type) bool {
return t.ConvertibleTo(reflect.TypeFor[time.Time]()) return t.ConvertibleTo(reflect.TypeFor[time.Time]())
} }
func isDuration(t reflect.Type) bool {
if t == nil {
return false
}
t = unpackType(t)
return t == reflect.TypeFor[time.Duration]()
}
func isStruct(t reflect.Type) bool { func isStruct(t reflect.Type) bool {
if t == nil { if t == nil {
return false return false
@ -254,7 +263,7 @@ func positionalIndices(f any) []int {
t := r.Type() t := r.Type()
for i := 0; i < t.NumIn(); i++ { for i := 0; i < t.NumIn(); i++ {
p := t.In(i) p := t.In(i)
if isTime(p) || isStruct(p) || isReader(p) || isWriter(p) { if isStruct(p) || isReader(p) || isWriter(p) {
continue continue
} }
@ -279,12 +288,23 @@ func bindable(t reflect.Type) bool {
return bindable return bindable
} }
func scalarTypeStringOf(t reflect.Type) string {
if t == nil {
return ""
}
t = unpackType(t)
return scalarTypeString(bind.FieldType(t.Kind()))
}
func scalarTypeString(t bind.FieldType) string { func scalarTypeString(t bind.FieldType) string {
switch t { switch t {
case bind.Duration: case bind.Duration:
return "duration" return "duration"
case bind.Time: case bind.Time:
return "time" return "time"
case reflect.Interface:
return "any"
default: default:
return strings.ToLower(reflect.Kind(t).String()) return strings.ToLower(reflect.Kind(t).String())
} }

View File

@ -195,12 +195,12 @@ func TestReflect(t *testing.T) {
t.Run("indices of positional parameters", func(t *testing.T) { t.Run("indices of positional parameters", func(t *testing.T) {
t.Run( t.Run(
"show function params", "show function params",
testExec(testCase{impl: testlib.Foo, command: "foo help", contains: true}, "", "foo help"), testExec(testCase{impl: testlib.Foo, command: "foo help", contains: true}, "", "foo [options]... [--] <a int> <b int> <c int>"),
) )
t.Run( t.Run(
"skip non-positional params", "skip non-positional params",
testExec(testCase{impl: testlib.Bar, command: "bar help", contains: true}, "", "bar help"), testExec(testCase{impl: testlib.Bar, command: "bar help", contains: true}, "", "bar [options]... [--] <a int> <b int> <c int>"),
) )
}) })
@ -210,7 +210,6 @@ func TestReflect(t *testing.T) {
testExec( testExec(
testCase{impl: testlib.Baz, command: "baz help", contains: true}, testCase{impl: testlib.Baz, command: "baz help", contains: true},
"", "",
"baz help",
"--foo int", "--foo int",
), ),
) )

View File

@ -2,6 +2,7 @@ package tools
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@ -19,6 +20,10 @@ func execc(stdin io.Reader, stdout, stderr io.Writer, command string, args []str
} }
func execCommandDir(out io.Writer, commandDir string, env ...string) error { func execCommandDir(out io.Writer, commandDir string, env ...string) error {
if len(commandDir) > 0 && commandDir[0] != '/' && commandDir[0] != '.' {
commandDir = fmt.Sprintf("./%s", commandDir)
}
stderr := bytes.NewBuffer(nil) stderr := bytes.NewBuffer(nil)
if err := execc(nil, out, stderr, "go run", []string{commandDir}, env); err != nil { if err := execc(nil, out, stderr, "go run", []string{commandDir}, env); err != nil {
io.Copy(os.Stderr, stderr) io.Copy(os.Stderr, stderr)

View File

@ -6,6 +6,11 @@ import (
"io" "io"
) )
type ManOptions struct {
DateString string
Version string
}
type MarkdownOptions struct { type MarkdownOptions struct {
Level int Level int
} }
@ -14,10 +19,21 @@ func Docreflect(out io.Writer, packageName string, gopaths ...string) error {
return generate.GenerateRegistry(out, packageName, gopaths...) return generate.GenerateRegistry(out, packageName, gopaths...)
} }
func Man(out io.Writer, commandDir string) error { func Man(out io.Writer, o ManOptions, commandDir string) error {
return execCommandDir(out, commandDir, "_wandgenerate=man") return execCommandDir(
out,
commandDir,
"_wandgenerate=man",
fmt.Sprintf("_wandgeneratedate=%s", o.DateString),
fmt.Sprintf("_wandgenerateversion=%s", o.Version),
)
} }
func Markdown(out io.Writer, o MarkdownOptions, commandDir string) error { func Markdown(out io.Writer, o MarkdownOptions, commandDir string) error {
return execCommandDir(out, commandDir, "_wandgenerate=markdown", fmt.Sprintf("_wandmarkdownlevel=%d", o.Level)) return execCommandDir(
out,
commandDir,
"_wandgenerate=markdown",
fmt.Sprintf("_wandmarkdownlevel=%d", o.Level),
)
} }