testing
This commit is contained in:
parent
1dac4b0af0
commit
796b3c2a4b
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
iniparser.gen.go
|
iniparser.gen.go
|
||||||
docreflect.gen.go
|
docreflect.gen.go
|
||||||
.bin
|
.build
|
||||||
|
|||||||
24
Makefile
24
Makefile
@ -6,7 +6,7 @@ lib: $(SOURCES) iniparser.gen.go docreflect.gen.go
|
|||||||
go build
|
go build
|
||||||
go build ./tools
|
go build ./tools
|
||||||
|
|
||||||
build: lib wand
|
build: lib .build/wand
|
||||||
|
|
||||||
check: $(SOURCES) build
|
check: $(SOURCES) build
|
||||||
go test -count 1 ./...
|
go test -count 1 ./...
|
||||||
@ -24,7 +24,7 @@ fmt: $(SOURCES) iniparser.gen.go docreflect.gen.go
|
|||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
|
||||||
iniparser.gen.go: ini.treerack
|
iniparser.gen.go: ini.treerack
|
||||||
go run script/ini-parser/parser.go wand < ini.treerack > iniparser.gen.go || rm iniparser.gen.go
|
go run script/ini-parser/parser.go wand < ini.treerack > iniparser.gen.go || rm -f iniparser.gen.go
|
||||||
|
|
||||||
docreflect.gen.go: $(SOURCES)
|
docreflect.gen.go: $(SOURCES)
|
||||||
go run script/docreflect/docs.go \
|
go run script/docreflect/docs.go \
|
||||||
@ -32,13 +32,19 @@ docreflect.gen.go: $(SOURCES)
|
|||||||
code.squareroundforest.org/arpio/docreflect/generate \
|
code.squareroundforest.org/arpio/docreflect/generate \
|
||||||
code.squareroundforest.org/arpio/wand/tools \
|
code.squareroundforest.org/arpio/wand/tools \
|
||||||
> docreflect.gen.go \
|
> docreflect.gen.go \
|
||||||
|| rm docreflect.gen.go
|
|| rm -f docreflect.gen.go
|
||||||
|
|
||||||
.bin:
|
.build:
|
||||||
mkdir -p .bin
|
mkdir -p .build
|
||||||
|
|
||||||
wand: $(SOURCES) iniparser.gen.go docreflect.gen.go .bin
|
.build/wand: $(SOURCES) iniparser.gen.go docreflect.gen.go .build
|
||||||
go build -o .bin/wand ./cmd/wand
|
go build -o .build/wand -ldflags "-X main.version=$(shell date +%Y-%m-%d)-$(shell git rev-parse --short HEAD)" ./cmd/wand
|
||||||
|
|
||||||
install: wand
|
install: .build/wand
|
||||||
cp .bin/wand ~/bin
|
cp .build/wand ~/bin
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf .build
|
||||||
|
rm -f docreflect.gen.go
|
||||||
|
rm -f iniparser.gen.go
|
||||||
|
rm -f .cover
|
||||||
|
|||||||
6
apply.go
6
apply.go
@ -176,6 +176,10 @@ func createStructArg(t reflect.Type, shortForms []string, c config, e env, o []o
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createPositional(t reflect.Type, v string) reflect.Value {
|
func createPositional(t reflect.Type, v string) reflect.Value {
|
||||||
|
if t.Kind() == reflect.Interface {
|
||||||
|
return reflect.ValueOf(v)
|
||||||
|
}
|
||||||
|
|
||||||
tup := unpack(t)
|
tup := unpack(t)
|
||||||
sv := reflect.ValueOf(scan(tup, v))
|
sv := reflect.ValueOf(scan(tup, v))
|
||||||
return pack(sv, t)
|
return pack(sv, t)
|
||||||
@ -223,7 +227,7 @@ func processResults(t reflect.Type, out []reflect.Value) ([]any, error) {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
last := len(out) - 1
|
last := len(out) - 1
|
||||||
isErrorType := t.Out(last) == reflect.TypeOf(err)
|
isErrorType := t.Out(last) == reflect.TypeFor[error]()
|
||||||
if isErrorType && !out[last].IsZero() {
|
if isErrorType && !out[last].IsZero() {
|
||||||
err = out[last].Interface().(error)
|
err = out[last].Interface().(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,10 +5,15 @@ import (
|
|||||||
"code.squareroundforest.org/arpio/wand/tools"
|
"code.squareroundforest.org/arpio/wand/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
docreflect := Command("docreflect", tools.Docreflect)
|
docreflect := Command("docreflect", tools.Docreflect)
|
||||||
man := Command("manpages", tools.Man)
|
man := Command("manpages", tools.Man)
|
||||||
md := Command("markdown", tools.Markdown)
|
md := Command("markdown", tools.Markdown)
|
||||||
exec := Default(Command("exec", tools.Exec))
|
exec := Command("exec", tools.Exec)
|
||||||
Exec(Command("wand", nil, docreflect, man, md, exec), Etc(), UserConfig())
|
wand := Command("wand", nil, docreflect, man, md, Default(exec))
|
||||||
|
wand = Version(wand, version)
|
||||||
|
conf := MergeConfig(Etc(), UserConfig())
|
||||||
|
Exec(wand, conf)
|
||||||
}
|
}
|
||||||
|
|||||||
19
command.go
19
command.go
@ -4,9 +4,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$")
|
||||||
|
|
||||||
func command(name string, impl any, subcmds ...Cmd) Cmd {
|
func command(name string, impl any, subcmds ...Cmd) Cmd {
|
||||||
return Cmd{
|
return Cmd{
|
||||||
name: name,
|
name: name,
|
||||||
@ -74,7 +77,7 @@ func validateParameter(visited map[reflect.Type]bool, t reflect.Type) error {
|
|||||||
return validateParameter(visited, t)
|
return validateParameter(visited, t)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
if t.NumMethod() > 0 {
|
if t.NumMethod() > 0 {
|
||||||
return errors.New("'non-empty' interface parameter")
|
return errors.New("non-empty interface parameter")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -203,11 +206,19 @@ func validateShortForms(cmd Cmd, assignedShortForms map[string]string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]string) error {
|
func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]string, root bool) error {
|
||||||
if cmd.isHelp {
|
if cmd.isHelp {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.version != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !root && !commandNameExpression.MatchString(cmd.name) {
|
||||||
|
return fmt.Errorf("command name is not a valid symbol: '%s'", cmd.name)
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.impl != nil {
|
if cmd.impl != nil {
|
||||||
if err := validateImpl(cmd, conf); err != nil {
|
if err := validateImpl(cmd, conf); err != nil {
|
||||||
return fmt.Errorf("%s: %w", cmd.name, err)
|
return fmt.Errorf("%s: %w", cmd.name, err)
|
||||||
@ -236,7 +247,7 @@ func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]str
|
|||||||
}
|
}
|
||||||
|
|
||||||
names[s.name] = true
|
names[s.name] = true
|
||||||
if err := validateCommandTree(s, conf, assignedShortForms); err != nil {
|
if err := validateCommandTree(s, conf, assignedShortForms, false); err != nil {
|
||||||
return fmt.Errorf("%s: %w", s.name, err)
|
return fmt.Errorf("%s: %w", s.name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,7 +286,7 @@ func allShortForms(cmd Cmd) []string {
|
|||||||
|
|
||||||
func validateCommand(cmd Cmd, conf Config) error {
|
func validateCommand(cmd Cmd, conf Config) error {
|
||||||
assignedShortForms := make(map[string]string)
|
assignedShortForms := make(map[string]string)
|
||||||
if err := validateCommandTree(cmd, conf, assignedShortForms); err != nil {
|
if err := validateCommandTree(cmd, conf, assignedShortForms, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -182,11 +182,6 @@ func selectCommand(cmd Cmd, args []string) (Cmd, []string, []string) {
|
|||||||
return defaultCommand(cmd), []string{cmd.name}, args
|
return defaultCommand(cmd), []string{cmd.name}, args
|
||||||
}
|
}
|
||||||
|
|
||||||
if sc.isHelp {
|
|
||||||
cmd.helpRequested = true
|
|
||||||
return cmd, []string{cmd.name}, args[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd, fullCommand, args := selectCommand(sc, args[1:])
|
cmd, fullCommand, args := selectCommand(sc, args[1:])
|
||||||
fullCommand = append([]string{cmd.name}, fullCommand...)
|
fullCommand = append([]string{cmd.name}, fullCommand...)
|
||||||
return cmd, fullCommand, args
|
return cmd, fullCommand, args
|
||||||
|
|||||||
12
config.go
12
config.go
@ -1,10 +1,12 @@
|
|||||||
package wand
|
package wand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/iancoleman/strcase"
|
"github.com/iancoleman/strcase"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -60,8 +62,14 @@ func (f *file) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readConfigFile(cmd Cmd, conf Config) (config, error) {
|
func readConfigFile(cmd Cmd, conf Config) (config, error) {
|
||||||
f := conf.file(cmd)
|
var f io.ReadCloser
|
||||||
|
if conf.test == "" {
|
||||||
|
f = conf.file(cmd)
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
} else {
|
||||||
|
f = ioutil.NopCloser(bytes.NewBufferString(conf.test))
|
||||||
|
}
|
||||||
|
|
||||||
doc, err := parse(f)
|
doc, err := parse(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if conf.optional && (errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrNotExist)) {
|
if conf.optional && (errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrNotExist)) {
|
||||||
@ -165,7 +173,7 @@ func readMergeConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
|
func readConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
|
||||||
if conf.file != nil {
|
if conf.file != nil || conf.test != "" {
|
||||||
return readConfigFile(cmd, conf)
|
return readConfigFile(cmd, conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
27
doclets.go
Normal file
27
doclets.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package wand
|
||||||
|
|
||||||
|
const envDocs = `Every command line option's value can also be provided as an environment variable. Environment variable
|
||||||
|
names need to use snake casing like myapp_foo_bar_baz or MYAPP_FOO_BAR_BAZ, or other casing that doesn't include the '-' dash
|
||||||
|
character, and they need to be prefixed with the name of the application, as in the base name of the command. When both the
|
||||||
|
environment variable and the command line option is defined, the command line option overrides the environment variable.
|
||||||
|
Multiple values for the same environment variable can be defined by concatenating the values with the ':' separator
|
||||||
|
character. When overriding multiple values with command line options, all the environment values of the same field are
|
||||||
|
dropped.`
|
||||||
|
|
||||||
|
const configOptionDocs = `the config option allows to define zero or more configuration files at arbitrary path`
|
||||||
|
|
||||||
|
const configDocs = `Every command line option's value can also be provided as an entry in a configuration file. Configuration
|
||||||
|
file entries can use keys with different casings, e.g. snake case foo_bar_baz, or kebab case foo-bar-baz. The keys of the
|
||||||
|
entries can use a limited set of characters: [a-zA-Z0-9_-], and the first character needs to be one of [a-zA-Z_]. Entry
|
||||||
|
values can consist of any characters, except for newline, control characters, " (quote) and \ (backslash), or the values can
|
||||||
|
be quoted, in which case they can consist of any characters, spanning multiple lines, and only the " (quote) and \ (backslash)
|
||||||
|
characters need to be escaped by the \ (backslash) character. Configuration files allow multiple entries with the same key,
|
||||||
|
when if the associated command line option also allows multiple instances (marked with [*]). When an entry is defined
|
||||||
|
multiple configuration files, the effective value is overridden in the order of the definition of the possible config files
|
||||||
|
(see the listing order below). To discard values defined in the overridden config files without defining new ones, we can
|
||||||
|
set entries with only the key, omitting the = key/value separator. Entries in the config files are overridden by the
|
||||||
|
environment variables, when defined, and by the command line options when defined. Config files marked as optional don't
|
||||||
|
need to be present in the file system, but if they exist, then they must contain valid configuration syntax which is wand's
|
||||||
|
flavor of .ini files (https://code.squareroundforest.org/arpio/ini.treerack).`
|
||||||
|
|
||||||
|
const versionDocs = `Print the version of the current binary release.`
|
||||||
59
exec.go
59
exec.go
@ -3,7 +3,6 @@ package wand
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/iancoleman/strcase"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -13,9 +12,10 @@ import (
|
|||||||
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) {
|
||||||
cmd = insertHelp(cmd)
|
cmd = insertHelp(cmd)
|
||||||
_, cmd.name = filepath.Split(args[0])
|
_, cmd.name = filepath.Split(args[0])
|
||||||
cmd.name = strcase.ToKebab(cmd.name)
|
|
||||||
if err := validateCommand(cmd, conf); err != nil {
|
if err := validateCommand(cmd, conf); err != nil {
|
||||||
panic(err)
|
fmt.Fprintf(stderr, "program error: %v\n", err)
|
||||||
|
exit(1)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("wandgenerate") == "man" {
|
if os.Getenv("wandgenerate") == "man" {
|
||||||
@ -39,6 +39,39 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
|||||||
|
|
||||||
e := readEnv(cmd.name, env)
|
e := readEnv(cmd.name, env)
|
||||||
cmd, fullCmd, args := selectCommand(cmd, args[1:])
|
cmd, fullCmd, args := selectCommand(cmd, args[1:])
|
||||||
|
if cmd.isHelp {
|
||||||
|
if err := showHelp(stdout, cmd, conf, fullCmd); err != nil {
|
||||||
|
fmt.Fprintln(stderr, err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.version != "" {
|
||||||
|
if err := showVersion(stdout, cmd); err != nil {
|
||||||
|
fmt.Fprintln(stderr, err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var bo []string
|
||||||
|
if cmd.impl != nil {
|
||||||
|
bo = boolOptions(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
cl := readArgs(bo, args)
|
||||||
|
if hasHelpOption(cmd, cl.options) {
|
||||||
|
if err := showHelp(stdout, cmd, conf, fullCmd); err != nil {
|
||||||
|
fmt.Fprintln(stderr, err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.impl == nil {
|
if cmd.impl == nil {
|
||||||
fmt.Fprintln(stderr, errors.New("subcommand not specified"))
|
fmt.Fprintln(stderr, errors.New("subcommand not specified"))
|
||||||
suggestHelp(stderr, cmd, fullCmd)
|
suggestHelp(stderr, cmd, fullCmd)
|
||||||
@ -46,26 +79,6 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.helpRequested {
|
|
||||||
if err := showHelp(stdout, cmd, fullCmd, conf); err != nil {
|
|
||||||
fmt.Fprintln(stderr, err)
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bo := boolOptions(cmd)
|
|
||||||
cl := readArgs(bo, args)
|
|
||||||
if hasHelpOption(cmd, cl.options) {
|
|
||||||
if err := showHelp(stdout, cmd, fullCmd, conf); err != nil {
|
|
||||||
fmt.Fprintln(stderr, err)
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := readConfig(cmd, cl, conf)
|
c, err := readConfig(cmd, cl, conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "configuration error: %v", err)
|
fmt.Fprintf(stderr, "configuration error: %v", err)
|
||||||
|
|||||||
34
exec_test.go
34
exec_test.go
@ -1,23 +1,37 @@
|
|||||||
package wand
|
package wand
|
||||||
|
|
||||||
/*
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testExec(impl any, env, commandLine, err string, expect ...string) func(*testing.T) {
|
type testCase struct {
|
||||||
|
impl any
|
||||||
|
stdin string
|
||||||
|
conf string
|
||||||
|
env string
|
||||||
|
command string
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExec(test testCase, err string, expect ...string) func(*testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
var exitCode int
|
var exitCode int
|
||||||
exit := func(code int) { exitCode = code }
|
exit := func(code int) { exitCode = code }
|
||||||
|
|
||||||
|
var stdinr io.Reader
|
||||||
|
if test.stdin != "" {
|
||||||
|
stdinr = bytes.NewBuffer([]byte(test.stdin))
|
||||||
|
}
|
||||||
|
|
||||||
stdout := bytes.NewBuffer(nil)
|
stdout := bytes.NewBuffer(nil)
|
||||||
stderr := bytes.NewBuffer(nil)
|
stderr := bytes.NewBuffer(nil)
|
||||||
cmd := wrap(impl)
|
cmd := wrap(test.impl)
|
||||||
e := strings.Split(env, ";")
|
e := strings.Split(test.env, ";")
|
||||||
a := strings.Split(commandLine, " ")
|
a := strings.Split(test.command, " ")
|
||||||
exec(stdout, stderr, exit, cmd, e, a)
|
exec(stdinr, stdout, stderr, exit, cmd, Config{test: test.conf}, e, a)
|
||||||
if exitCode != 0 && err == "" {
|
if exitCode != 0 && err == "" {
|
||||||
t.Fatal("non-zero exit code:", stderr.String())
|
t.Fatal("non-zero exit code:", stderr.String())
|
||||||
}
|
}
|
||||||
@ -39,9 +53,13 @@ func testExec(impl any, env, commandLine, err string, expect ...string) func(*te
|
|||||||
expstr = append(expstr, fmt.Sprint(e))
|
expstr = append(expstr, fmt.Sprint(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
if stdout.String() != strings.Join(expstr, "\n")+"\n" {
|
output := stdout.String()
|
||||||
|
if output[len(output) - 1] != '\n' {
|
||||||
|
output = output + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if output != strings.Join(expstr, "\n")+"\n" {
|
||||||
t.Fatal("unexpected output:", stdout.String())
|
t.Fatal("unexpected output:", stdout.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|||||||
201
format.go
Normal file
201
format.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
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 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 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 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
|
||||||
|
}
|
||||||
135
formathelp.go
135
formathelp.go
@ -8,25 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func formatHelp(w io.Writer, doc doc) error {
|
func formatHelp(out io.Writer, doc doc) error {
|
||||||
var err error
|
printf, println, finish := printer(out)
|
||||||
println := func(a ...any) {
|
printf(escapeTeletype(doc.fullCommand))
|
||||||
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()
|
println()
|
||||||
if doc.hasImplementation || doc.synopsis.hasSubcommands {
|
if doc.hasImplementation || doc.synopsis.hasSubcommands {
|
||||||
println()
|
println()
|
||||||
@ -36,23 +20,30 @@ func formatHelp(w io.Writer, doc doc) error {
|
|||||||
|
|
||||||
if doc.hasImplementation {
|
if doc.hasImplementation {
|
||||||
println()
|
println()
|
||||||
printf(doc.synopsis.command)
|
printf(escapeTeletype(doc.synopsis.command))
|
||||||
if doc.synopsis.hasOptions {
|
if doc.synopsis.hasOptions {
|
||||||
printf(" [options ...]")
|
printf(" [options...]")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, n := range doc.synopsis.arguments.names {
|
for i := range doc.synopsis.arguments.names {
|
||||||
printf(" %s", n)
|
printf(
|
||||||
|
" [%s %s]",
|
||||||
|
doc.synopsis.arguments.names[i],
|
||||||
|
doc.synopsis.arguments.types[i],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc.synopsis.arguments.variadic {
|
if doc.synopsis.arguments.variadic {
|
||||||
printf("...")
|
printf("...")
|
||||||
if doc.synopsis.arguments.minPositional > 0 {
|
min := doc.synopsis.arguments.minPositional
|
||||||
printf(" min %d arguments", doc.synopsis.arguments.minPositional)
|
max := doc.synopsis.arguments.maxPositional
|
||||||
}
|
switch {
|
||||||
|
case min > 0 && max > 0:
|
||||||
if doc.synopsis.arguments.maxPositional > 0 {
|
printf("\nmin %d and max %d total positional arguments", min, max)
|
||||||
printf(" max %d arguments", doc.synopsis.arguments.maxPositional)
|
case min > 0:
|
||||||
|
printf("\nmin %d total positional arguments", min)
|
||||||
|
case max > 0:
|
||||||
|
printf("\nmax %d total positional arguments", max)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,56 +51,39 @@ func formatHelp(w io.Writer, doc doc) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if doc.synopsis.hasSubcommands {
|
if doc.synopsis.hasSubcommands {
|
||||||
if !doc.hasImplementation {
|
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()
|
println()
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("%s <subcommand> [options or args...]", doc.synopsis.command)
|
if doc.description != "" {
|
||||||
println()
|
println()
|
||||||
println()
|
printf(escapeTeletype(paragraphs(doc.description)))
|
||||||
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()
|
println()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(doc.options) > 0 {
|
if len(doc.options) > 0 {
|
||||||
println()
|
println()
|
||||||
printf("Options")
|
printf("Options")
|
||||||
|
if doc.hasBoolOptions || doc.hasListOptions {
|
||||||
println()
|
println()
|
||||||
println()
|
if doc.hasBoolOptions {
|
||||||
printf("[*]: accepts multiple instances of the same option")
|
|
||||||
println()
|
println()
|
||||||
printf("[b]: booelan flag, true or false, or no argument means true")
|
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()
|
||||||
println()
|
println()
|
||||||
|
names, od := prepareOptions(doc.options)
|
||||||
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
|
var max int
|
||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
@ -120,13 +94,13 @@ func formatHelp(w io.Writer, doc doc) error {
|
|||||||
|
|
||||||
for i := range names {
|
for i := range names {
|
||||||
pad := strings.Join(make([]string, max-len(names[i])+1), " ")
|
pad := strings.Join(make([]string, max-len(names[i])+1), " ")
|
||||||
names[i] = fmt.Sprintf("%s%s", names[i], pad)
|
names[i] = fmt.Sprintf("%s:%s", names[i], pad)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
printf(n)
|
printf(n)
|
||||||
if od[n] != "" {
|
if od[n] != "" {
|
||||||
printf(": %s", paragraphs(od[n]))
|
printf(" %s", escapeTeletype(lines(od[n])))
|
||||||
}
|
}
|
||||||
|
|
||||||
println()
|
println()
|
||||||
@ -153,9 +127,9 @@ func formatHelp(w io.Writer, doc doc) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sc.hasHelpSubcommand {
|
if sc.hasHelpSubcommand {
|
||||||
d = fmt.Sprintf("%s - For help, see: %s %s help", d, doc.name, sc.name)
|
d = fmt.Sprintf("%s - For help, see: %s %s help", d, escapeTeletype(doc.name), sc.name)
|
||||||
} else if sc.hasHelpOption {
|
} else if sc.hasHelpOption {
|
||||||
d = fmt.Sprintf("%s - For help, see: %s %s --help", d, doc.name, sc.name)
|
d = fmt.Sprintf("%s - For help, see: %s %s --help", d, escapeTeletype(doc.name), sc.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
cd[name] = d
|
cd[name] = d
|
||||||
@ -178,22 +152,24 @@ func formatHelp(w io.Writer, doc doc) error {
|
|||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
printf(n)
|
printf(n)
|
||||||
if cd[n] != "" {
|
if cd[n] != "" {
|
||||||
printf(": %s", paragraphs(cd[n]))
|
printf(": %s", escapeTeletype(paragraphs(cd[n])))
|
||||||
}
|
}
|
||||||
|
|
||||||
println()
|
println()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(doc.options) > 0 {
|
if len(doc.options) > 0 && commandNameExpression.MatchString(doc.appName) {
|
||||||
printf(paragraphs(envDocs))
|
println("Environment Variables")
|
||||||
|
println()
|
||||||
|
printf(escapeTeletype(paragraphs(envDocs)))
|
||||||
println()
|
println()
|
||||||
println()
|
println()
|
||||||
o := doc.options[0]
|
o := doc.options[0]
|
||||||
printf("Example environment variable:")
|
printf("Example environment variable:")
|
||||||
println()
|
println()
|
||||||
println()
|
println()
|
||||||
printf(strcase.ToSnake(o.name))
|
printf(strcase.ToSnake(fmt.Sprintf("%s-%s", doc.appName, o.name)))
|
||||||
printf("=")
|
printf("=")
|
||||||
if o.isBool {
|
if o.isBool {
|
||||||
printf("true")
|
printf("true")
|
||||||
@ -205,7 +181,9 @@ func formatHelp(w io.Writer, doc doc) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(doc.options) > 0 && len(doc.configFiles) > 0 {
|
if len(doc.options) > 0 && len(doc.configFiles) > 0 {
|
||||||
printf(paragraphs(configDocs))
|
println("Configuration Files")
|
||||||
|
println()
|
||||||
|
printf(escapeTeletype(paragraphs(configDocs)))
|
||||||
println()
|
println()
|
||||||
println()
|
println()
|
||||||
printf("Config files:")
|
printf("Config files:")
|
||||||
@ -236,6 +214,7 @@ func formatHelp(w io.Writer, doc doc) error {
|
|||||||
println()
|
println()
|
||||||
printf("# default for --")
|
printf("# default for --")
|
||||||
printf(o.name)
|
printf(o.name)
|
||||||
|
printf(":")
|
||||||
println()
|
println()
|
||||||
printf(strcase.ToSnake(o.name))
|
printf(strcase.ToSnake(o.name))
|
||||||
printf(" = ")
|
printf(" = ")
|
||||||
@ -246,7 +225,15 @@ func formatHelp(w io.Writer, doc doc) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println()
|
println()
|
||||||
|
println()
|
||||||
|
printf("Example for discarding an inherited entry:")
|
||||||
|
println()
|
||||||
|
println()
|
||||||
|
printf("# discarding an inherited entry:")
|
||||||
|
println()
|
||||||
|
printf(strcase.ToSnake(o.name))
|
||||||
|
println()
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return finish()
|
||||||
}
|
}
|
||||||
|
|||||||
205
formatman.go
205
formatman.go
@ -1,11 +1,204 @@
|
|||||||
package wand
|
package wand
|
||||||
|
|
||||||
import "io"
|
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 {
|
func formatMan(out io.Writer, doc doc) error {
|
||||||
// if no subcommands, then similar to help
|
var hasSubcommands bool
|
||||||
// otherwise:
|
for _, sc := range doc.subcommands {
|
||||||
// title
|
if !sc.isHelp && !sc.isVersion {
|
||||||
// all commands
|
continue
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
hasSubcommands = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasSubcommands {
|
||||||
|
return formatManMultiCommand(out, doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatManSingleCommand(out, doc)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,201 @@
|
|||||||
package wand
|
package wand
|
||||||
|
|
||||||
import "io"
|
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 {
|
func formatMarkdown(out io.Writer, doc doc, level int) error {
|
||||||
// if no subcommands, then similar to help
|
var hasSubcommands bool
|
||||||
// otherwise:
|
for _, sc := range doc.subcommands {
|
||||||
// title
|
if !sc.isHelp && !sc.isVersion {
|
||||||
// all commands
|
continue
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
hasSubcommands = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasSubcommands {
|
||||||
|
return formatMarkdownMultiCommand(out, doc, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatMarkdownSingleCommand(out, doc, level)
|
||||||
}
|
}
|
||||||
|
|||||||
160
help.go
160
help.go
@ -5,41 +5,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 (
|
type (
|
||||||
argumentSet struct {
|
argumentSet struct {
|
||||||
count int
|
count int
|
||||||
names []string
|
names []string
|
||||||
|
types []string
|
||||||
variadic bool
|
variadic bool
|
||||||
usesStdin bool
|
usesStdin bool
|
||||||
usesStdout bool
|
usesStdout bool
|
||||||
@ -56,6 +31,7 @@ type (
|
|||||||
|
|
||||||
docOption struct {
|
docOption struct {
|
||||||
name string
|
name string
|
||||||
|
typ string
|
||||||
description string
|
description string
|
||||||
shortNames []string
|
shortNames []string
|
||||||
isBool bool
|
isBool bool
|
||||||
@ -70,19 +46,24 @@ type (
|
|||||||
|
|
||||||
doc struct {
|
doc struct {
|
||||||
name string
|
name string
|
||||||
|
appName string
|
||||||
fullCommand string
|
fullCommand string
|
||||||
synopsis synopsis
|
synopsis synopsis
|
||||||
description string
|
description string
|
||||||
hasImplementation bool
|
hasImplementation bool
|
||||||
isHelp bool
|
isHelp bool
|
||||||
|
isVersion bool
|
||||||
isDefault bool
|
isDefault bool
|
||||||
hasHelpSubcommand bool
|
hasHelpSubcommand bool
|
||||||
hasHelpOption bool
|
hasHelpOption bool
|
||||||
hasConfigFromOption bool
|
hasConfigFromOption bool
|
||||||
options []docOption
|
options []docOption
|
||||||
|
hasBoolOptions bool
|
||||||
|
hasListOptions bool
|
||||||
arguments argumentSet
|
arguments argumentSet
|
||||||
subcommands []doc
|
subcommands []doc
|
||||||
configFiles []docConfig
|
configFiles []docConfig
|
||||||
|
date time.Time
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -102,7 +83,7 @@ func insertHelp(cmd Cmd) Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasHelpCmd {
|
if !hasHelpCmd && cmd.version == "" {
|
||||||
cmd.subcommands = append(cmd.subcommands, help())
|
cmd.subcommands = append(cmd.subcommands, help())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +101,10 @@ func hasHelpSubcommand(cmd Cmd) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hasCustomHelpOption(cmd Cmd) bool {
|
func hasCustomHelpOption(cmd Cmd) bool {
|
||||||
|
if cmd.impl == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
mf := mapFields(cmd.impl)
|
mf := mapFields(cmd.impl)
|
||||||
_, has := mf["help"]
|
_, has := mf["help"]
|
||||||
return has
|
return has
|
||||||
@ -149,13 +134,38 @@ func hasOptions(cmd Cmd) bool {
|
|||||||
return len(fields(s...)) > 0
|
return len(fields(s...)) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func functionParams(v reflect.Value, skip []int) []string {
|
func allCommands(cmd doc) []doc {
|
||||||
names := docreflect.FunctionParams(v)
|
commands := []doc{cmd}
|
||||||
for _, i := range skip {
|
for _, sc := range cmd.subcommands {
|
||||||
names = append(names[:i], names[i+1:]...)
|
commands = append(commands, allCommands(sc)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return names
|
sort.Slice(commands, func(i, j int) bool {
|
||||||
|
return commands[i].fullCommand < commands[j].fullCommand
|
||||||
|
})
|
||||||
|
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
|
func functionParams(v reflect.Value, skip []int) ([]string, []string) {
|
||||||
|
names := docreflect.FunctionParams(v)
|
||||||
|
|
||||||
|
var types []reflect.Kind
|
||||||
|
for i := 0; i < v.Type().NumIn(); i++ {
|
||||||
|
types = append(types, v.Type().In(i).Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range skip {
|
||||||
|
names = append(names[:i], names[i+1:]...)
|
||||||
|
types = append(types[:i], types[i+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stypes []string
|
||||||
|
for _, t := range types {
|
||||||
|
stypes = append(stypes, strings.ToLower(fmt.Sprint(t)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, stypes
|
||||||
}
|
}
|
||||||
|
|
||||||
func constructArguments(cmd Cmd) argumentSet {
|
func constructArguments(cmd Cmd) argumentSet {
|
||||||
@ -163,12 +173,12 @@ func constructArguments(cmd Cmd) argumentSet {
|
|||||||
return argumentSet{}
|
return argumentSet{}
|
||||||
}
|
}
|
||||||
|
|
||||||
v := reflect.ValueOf(cmd.impl)
|
v := unpack(reflect.ValueOf(cmd.impl))
|
||||||
t := unpack(v.Type())
|
t := v.Type()
|
||||||
p := positionalParameters(t)
|
p := positionalParameters(t)
|
||||||
ior, iow := ioParameters(p)
|
ior, iow := ioParameters(p)
|
||||||
count := len(p) - len(ior) - len(iow)
|
count := len(p) - len(ior) - len(iow)
|
||||||
names := functionParams(v, append(ior, iow...))
|
names, types := functionParams(v, append(ior, iow...))
|
||||||
if len(names) < count {
|
if len(names) < count {
|
||||||
names = nil
|
names = nil
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
@ -179,6 +189,7 @@ func constructArguments(cmd Cmd) argumentSet {
|
|||||||
return argumentSet{
|
return argumentSet{
|
||||||
count: count,
|
count: count,
|
||||||
names: names,
|
names: names,
|
||||||
|
types: types,
|
||||||
variadic: t.IsVariadic(),
|
variadic: t.IsVariadic(),
|
||||||
usesStdin: len(ior) > 0,
|
usesStdin: len(ior) > 0,
|
||||||
usesStdout: len(iow) > 0,
|
usesStdout: len(iow) > 0,
|
||||||
@ -197,6 +208,10 @@ func constructSynopsis(cmd Cmd, fullCommand []string) synopsis {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func constructDescription(cmd Cmd) string {
|
func constructDescription(cmd Cmd) string {
|
||||||
|
if cmd.version != "" {
|
||||||
|
return versionDocs
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.impl == nil {
|
if cmd.impl == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -230,6 +245,7 @@ func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption {
|
|||||||
for name, fi := range f {
|
for name, fi := range f {
|
||||||
opt := docOption{
|
opt := docOption{
|
||||||
name: name,
|
name: name,
|
||||||
|
typ: strings.ToLower(fmt.Sprint(fi[0].typ.Kind())),
|
||||||
description: d[name],
|
description: d[name],
|
||||||
shortNames: sf[name],
|
shortNames: sf[name],
|
||||||
isBool: fi[0].typ.Kind() == reflect.Bool,
|
isBool: fi[0].typ.Kind() == reflect.Bool,
|
||||||
@ -275,70 +291,64 @@ func constructConfigDocs(cmd Cmd, conf Config) []docConfig {
|
|||||||
return docs
|
return docs
|
||||||
}
|
}
|
||||||
|
|
||||||
func constructDoc(cmd Cmd, conf Config, fullCommand []string, hasConfigFromOption bool) doc {
|
func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
|
||||||
|
hasConfigFromOption := hasConfigFromOption(conf)
|
||||||
|
|
||||||
var subcommands []doc
|
var subcommands []doc
|
||||||
for _, sc := range cmd.subcommands {
|
for _, sc := range cmd.subcommands {
|
||||||
subcommands = append(subcommands, constructDoc(sc, conf, append(fullCommand, sc.name), hasConfigFromOption))
|
subcommands = append(subcommands, constructDoc(sc, conf, append(fullCommand, sc.name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasBoolOptions, hasListOptions bool
|
||||||
|
options := constructOptions(cmd, hasConfigFromOption)
|
||||||
|
for _, o := range options {
|
||||||
|
if o.isBool {
|
||||||
|
hasBoolOptions = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.acceptsMultiple {
|
||||||
|
hasListOptions = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc{
|
return doc{
|
||||||
name: fullCommand[len(fullCommand)-1],
|
name: cmd.name,
|
||||||
|
appName: fullCommand[0],
|
||||||
fullCommand: strings.Join(fullCommand, " "),
|
fullCommand: strings.Join(fullCommand, " "),
|
||||||
synopsis: constructSynopsis(cmd, fullCommand),
|
synopsis: constructSynopsis(cmd, fullCommand),
|
||||||
description: constructDescription(cmd),
|
description: constructDescription(cmd),
|
||||||
hasImplementation: cmd.impl != nil,
|
hasImplementation: cmd.impl != nil,
|
||||||
isDefault: cmd.isDefault,
|
isDefault: cmd.isDefault,
|
||||||
isHelp: cmd.isHelp,
|
isHelp: cmd.isHelp,
|
||||||
|
isVersion: cmd.version != "",
|
||||||
hasHelpSubcommand: hasHelpSubcommand(cmd),
|
hasHelpSubcommand: hasHelpSubcommand(cmd),
|
||||||
hasHelpOption: hasCustomHelpOption(cmd),
|
hasHelpOption: hasCustomHelpOption(cmd),
|
||||||
options: constructOptions(cmd, hasConfigFromOption),
|
options: options,
|
||||||
|
hasBoolOptions: hasBoolOptions,
|
||||||
|
hasListOptions: hasListOptions,
|
||||||
arguments: constructArguments(cmd),
|
arguments: constructArguments(cmd),
|
||||||
subcommands: subcommands,
|
subcommands: subcommands,
|
||||||
configFiles: constructConfigDocs(cmd, conf),
|
configFiles: constructConfigDocs(cmd, conf),
|
||||||
|
date: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func paragraphs(s string) string {
|
func showHelp(out io.Writer, cmd Cmd, conf Config, fullCommand []string) error {
|
||||||
var (
|
doc := constructDoc(cmd, conf, fullCommand)
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|
||||||
paragraphs = append(paragraphs, paragraph)
|
|
||||||
|
|
||||||
var cparagraphs []string
|
|
||||||
for _, p := range paragraphs {
|
|
||||||
cparagraphs = append(cparagraphs, strings.Join(p, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(cparagraphs, "\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func showHelp(out io.Writer, cmd Cmd, fullCommand []string, conf Config) error {
|
|
||||||
doc := constructDoc(cmd, conf, fullCommand, hasConfigFromOption(conf))
|
|
||||||
return formatHelp(out, doc)
|
return formatHelp(out, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateMan(out io.Writer, cmd Cmd, conf Config) error {
|
func generateMan(out io.Writer, cmd Cmd, conf Config) error {
|
||||||
doc := constructDoc(cmd, conf, []string{cmd.name}, hasConfigFromOption(conf))
|
doc := constructDoc(cmd, conf, []string{cmd.name})
|
||||||
return formatMan(out, doc)
|
return formatMan(out, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
|
func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
|
||||||
doc := constructDoc(cmd, conf, []string{cmd.name}, hasConfigFromOption(conf))
|
doc := constructDoc(cmd, conf, []string{cmd.name})
|
||||||
return formatMarkdown(out, doc, level)
|
return formatMarkdown(out, doc, level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showVersion(out io.Writer, cmd Cmd) error {
|
||||||
|
_, err := fmt.Fprintln(out, cmd.version)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
8
input.go
8
input.go
@ -149,6 +149,10 @@ func validatePositionalArgs(cmd Cmd, a []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, ai := range a {
|
for i, ai := range a {
|
||||||
|
if slices.Contains(ior, i) || slices.Contains(iow, i) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var pi reflect.Type
|
var pi reflect.Type
|
||||||
if i >= length {
|
if i >= length {
|
||||||
pi = p[length-1]
|
pi = p[length-1]
|
||||||
@ -156,6 +160,10 @@ func validatePositionalArgs(cmd Cmd, a []string) error {
|
|||||||
pi = p[i]
|
pi = p[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pi.Kind() == reflect.Interface {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if !canScan(pi, ai) {
|
if !canScan(pi, ai) {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"cannot apply positional argument at index %d, expecting %v",
|
"cannot apply positional argument at index %d, expecting %v",
|
||||||
|
|||||||
21
notes.txt
21
notes.txt
@ -0,0 +1,21 @@
|
|||||||
|
fix go.mod file path in exec
|
||||||
|
test starting from the most referenced file to the least referenced one
|
||||||
|
run only the related test when testing a file
|
||||||
|
|
||||||
|
reflect
|
||||||
|
command
|
||||||
|
commandline
|
||||||
|
config
|
||||||
|
doclets
|
||||||
|
format
|
||||||
|
formathelp
|
||||||
|
formatman
|
||||||
|
formatmarkdown
|
||||||
|
help
|
||||||
|
env
|
||||||
|
input
|
||||||
|
output
|
||||||
|
apply
|
||||||
|
exec
|
||||||
|
wand
|
||||||
|
tools/tools
|
||||||
@ -80,7 +80,7 @@ func parseInt(s string, byteSize int) (int64, error) {
|
|||||||
case strings.HasPrefix(s, "0"):
|
case strings.HasPrefix(s, "0"):
|
||||||
return strconv.ParseInt(s[1:], 8, bitSize)
|
return strconv.ParseInt(s[1:], 8, bitSize)
|
||||||
default:
|
default:
|
||||||
return strconv.ParseInt(s[2:], 2, byteSize*8)
|
return strconv.ParseInt(s, 10, bitSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ func parseUint(s string, byteSize int) (uint64, error) {
|
|||||||
case strings.HasPrefix(s, "0"):
|
case strings.HasPrefix(s, "0"):
|
||||||
return strconv.ParseUint(s[1:], 8, bitSize)
|
return strconv.ParseUint(s[1:], 8, bitSize)
|
||||||
default:
|
default:
|
||||||
return strconv.ParseUint(s[2:], 2, bitSize)
|
return strconv.ParseUint(s, 10, bitSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
84
reflect_test.go
Normal file
84
reflect_test.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package wand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"io"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReflect(t *testing.T) {
|
||||||
|
t.Run("pack and unpack", func(t *testing.T) {
|
||||||
|
f := func(a int) int { return a }
|
||||||
|
t.Run("no need to pack", testExec(testCase{impl: f, command: "foo 42"}, "", "42"))
|
||||||
|
g := func(a *int) int { return *a }
|
||||||
|
t.Run("pointer", testExec(testCase{impl: g, command: "foo 42"}, "", "42"))
|
||||||
|
h := func(a []int) int { return a[0] }
|
||||||
|
t.Run("slice", testExec(testCase{impl: h, command: "foo 42"}, "", "42"))
|
||||||
|
i := func(a *[]*[]int) int { return (*((*a)[0]))[0] }
|
||||||
|
t.Run("pointer and slice", testExec(testCase{impl: i, command: "foo 42"}, "", "42"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("io params", func(t *testing.T) {
|
||||||
|
f := func(in io.Reader) string { b, _ := io.ReadAll(in); return string(b) }
|
||||||
|
t.Run("in", testExec(testCase{impl: f, stdin: "foo", command: "bar"}, "", "foo"))
|
||||||
|
g := func(a string) io.Reader { return bytes.NewBufferString(a) }
|
||||||
|
t.Run("out", testExec(testCase{impl: g, command: "foo bar"}, "", "bar"))
|
||||||
|
h := func(out io.Writer, a string) { out.Write([]byte(a)) }
|
||||||
|
t.Run("stdout param", testExec(testCase{impl: h, command: "foo bar"}, "", "bar"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("struct param", func(t *testing.T) {
|
||||||
|
f := func(s struct{Bar int}) int { return s.Bar }
|
||||||
|
t.Run("basic", testExec(testCase{impl: f, command: "foo --bar 42"}, "", "42"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("basic types", func(t *testing.T) {
|
||||||
|
t.Run("signed int", func(t *testing.T) {
|
||||||
|
f := func(a int) int { return a }
|
||||||
|
t.Run("decimal", testExec(testCase{impl: f, command: "foo 42"}, "", "42"))
|
||||||
|
t.Run("hexa", testExec(testCase{impl: f, command: "foo 0x2a"}, "", "42"))
|
||||||
|
t.Run("octal", testExec(testCase{impl: f, command: "foo 052"}, "", "42"))
|
||||||
|
t.Run("binary", testExec(testCase{impl: f, command: "foo 0b101010"}, "", "42"))
|
||||||
|
t.Run("fail", testExec(testCase{impl: f, command: "foo bar"}, "expecting int", ""))
|
||||||
|
g := func(a int32) int32 { return a }
|
||||||
|
t.Run("sized", testExec(testCase{impl: g, command: "foo 42"}, "", "42"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unsigned int", func(t *testing.T) {
|
||||||
|
f := func(a uint) uint { return a }
|
||||||
|
t.Run("decimal", testExec(testCase{impl: f, command: "foo 42"}, "", "42"))
|
||||||
|
t.Run("hexa", testExec(testCase{impl: f, command: "foo 0x2a"}, "", "42"))
|
||||||
|
t.Run("octal", testExec(testCase{impl: f, command: "foo 052"}, "", "42"))
|
||||||
|
t.Run("binary", testExec(testCase{impl: f, command: "foo 0b101010"}, "", "42"))
|
||||||
|
t.Run("fail", testExec(testCase{impl: f, command: "foo bar"}, "expecting uint", ""))
|
||||||
|
g := func(a uint32) uint32 { return a }
|
||||||
|
t.Run("sized", testExec(testCase{impl: g, command: "foo 42"}, "", "42"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bool", func(t *testing.T) {
|
||||||
|
f := func(a bool) bool { return a }
|
||||||
|
t.Run("true", testExec(testCase{impl: f, command: "foo true"}, "", "true"))
|
||||||
|
t.Run("false", testExec(testCase{impl: f, command: "foo false"}, "", "false"))
|
||||||
|
t.Run("fail", testExec(testCase{impl: f, command: "foo yes"}, "expecting bool", ""))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("float", func(t *testing.T) {
|
||||||
|
f := func(a float64) float64 { return a }
|
||||||
|
t.Run("accept", testExec(testCase{impl: f, command: "foo 3.14"}, "", "3.14"))
|
||||||
|
t.Run("fail", testExec(testCase{impl: f, command: "foo bar"}, "expecting float", ""))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
f := func(a string) string { return a }
|
||||||
|
t.Run("accept", testExec(testCase{impl: f, command: "foo bar"}, "", "bar"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("interface", func(t *testing.T) {
|
||||||
|
f := func(a any) any { return a }
|
||||||
|
t.Run("any", testExec(testCase{impl: f, command: "foo bar"}, "", "bar"))
|
||||||
|
type i interface{Foo()}
|
||||||
|
g := func(a i) any { return a }
|
||||||
|
t.Run("unscannable", testExec(testCase{impl: g, command: "foo bar"}, "non-empty interface", ""))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -180,7 +180,7 @@ func Exec(o ExecOptions, function string, args ...string) error {
|
|||||||
|
|
||||||
cacheDir := o.CacheDir
|
cacheDir := o.CacheDir
|
||||||
if cacheDir == "" {
|
if cacheDir == "" {
|
||||||
path.Join(os.Getenv("HOME"), ".wand")
|
cacheDir = path.Join(os.Getenv("HOME"), ".wand")
|
||||||
}
|
}
|
||||||
|
|
||||||
functionDir := path.Join(cacheDir, functionHash)
|
functionDir := path.Join(cacheDir, functionHash)
|
||||||
@ -204,6 +204,7 @@ func Exec(o ExecOptions, function string, args ...string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goGet := func(pkg string) error {
|
goGet := func(pkg string) error {
|
||||||
|
println("go get", pkg)
|
||||||
if err := execInternal("go get", pkg); err != nil {
|
if err := execInternal("go get", pkg); err != nil {
|
||||||
return fmt.Errorf("failed to get go module: %w", err)
|
return fmt.Errorf("failed to get go module: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
14
wand.go
14
wand.go
@ -11,6 +11,7 @@ type Config struct {
|
|||||||
merge []Config
|
merge []Config
|
||||||
fromOption bool
|
fromOption bool
|
||||||
optional bool
|
optional bool
|
||||||
|
test string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
@ -23,9 +24,11 @@ type Cmd struct {
|
|||||||
shortForms []string
|
shortForms []string
|
||||||
description string
|
description string
|
||||||
isHelp bool
|
isHelp bool
|
||||||
helpRequested bool
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// name needs to be valid symbol. The application name should also be a valid symbol,
|
||||||
|
// though not mandatory. If it is not, the environment variables may not work properly.
|
||||||
func Command(name string, impl any, subcmds ...Cmd) Cmd {
|
func Command(name string, impl any, subcmds ...Cmd) Cmd {
|
||||||
return command(name, impl, subcmds...)
|
return command(name, impl, subcmds...)
|
||||||
}
|
}
|
||||||
@ -53,6 +56,15 @@ func ShortFormOptions(cmd Cmd, f ...string) Cmd {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Version(cmd Cmd, version string) Cmd {
|
||||||
|
cmd.subcommands = append(
|
||||||
|
cmd.subcommands,
|
||||||
|
Cmd{name: "version", version: version},
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
func MergeConfig(conf ...Config) Config {
|
func MergeConfig(conf ...Config) Config {
|
||||||
return Config{
|
return Config{
|
||||||
merge: conf,
|
merge: conf,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user