help and docs
This commit is contained in:
parent
b9391a862d
commit
60acf6e820
5
Makefile
5
Makefile
@ -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
|
||||||
|
|||||||
6
apply.go
6
apply.go
@ -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
|
||||||
|
|||||||
27
command.go
27
command.go
@ -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...)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
45
config.go
45
config.go
@ -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 {
|
||||||
|
|||||||
23
debug.go
23
debug.go
@ -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...)
|
|
||||||
}
|
|
||||||
50
doclets.go
50
doclets.go
@ -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.`
|
||||||
|
|||||||
@ -2,30 +2,34 @@
|
|||||||
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)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Exec", "\nfunc(o, stdin, args)")
|
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", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "")
|
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.ClearCache", "")
|
||||||
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.Markdown", "\nfunc(out, o, commandDir)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions.DateString", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions.Version", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)")
|
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)")
|
||||||
}
|
}
|
||||||
@ -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.Options", "")
|
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.Options.Duration", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.OptionWithHelp", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Foo", "")
|
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.Time", "")
|
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
14
exec.go
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
202
format.go
@ -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
|
|
||||||
}
|
|
||||||
239
formathelp.go
239
formathelp.go
@ -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()
|
|
||||||
}
|
|
||||||
204
formatman.go
204
formatman.go
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
11
go.mod
@ -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
18
go.sum
@ -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=
|
||||||
|
|||||||
1832
help_test.go
Normal file
1832
help_test.go
Normal file
File diff suppressed because it is too large
Load Diff
12
input.go
12
input.go
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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) {}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
22
reflect.go
22
reflect.go
@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
22
tools/lib.go
22
tools/lib.go
@ -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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user