complete help

This commit is contained in:
Arpad Ryszka 2025-08-24 04:46:54 +02:00
parent f206c694b2
commit 1dac4b0af0
12 changed files with 484 additions and 243 deletions

4
.gitignore vendored
View File

@ -1,3 +1,3 @@
iniparser.go iniparser.gen.go
docreflect.go docreflect.gen.go
.bin .bin

View File

@ -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 default: build
lib: $(SOURCES) iniparser.go docreflect.go lib: $(SOURCES) iniparser.gen.go docreflect.gen.go
go build go build
go build ./tools go build ./tools
@ -20,23 +20,24 @@ cover: .cover
showcover: .cover showcover: .cover
go tool cover -html .cover go tool cover -html .cover
fmt: $(SOURCES) iniparser.go fmt: $(SOURCES) iniparser.gen.go docreflect.gen.go
go fmt ./... go fmt ./...
iniparser.go: ini.treerack iniparser.gen.go: ini.treerack
go run script/ini-parser/parser.go wand < ini.treerack > iniparser.go || rm iniparser.go 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 \ go run script/docreflect/docs.go \
wand \ wand \
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.go > docreflect.gen.go \
|| rm docreflect.gen.go
.bin: .bin:
mkdir -p .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 go build -o .bin/wand ./cmd/wand
install: wand install: wand

View File

@ -19,7 +19,7 @@ type config struct {
originalNames map[string]string originalNames map[string]string
} }
func fileReader(filename string) io.ReadCloser { func fileReader(filename string) *file {
return &file{ return &file{
filename: filename, filename: filename,
} }
@ -129,7 +129,7 @@ func readConfigFromOption(cmd Cmd, cl commandLine, conf Config) (config, error)
continue 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}) return readConfig(cmd, cl, Config{merge: c})

View File

@ -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 os.Getenv("wandgenerate") == "man" {
if err := generateMan(stdout, cmd); err != nil { if err := generateMan(stdout, cmd, conf); err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
exit(1) 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" { if os.Getenv("wandgenerate") == "markdown" {
level, _ := strconv.Atoi(os.Getenv("wandmarkdownlevel")) 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) fmt.Fprintln(stderr, err)
exit(1) 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 cmd.helpRequested {
if err := showHelp(stdout, cmd, fullCmd); err != nil { if err := showHelp(stdout, cmd, fullCmd, conf); err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
exit(1) exit(1)
} }
@ -58,7 +58,7 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
bo := boolOptions(cmd) bo := boolOptions(cmd)
cl := readArgs(bo, args) cl := readArgs(bo, args)
if hasHelpOption(cmd, cl.options) { 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) fmt.Fprintln(stderr, err)
exit(1) exit(1)
} }

252
formathelp.go Normal file
View 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
View 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
View 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
View File

@ -5,12 +5,37 @@ import (
"fmt" "fmt"
"io" "io"
"reflect" "reflect"
"sort"
"strings" "strings"
) )
const defaultWrap = 112 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
@ -18,6 +43,8 @@ type (
variadic bool variadic bool
usesStdin bool usesStdin bool
usesStdout bool usesStdout bool
minPositional int
maxPositional int
} }
synopsis struct { synopsis struct {
@ -35,18 +62,27 @@ type (
acceptsMultiple bool acceptsMultiple bool
} }
docConfig struct {
fromOption bool
optional bool
fn string
}
doc struct { doc struct {
name string name string
fullCommand string fullCommand string
synopsis synopsis synopsis synopsis
description string description string
hasImplementation bool
isHelp bool isHelp bool
isDefault bool isDefault bool
hasHelpSubcommand bool hasHelpSubcommand bool
hasHelpOption bool hasHelpOption bool
hasConfigFromOption bool
options []docOption options []docOption
arguments argumentSet arguments argumentSet
subcommands []doc subcommands []doc
configFiles []docConfig
} }
) )
@ -146,6 +182,8 @@ func constructArguments(cmd Cmd) argumentSet {
variadic: t.IsVariadic(), variadic: t.IsVariadic(),
usesStdin: len(ior) > 0, usesStdin: len(ior) > 0,
usesStdout: len(iow) > 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))) 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 { if cmd.impl == nil {
return nil return nil
} }
@ -206,13 +244,41 @@ func constructOptions(cmd Cmd) []docOption {
o = append(o, opt) o = append(o, opt)
} }
if hasConfigFromOption {
o = append(o, docOption{
name: "config",
description: configOptionDocs,
shortNames: sf["config"],
acceptsMultiple: true,
})
}
return o 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 var subcommands []doc
for _, sc := range cmd.subcommands { 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{ return doc{
@ -220,204 +286,59 @@ func constructDoc(cmd Cmd, fullCommand []string) doc {
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,
isDefault: cmd.isDefault, isDefault: cmd.isDefault,
isHelp: cmd.isHelp, isHelp: cmd.isHelp,
hasHelpSubcommand: hasHelpSubcommand(cmd), hasHelpSubcommand: hasHelpSubcommand(cmd),
hasHelpOption: hasCustomHelpOption(cmd), hasHelpOption: hasCustomHelpOption(cmd),
options: constructOptions(cmd), options: constructOptions(cmd, hasConfigFromOption),
arguments: constructArguments(cmd), arguments: constructArguments(cmd),
subcommands: subcommands, subcommands: subcommands,
configFiles: constructConfigDocs(cmd, conf),
} }
} }
func formatHelp(w io.Writer, doc doc) error { func paragraphs(s string) string {
var err error var (
println := func(a ...any) { paragraph []string
if err != nil { paragraphs [][]string
return )
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) { paragraph = append(paragraph, l[i])
if err != nil {
return
} }
_, 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) return strings.Join(cparagraphs, "\n\n")
println()
println()
printf("Synopsis")
println()
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("...")
}
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 { func showHelp(out io.Writer, cmd Cmd, fullCommand []string, conf Config) error {
doc := constructDoc(cmd, fullCommand) doc := constructDoc(cmd, conf, fullCommand, hasConfigFromOption(conf))
return formatHelp(out, doc) return formatHelp(out, doc)
} }
func formatMan(out io.Writer, doc doc) error { func generateMan(out io.Writer, cmd Cmd, conf Config) error {
// if no subcommands, then similar to help doc := constructDoc(cmd, conf, []string{cmd.name}, hasConfigFromOption(conf))
// otherwise:
// title
// all commands
return nil
}
func generateMan(out io.Writer, cmd Cmd) error {
doc := constructDoc(cmd, []string{cmd.name})
return formatMan(out, doc) return formatMan(out, doc)
} }
func formatMarkdown(out io.Writer, doc doc) error { func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
// if no subcommands, then similar to help doc := constructDoc(cmd, conf, []string{cmd.name}, hasConfigFromOption(conf))
// otherwise: return formatMarkdown(out, doc, level)
// 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)
} }

