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
.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
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 {
ms := make(map[string]string)
for i := 0; i < len(shortForms); i += 2 {
ms[shortForms[i]] = shortForms[i+1]
}
ms := shortFormsToLong(shortForms)
v := make(map[string][]any)
for _, oi := range o {
n := oi.name

View File

@ -225,42 +225,44 @@ func validateShortFormsTree(cmd Cmd) (map[string]string, map[string]string, erro
mf := mapFields(cmd.impl)
_, helpDefined := mf["help"]
for i := 0; i < len(cmd.shortForms); i += 2 {
s, l := cmd.shortForms[i], cmd.shortForms[i+1]
if s == "h" && l == "help" && !helpDefined {
continue
}
r := []rune(s)
if len(r) != 1 || r[0] < 'a' || r[0] > 'z' {
return nil, nil, fmt.Errorf("invalid short form: %s", s)
}
if err := checkShortFormDefinition(mapped, s, l); err != nil {
return nil, nil, err
}
if err := checkShortFormDefinition(unmapped, s, l); err != nil {
return nil, nil, err
}
_, hasField := mf[l]
_, isMapped := mapped[s]
if !hasField && !isMapped {
if unmapped == nil {
unmapped = make(map[string]string)
l2s := longFormsToShort(cmd.shortForms)
for l, ss := range l2s {
for _, s := range ss {
if s == "h" && l == "help" && !helpDefined {
continue
}
unmapped[s] = l
continue
}
r := []rune(s)
if len(r) != 1 || r[0] < 'a' || r[0] > 'z' {
return nil, nil, fmt.Errorf("invalid short form: %s", s)
}
if mapped == nil {
mapped = make(map[string]string)
}
if err := checkShortFormDefinition(mapped, s, l); err != nil {
return nil, nil, err
}
delete(unmapped, s)
mapped[s] = l
if err := checkShortFormDefinition(unmapped, s, l); err != nil {
return nil, nil, err
}
_, hasField := mf[l]
_, isMapped := mapped[s]
if !hasField && !isMapped {
if unmapped == nil {
unmapped = make(map[string]string)
}
unmapped[s] = l
continue
}
if mapped == nil {
mapped = make(map[string]string)
}
delete(unmapped, s)
mapped[s] = l
}
}
return mapped, unmapped, nil
@ -317,27 +319,10 @@ func boolOptions(cmd Cmd) []string {
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
l2s := longFormsToShort(cmd.shortForms)
for _, ni := range n {
sf = append(sf, sfm[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")
sf = append(sf, l2s[ni]...)
}
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", ""),
)
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(
"foo",
f0,

View File

@ -273,17 +273,34 @@ func readArgs(boolOptions, args []string) commandLine {
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 {
var mf map[string][]bind.Field
if cmd.impl != nil {
mf = mapFields(cmd.impl)
}
sf := make(map[string]string)
for i := 0; i < len(cmd.shortForms); i += 2 {
sf[cmd.shortForms[i]] = cmd.shortForms[i+1]
}
sf := shortFormsToLong(cmd.shortForms)
for _, oi := range o {
if !oi.value.isBool {
continue

View File

@ -2,6 +2,7 @@ package wand
import (
"bytes"
"code.squareroundforest.org/arpio/textedit"
"errors"
"fmt"
"github.com/iancoleman/strcase"
@ -62,27 +63,33 @@ func (f *file) Close() error {
}
func unescapeConfig(s string) string {
var (
u []rune
escaped bool
var b bytes.Buffer
w := textedit.New(
&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)
for _, ri := range r {
if escaped {
u = append(u, ri)
continue
}
if ri == '\\' {
escaped = true
continue
}
u = append(u, ri)
}
return string(u)
w.Write([]byte(s))
return b.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
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
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, and they need to be prefixed with the name of the application, as in the base name of the command.
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
dropped.`
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
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
values can consist of any characters, except for newline, control characters, " (quote) and \ (backslash), or the values can
be quoted, in which case they can consist of any characters, spanning multiple lines, and only the " (quote) and \ (backslash)
characters need to be escaped by the \ (backslash) character. Configuration files allow multiple entries with the same key,
when if the associated command line option also allows multiple instances (marked with [*]). When an entry is defined
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
set entries with only the key, omitting the = key/value separator. Entries in 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/ini.treerack).`
const configDocs = `Every command line option's value can also be provided as an entry in a configuration file.
Configuration 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 values can consist of any characters, except for newline, control characters, " (quote) and \ (backslash), or the values
can be quoted, in which case they can consist of any characters, spanning multiple lines, and only the " (quote) and \
(backslash) characters need to be escaped by the \ (backslash) character.
Configuration files allow multiple entries with the same key, when if the associated command line option also allows multiple
instances (marked with [*]). When an entry is defined 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 set entries with only the key, omitting the = key/value separator. Entries in
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 (
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,30 +2,34 @@
Generated with https://code.squareroundforest.org/arpio/docreflect
*/
package wand
import "code.squareroundforest.org/arpio/docreflect"
func init() {
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.Exec", "\nfunc(o, stdin, args)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "")
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.NoCache", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, 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.Level", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)")
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.Exec", "\nfunc(o, stdin, args)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "")
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.NoCache", "")
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.MarkdownOptions", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)")
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)")
}

View File

@ -2,16 +2,21 @@
Generated with https://code.squareroundforest.org/arpio/docreflect
*/
package wand
import "code.squareroundforest.org/arpio/docreflect"
func init() {
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.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.Options", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Duration", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Foo", "")
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Time", "")
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.Baz", "\nfunc(o)")
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.Bar", "Bars, any number.\n")
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.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"
"path/filepath"
"strconv"
"time"
)
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
}
cmd = insertHelp(cmd)
// build time documentation generator mode:
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)
exit(1)
}
@ -25,6 +32,7 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
return
}
// build time documentation generator mode:
if getenv(env, "_wandgenerate") == "markdown" {
level, _ := strconv.Atoi(getenv(env, "_wandmarkdownlevel"))
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
}
cmd = insertHelp(cmd)
// will need root command for the config and the env:
rootCmd := cmd
cmd, fullCmd, args := selectCommand(cmd, args[1:])
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)
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
go 1.25.0
go 1.25.3
require (
code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5
code.squareroundforest.org/arpio/docreflect v0.0.0-20251031192707-01c5ff18fab1
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
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/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/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I=
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 h1:b7voJlwe0jKH568X+O7b/JTAUrHLTSKNSSL+hhV2Q/Q=
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/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610 h1:I0jebdyQQfqJcwq2lT/TkUPBU8secHa5xZ+VzOdYVsw=
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610/go.mod h1:9XhPcVt1Y1M609z02lHvEcp00dwPD9NUCoVxS2TpcH8=
code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f h1:gomu8xTD953IkL3M528qVEuZ2z93C2I6Hr4vyIwE7kI=
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/go.mod h1:9XhPcVt1Y1M609z02lHvEcp00dwPD9NUCoVxS2TpcH8=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
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/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=

1086
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 {
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
ms := shortFormsToLong(cmd.shortForms)
ml := longFormsToShort(cmd.shortForms)
mo := make(map[string][]option)
for _, oi := range o {
n := oi.name
@ -81,7 +75,7 @@ func validateOptions(cmd Cmd, o []option, conf Config) error {
for _, n := range names {
os := mo[n]
en := "--" + n
if sn, ok := ml[n]; ok {
for _, sn := range ml[n] {
en += ", -" + sn
}

View File

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

View File

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

View File

@ -115,6 +115,15 @@ func isTime(t reflect.Type) bool {
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 {
if t == nil {
return false
@ -254,7 +263,7 @@ func positionalIndices(f any) []int {
t := r.Type()
for i := 0; i < t.NumIn(); i++ {
p := t.In(i)
if isTime(p) || isStruct(p) || isReader(p) || isWriter(p) {
if isStruct(p) || isReader(p) || isWriter(p) {
continue
}
@ -279,12 +288,23 @@ func bindable(t reflect.Type) bool {
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 {
switch t {
case bind.Duration:
return "duration"
case bind.Time:
return "time"
case reflect.Interface:
return "any"
default:
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(
"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(
"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(
testCase{impl: testlib.Baz, command: "baz help", contains: true},
"",
"baz help",
"--foo int",
),
)

View File

@ -2,6 +2,7 @@ package tools
import (
"bytes"
"fmt"
"io"
"os"
"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 {
if len(commandDir) > 0 && commandDir[0] != '/' && commandDir[0] != '.' {
commandDir = fmt.Sprintf("./%s", commandDir)
}
stderr := bytes.NewBuffer(nil)
if err := execc(nil, out, stderr, "go run", []string{commandDir}, env); err != nil {
io.Copy(os.Stderr, stderr)

View File

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