complete help
This commit is contained in:
parent
f206c694b2
commit
1dac4b0af0
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,3 @@
|
||||
iniparser.go
|
||||
docreflect.go
|
||||
iniparser.gen.go
|
||||
docreflect.gen.go
|
||||
.bin
|
||||
|
||||
17
Makefile
17
Makefile
@ -1,8 +1,8 @@
|
||||
SOURCES = $(shell find . -name "*.go" | grep -v iniparser.go | grep -v docreflect.go)
|
||||
SOURCES = $(shell find . -name "*.go" | grep -v iniparser.gen.go | grep -v docreflect.gen.go)
|
||||
|
||||
default: build
|
||||
|
||||
lib: $(SOURCES) iniparser.go docreflect.go
|
||||
lib: $(SOURCES) iniparser.gen.go docreflect.gen.go
|
||||
go build
|
||||
go build ./tools
|
||||
|
||||
@ -20,23 +20,24 @@ cover: .cover
|
||||
showcover: .cover
|
||||
go tool cover -html .cover
|
||||
|
||||
fmt: $(SOURCES) iniparser.go
|
||||
fmt: $(SOURCES) iniparser.gen.go docreflect.gen.go
|
||||
go fmt ./...
|
||||
|
||||
iniparser.go: ini.treerack
|
||||
go run script/ini-parser/parser.go wand < ini.treerack > iniparser.go || rm iniparser.go
|
||||
iniparser.gen.go: ini.treerack
|
||||
go run script/ini-parser/parser.go wand < ini.treerack > iniparser.gen.go || rm iniparser.gen.go
|
||||
|
||||
docreflect.go: $(SOURCES)
|
||||
docreflect.gen.go: $(SOURCES)
|
||||
go run script/docreflect/docs.go \
|
||||
wand \
|
||||
code.squareroundforest.org/arpio/docreflect/generate \
|
||||
code.squareroundforest.org/arpio/wand/tools \
|
||||
> docreflect.go
|
||||
> docreflect.gen.go \
|
||||
|| rm docreflect.gen.go
|
||||
|
||||
.bin:
|
||||
mkdir -p .bin
|
||||
|
||||
wand: $(SOURCES) iniparser.go docreflect.go .bin
|
||||
wand: $(SOURCES) iniparser.gen.go docreflect.gen.go .bin
|
||||
go build -o .bin/wand ./cmd/wand
|
||||
|
||||
install: wand
|
||||
|
||||
@ -19,7 +19,7 @@ type config struct {
|
||||
originalNames map[string]string
|
||||
}
|
||||
|
||||
func fileReader(filename string) io.ReadCloser {
|
||||
func fileReader(filename string) *file {
|
||||
return &file{
|
||||
filename: filename,
|
||||
}
|
||||
@ -129,7 +129,7 @@ func readConfigFromOption(cmd Cmd, cl commandLine, conf Config) (config, error)
|
||||
continue
|
||||
}
|
||||
|
||||
c = append(c, Config{file: func(Cmd) io.ReadCloser { return fileReader(o.value.str) }})
|
||||
c = append(c, Config{file: func(Cmd) *file { return fileReader(o.value.str) }})
|
||||
}
|
||||
|
||||
return readConfig(cmd, cl, Config{merge: c})
|
||||
|
||||
8
exec.go
8
exec.go
@ -19,7 +19,7 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
||||
}
|
||||
|
||||
if os.Getenv("wandgenerate") == "man" {
|
||||
if err := generateMan(stdout, cmd); err != nil {
|
||||
if err := generateMan(stdout, cmd, conf); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
}
|
||||
@ -29,7 +29,7 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
||||
|
||||
if os.Getenv("wandgenerate") == "markdown" {
|
||||
level, _ := strconv.Atoi(os.Getenv("wandmarkdownlevel"))
|
||||
if err := generateMarkdown(stdout, cmd, level); err != nil {
|
||||
if err := generateMarkdown(stdout, cmd, conf, level); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
}
|
||||
@ -47,7 +47,7 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
||||
}
|
||||
|
||||
if cmd.helpRequested {
|
||||
if err := showHelp(stdout, cmd, fullCmd); err != nil {
|
||||
if err := showHelp(stdout, cmd, fullCmd, conf); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
}
|
||||
@ -58,7 +58,7 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
||||
bo := boolOptions(cmd)
|
||||
cl := readArgs(bo, args)
|
||||
if hasHelpOption(cmd, cl.options) {
|
||||
if err := showHelp(stdout, cmd, fullCmd); err != nil {
|
||||
if err := showHelp(stdout, cmd, fullCmd, conf); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
252
formathelp.go
Normal file
252
formathelp.go
Normal file
@ -0,0 +1,252 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func formatHelp(w io.Writer, doc doc) error {
|
||||
var err error
|
||||
println := func(a ...any) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(w, a...)
|
||||
}
|
||||
|
||||
printf := func(f string, a ...any) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(w, f, a...)
|
||||
}
|
||||
|
||||
printf(doc.fullCommand)
|
||||
println()
|
||||
if doc.hasImplementation || doc.synopsis.hasSubcommands {
|
||||
println()
|
||||
printf("Synopsis")
|
||||
println()
|
||||
}
|
||||
|
||||
if doc.hasImplementation {
|
||||
println()
|
||||
printf(doc.synopsis.command)
|
||||
if doc.synopsis.hasOptions {
|
||||
printf(" [options ...]")
|
||||
}
|
||||
|
||||
for _, n := range doc.synopsis.arguments.names {
|
||||
printf(" %s", n)
|
||||
}
|
||||
|
||||
if doc.synopsis.arguments.variadic {
|
||||
printf("...")
|
||||
if doc.synopsis.arguments.minPositional > 0 {
|
||||
printf(" min %d arguments", doc.synopsis.arguments.minPositional)
|
||||
}
|
||||
|
||||
if doc.synopsis.arguments.maxPositional > 0 {
|
||||
printf(" max %d arguments", doc.synopsis.arguments.maxPositional)
|
||||
}
|
||||
}
|
||||
|
||||
println()
|
||||
}
|
||||
|
||||
if doc.synopsis.hasSubcommands {
|
||||
if !doc.hasImplementation {
|
||||
println()
|
||||
}
|
||||
|
||||
printf("%s <subcommand> [options or args...]", doc.synopsis.command)
|
||||
println()
|
||||
println()
|
||||
printf("(For the details about the available subcommands, see the according section below.)")
|
||||
println()
|
||||
}
|
||||
|
||||
if len(doc.description) > 0 {
|
||||
println()
|
||||
printf(paragraphs(doc.description))
|
||||
println()
|
||||
}
|
||||
|
||||
if len(doc.options) > 0 {
|
||||
println()
|
||||
printf("Options")
|
||||
println()
|
||||
println()
|
||||
printf("[*]: accepts multiple instances of the same option")
|
||||
println()
|
||||
printf("[b]: booelan flag, true or false, or no argument means true")
|
||||
println()
|
||||
println()
|
||||
|
||||
var names []string
|
||||
od := make(map[string]string)
|
||||
for _, o := range doc.options {
|
||||
ons := []string{fmt.Sprintf("--%s", o.name)}
|
||||
for _, sn := range o.shortNames {
|
||||
ons = append(ons, fmt.Sprintf("-%s", sn))
|
||||
}
|
||||
|
||||
n := strings.Join(ons, ", ")
|
||||
if o.acceptsMultiple {
|
||||
n = fmt.Sprintf("%s [*]", n)
|
||||
}
|
||||
|
||||
if o.isBool {
|
||||
n = fmt.Sprintf("%s [b]", n)
|
||||
}
|
||||
|
||||
names = append(names, n)
|
||||
od[n] = o.description
|
||||
}
|
||||
|
||||
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 od[n] != "" {
|
||||
printf(": %s", paragraphs(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, doc.name, sc.name)
|
||||
} else if sc.hasHelpOption {
|
||||
d = fmt.Sprintf("%s - For help, see: %s %s --help", d, 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", paragraphs(cd[n]))
|
||||
}
|
||||
|
||||
println()
|
||||
}
|
||||
}
|
||||
|
||||
if len(doc.options) > 0 {
|
||||
printf(paragraphs(envDocs))
|
||||
println()
|
||||
println()
|
||||
o := doc.options[0]
|
||||
printf("Example environment variable:")
|
||||
println()
|
||||
println()
|
||||
printf(strcase.ToSnake(o.name))
|
||||
printf("=")
|
||||
if o.isBool {
|
||||
printf("true")
|
||||
} else {
|
||||
printf("42")
|
||||
}
|
||||
|
||||
println()
|
||||
}
|
||||
|
||||
if len(doc.options) > 0 && len(doc.configFiles) > 0 {
|
||||
printf(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)
|
||||
println()
|
||||
printf(strcase.ToSnake(o.name))
|
||||
printf(" = ")
|
||||
if o.isBool {
|
||||
printf("true")
|
||||
} else {
|
||||
printf("42")
|
||||
}
|
||||
|
||||
println()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
11
formatman.go
Normal file
11
formatman.go
Normal file
@ -0,0 +1,11 @@
|
||||
package wand
|
||||
|
||||
import "io"
|
||||
|
||||
func formatMan(out io.Writer, doc doc) error {
|
||||
// if no subcommands, then similar to help
|
||||
// otherwise:
|
||||
// title
|
||||
// all commands
|
||||
return nil
|
||||
}
|
||||
11
formatmarkdown.go
Normal file
11
formatmarkdown.go
Normal file
@ -0,0 +1,11 @@
|
||||
package wand
|
||||
|
||||
import "io"
|
||||
|
||||
func formatMarkdown(out io.Writer, doc doc, level int) error {
|
||||
// if no subcommands, then similar to help
|
||||
// otherwise:
|
||||
// title
|
||||
// all commands
|
||||
return nil
|
||||
}
|
||||
279
help.go
279
help.go
@ -5,12 +5,37 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const defaultWrap = 112
|
||||
|
||||
const envDocs = `Environment Variables
|
||||
|
||||
Every command line option's value can also be provided as an environment variable. Environment variable names need to use
|
||||
snake casing like foo_bar_baz or FOO_BAR_BAZ, or other casing that doesn't include the '-' dash character. 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 = `Configuration Files
|
||||
|
||||
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). 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 exist 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).`
|
||||
|
||||
type (
|
||||
argumentSet struct {
|
||||
count int
|
||||
@ -18,6 +43,8 @@ type (
|
||||
variadic bool
|
||||
usesStdin bool
|
||||
usesStdout bool
|
||||
minPositional int
|
||||
maxPositional int
|
||||
}
|
||||
|
||||
synopsis struct {
|
||||
@ -35,18 +62,27 @@ type (
|
||||
acceptsMultiple bool
|
||||
}
|
||||
|
||||
docConfig struct {
|
||||
fromOption bool
|
||||
optional bool
|
||||
fn string
|
||||
}
|
||||
|
||||
doc struct {
|
||||
name string
|
||||
fullCommand string
|
||||
synopsis synopsis
|
||||
description string
|
||||
hasImplementation bool
|
||||
isHelp bool
|
||||
isDefault bool
|
||||
hasHelpSubcommand bool
|
||||
hasHelpOption bool
|
||||
hasConfigFromOption bool
|
||||
options []docOption
|
||||
arguments argumentSet
|
||||
subcommands []doc
|
||||
configFiles []docConfig
|
||||
}
|
||||
)
|
||||
|
||||
@ -146,6 +182,8 @@ func constructArguments(cmd Cmd) argumentSet {
|
||||
variadic: t.IsVariadic(),
|
||||
usesStdin: len(ior) > 0,
|
||||
usesStdout: len(iow) > 0,
|
||||
minPositional: cmd.minPositional,
|
||||
maxPositional: cmd.maxPositional,
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +204,7 @@ func constructDescription(cmd Cmd) string {
|
||||
return strings.TrimSpace(docreflect.Function(reflect.ValueOf(cmd.impl)))
|
||||
}
|
||||
|
||||
func constructOptions(cmd Cmd) []docOption {
|
||||
func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption {
|
||||
if cmd.impl == nil {
|
||||
return nil
|
||||
}
|
||||
@ -206,13 +244,41 @@ func constructOptions(cmd Cmd) []docOption {
|
||||
o = append(o, opt)
|
||||
}
|
||||
|
||||
if hasConfigFromOption {
|
||||
o = append(o, docOption{
|
||||
name: "config",
|
||||
description: configOptionDocs,
|
||||
shortNames: sf["config"],
|
||||
acceptsMultiple: true,
|
||||
})
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func constructDoc(cmd Cmd, fullCommand []string) doc {
|
||||
func constructConfigDocs(cmd Cmd, conf Config) []docConfig {
|
||||
var docs []docConfig
|
||||
if conf.file != nil {
|
||||
docs = append(docs, docConfig{fn: conf.file(cmd).filename, optional: conf.optional})
|
||||
return docs
|
||||
}
|
||||
|
||||
if conf.fromOption {
|
||||
docs = append(docs, docConfig{fromOption: true})
|
||||
return docs
|
||||
}
|
||||
|
||||
for _, m := range conf.merge {
|
||||
docs = append(docs, constructConfigDocs(cmd, m)...)
|
||||
}
|
||||
|
||||
return docs
|
||||
}
|
||||
|
||||
func constructDoc(cmd Cmd, conf Config, fullCommand []string, hasConfigFromOption bool) doc {
|
||||
var subcommands []doc
|
||||
for _, sc := range cmd.subcommands {
|
||||
subcommands = append(subcommands, constructDoc(sc, append(fullCommand, sc.name)))
|
||||
subcommands = append(subcommands, constructDoc(sc, conf, append(fullCommand, sc.name), hasConfigFromOption))
|
||||
}
|
||||
|
||||
return doc{
|
||||
@ -220,204 +286,59 @@ func constructDoc(cmd Cmd, fullCommand []string) doc {
|
||||
fullCommand: strings.Join(fullCommand, " "),
|
||||
synopsis: constructSynopsis(cmd, fullCommand),
|
||||
description: constructDescription(cmd),
|
||||
hasImplementation: cmd.impl != nil,
|
||||
isDefault: cmd.isDefault,
|
||||
isHelp: cmd.isHelp,
|
||||
hasHelpSubcommand: hasHelpSubcommand(cmd),
|
||||
hasHelpOption: hasCustomHelpOption(cmd),
|
||||
options: constructOptions(cmd),
|
||||
options: constructOptions(cmd, hasConfigFromOption),
|
||||
arguments: constructArguments(cmd),
|
||||
subcommands: subcommands,
|
||||
configFiles: constructConfigDocs(cmd, conf),
|
||||
}
|
||||
}
|
||||
|
||||
func formatHelp(w io.Writer, doc doc) error {
|
||||
var err error
|
||||
println := func(a ...any) {
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(w, a...)
|
||||
continue
|
||||
}
|
||||
|
||||
printf := func(f string, a ...any) {
|
||||
if err != nil {
|
||||
return
|
||||
paragraph = append(paragraph, l[i])
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(w, f, a...)
|
||||
paragraphs = append(paragraphs, paragraph)
|
||||
|
||||
var cparagraphs []string
|
||||
for _, p := range paragraphs {
|
||||
cparagraphs = append(cparagraphs, strings.Join(p, " "))
|
||||
}
|
||||
|
||||
printf(doc.fullCommand)
|
||||
println()
|
||||
println()
|
||||
printf("Synopsis")
|
||||
println()
|
||||
println()
|
||||
printf(doc.synopsis.command)
|
||||
if doc.synopsis.hasOptions {
|
||||
printf(" [options ...]")
|
||||
return strings.Join(cparagraphs, "\n\n")
|
||||
}
|
||||
|
||||
for _, n := range doc.synopsis.arguments.names {
|
||||
printf(" %s", n)
|
||||
}
|
||||
|
||||
if doc.synopsis.arguments.variadic {
|
||||
printf("...")
|
||||
}
|
||||
|
||||
println()
|
||||
if doc.synopsis.hasSubcommands {
|
||||
printf("%s <subcommand> [options or args...]", doc.synopsis.command)
|
||||
println()
|
||||
println()
|
||||
printf("(For the details about the available subcommands, see the according section below.)")
|
||||
}
|
||||
|
||||
if len(doc.description) > 0 {
|
||||
println()
|
||||
println()
|
||||
printf(doc.description)
|
||||
}
|
||||
|
||||
if len(doc.options) > 0 {
|
||||
println()
|
||||
println()
|
||||
printf("Options")
|
||||
println()
|
||||
println()
|
||||
printf("[*]: accepts multiple instances of the same option")
|
||||
println()
|
||||
printf("[b]: booelan flag, true or false, or no argument means true")
|
||||
println()
|
||||
|
||||
var names []string
|
||||
od := make(map[string]string)
|
||||
for _, o := range doc.options {
|
||||
ons := []string{fmt.Sprintf("--%s", o.name)}
|
||||
for _, sn := range o.shortNames {
|
||||
ons = append(ons, fmt.Sprintf("-%s", sn))
|
||||
}
|
||||
|
||||
n := strings.Join(ons, ", ")
|
||||
if o.acceptsMultiple {
|
||||
n = fmt.Sprintf("%s [*]", n)
|
||||
}
|
||||
|
||||
if o.isBool {
|
||||
n = fmt.Sprintf("%s [b]", n)
|
||||
}
|
||||
|
||||
names = append(names, n)
|
||||
od[n] = o.description
|
||||
}
|
||||
|
||||
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 {
|
||||
println()
|
||||
printf(n)
|
||||
if od[n] != "" {
|
||||
printf(": %s", od[n])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(doc.subcommands) > 0 {
|
||||
println()
|
||||
println()
|
||||
printf("Subcommands")
|
||||
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, doc.name, sc.name)
|
||||
} else if sc.hasHelpOption {
|
||||
d = fmt.Sprintf("%s - For help, see: %s %s --help", d, 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 {
|
||||
println()
|
||||
printf(n)
|
||||
if cd[n] != "" {
|
||||
printf(": %s", cd[n])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println()
|
||||
return err
|
||||
}
|
||||
|
||||
func showHelp(out io.Writer, cmd Cmd, fullCommand []string) error {
|
||||
doc := constructDoc(cmd, fullCommand)
|
||||
func showHelp(out io.Writer, cmd Cmd, fullCommand []string, conf Config) error {
|
||||
doc := constructDoc(cmd, conf, fullCommand, hasConfigFromOption(conf))
|
||||
return formatHelp(out, doc)
|
||||
}
|
||||
|
||||
func formatMan(out io.Writer, doc doc) error {
|
||||
// if no subcommands, then similar to help
|
||||
// otherwise:
|
||||
// title
|
||||
// all commands
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateMan(out io.Writer, cmd Cmd) error {
|
||||
doc := constructDoc(cmd, []string{cmd.name})
|
||||
func generateMan(out io.Writer, cmd Cmd, conf Config) error {
|
||||
doc := constructDoc(cmd, conf, []string{cmd.name}, hasConfigFromOption(conf))
|
||||
return formatMan(out, doc)
|
||||
}
|
||||
|
||||
func formatMarkdown(out io.Writer, doc doc) error {
|
||||
// if no subcommands, then similar to help
|
||||
// otherwise:
|
||||
// title
|
||||
// all commands
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateMarkdown(out io.Writer, cmd Cmd, level int) error {
|
||||
doc := constructDoc(cmd, []string{cmd.name})
|
||||
return formatMarkdown(out, doc)
|
||||
func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
|
||||
doc := constructDoc(cmd, conf, []string{cmd.name}, hasConfigFromOption(conf))
|
||||
return formatMarkdown(out, doc, level)
|
||||
}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
help:
|
||||
- what if cmd.impl is nil, but there is a default?
|
||||
- config in help
|
||||
- min/max args in help
|
||||
- env vars in help
|
||||
- test: method docs
|
||||
- testing formatting may need to be necessary for the help docs
|
||||
46
output.go
46
output.go
@ -1,23 +1,63 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"code.squareroundforest.org/arpio/notation"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func printOutput(w io.Writer, o []any) error {
|
||||
wraperr := func(err error) error {
|
||||
return fmt.Errorf("error copying output: %w", err)
|
||||
}
|
||||
|
||||
for _, oi := range o {
|
||||
r, ok := oi.(io.Reader)
|
||||
if ok {
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
return fmt.Errorf("error copying output: %w", err)
|
||||
return wraperr(err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(w, "%v\n", oi); err != nil {
|
||||
return fmt.Errorf("error printing output: %w", err)
|
||||
t := reflect.TypeOf(oi)
|
||||
if t.Implements(reflect.TypeFor[fmt.Stringer]()) {
|
||||
if _, err := fmt.Fprintln(w, oi); err != nil {
|
||||
return wraperr(err)
|
||||
}
|
||||
}
|
||||
|
||||
t = unpack(t, reflect.Pointer)
|
||||
switch t.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Uintptr,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String,
|
||||
reflect.UnsafePointer:
|
||||
if _, err := fmt.Fprintln(w, oi); err != nil {
|
||||
return wraperr(err)
|
||||
}
|
||||
default:
|
||||
if _, err := notation.Fprintwt(w, oi); err != nil {
|
||||
return wraperr(err)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintln(w); err != nil {
|
||||
return wraperr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
reflect.go
17
reflect.go
@ -5,6 +5,7 @@ import (
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@ -44,14 +45,16 @@ func pack(v reflect.Value, t reflect.Type) reflect.Value {
|
||||
return s
|
||||
}
|
||||
|
||||
func unpack[T packedKind[T]](p T) T {
|
||||
switch p.Kind() {
|
||||
case reflect.Pointer,
|
||||
reflect.Slice:
|
||||
return unpack(p.Elem())
|
||||
default:
|
||||
return p
|
||||
func unpack[T packedKind[T]](p T, kinds ...reflect.Kind) T {
|
||||
if len(kinds) == 0 {
|
||||
kinds = []reflect.Kind{reflect.Pointer, reflect.Slice}
|
||||
}
|
||||
|
||||
if slices.Contains(kinds, p.Kind()) {
|
||||
return unpack(p.Elem(), kinds...)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func isReader(t reflect.Type) bool {
|
||||
|
||||
21
wand.go
21
wand.go
@ -1,13 +1,13 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"io"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
file func(Cmd) io.ReadCloser
|
||||
file func(Cmd) *file
|
||||
merge []Config
|
||||
fromOption bool
|
||||
optional bool
|
||||
@ -70,20 +70,29 @@ func OptionalConfig(conf Config) Config {
|
||||
|
||||
func Etc() Config {
|
||||
return OptionalConfig(Config{
|
||||
file: func(cmd Cmd) io.ReadCloser {
|
||||
file: func(cmd Cmd) *file {
|
||||
return fileReader(path.Join("/etc", cmd.name, "config"))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func UserConfig() Config {
|
||||
return OptionalConfig(Config{
|
||||
file: func(cmd Cmd) io.ReadCloser {
|
||||
return OptionalConfig(MergeConfig(
|
||||
Config{
|
||||
file: func(cmd Cmd) *file {
|
||||
return fileReader(
|
||||
path.Join(os.Getenv("HOME"), fmt.Sprintf(".%s", cmd.name), "config"),
|
||||
)
|
||||
},
|
||||
},
|
||||
Config{
|
||||
file: func(cmd Cmd) *file {
|
||||
return fileReader(
|
||||
path.Join(os.Getenv("HOME"), ".config", cmd.name, "config"),
|
||||
)
|
||||
},
|
||||
})
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
func ConfigFromOption() Config {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user