View File

@ -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

View File

@ -1,23 +1,63 @@
package wand package wand
import ( import (
"code.squareroundforest.org/arpio/notation"
"fmt" "fmt"
"io" "io"
"reflect"
) )
func printOutput(w io.Writer, o []any) error { 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 { for _, oi := range o {
r, ok := oi.(io.Reader) r, ok := oi.(io.Reader)
if ok { if ok {
if _, err := io.Copy(w, r); err != nil { if _, err := io.Copy(w, r); err != nil {
return fmt.Errorf("error copying output: %w", err) return wraperr(err)
} }
continue continue
} }
if _, err := fmt.Fprintf(w, "%v\n", oi); err != nil { t := reflect.TypeOf(oi)
return fmt.Errorf("error printing output: %w", err) 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)
}
} }
} }

View File

@ -5,6 +5,7 @@ import (
"github.com/iancoleman/strcase" "github.com/iancoleman/strcase"
"io" "io"
"reflect" "reflect"
"slices"
"strconv" "strconv"
"strings" "strings"
) )
@ -44,14 +45,16 @@ func pack(v reflect.Value, t reflect.Type) reflect.Value {
return s return s
} }
func unpack[T packedKind[T]](p T) T { func unpack[T packedKind[T]](p T, kinds ...reflect.Kind) T {
switch p.Kind() { if len(kinds) == 0 {
case reflect.Pointer, kinds = []reflect.Kind{reflect.Pointer, reflect.Slice}
reflect.Slice:
return unpack(p.Elem())
default:
return p
} }
if slices.Contains(kinds, p.Kind()) {
return unpack(p.Elem(), kinds...)
}
return p
} }
func isReader(t reflect.Type) bool { func isReader(t reflect.Type) bool {

21
wand.go
View File

@ -1,13 +1,13 @@
package wand package wand
import ( import (
"io" "fmt"
"os" "os"
"path" "path"
) )
type Config struct { type Config struct {
file func(Cmd) io.ReadCloser file func(Cmd) *file
merge []Config merge []Config
fromOption bool fromOption bool
optional bool optional bool
@ -70,20 +70,29 @@ func OptionalConfig(conf Config) Config {
func Etc() Config { func Etc() Config {
return OptionalConfig(Config{ return OptionalConfig(Config{
file: func(cmd Cmd) io.ReadCloser { file: func(cmd Cmd) *file {
return fileReader(path.Join("/etc", cmd.name, "config")) return fileReader(path.Join("/etc", cmd.name, "config"))
}, },
}) })
} }
func UserConfig() Config { func UserConfig() Config {
return OptionalConfig(Config{ return OptionalConfig(MergeConfig(
file: func(cmd Cmd) io.ReadCloser { 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( return fileReader(
path.Join(os.Getenv("HOME"), ".config", cmd.name, "config"), path.Join(os.Getenv("HOME"), ".config", cmd.name, "config"),
) )
}, },
}) },
))
} }
func ConfigFromOption() Config { func ConfigFromOption() Config {