Skip to content

Commit

Permalink
ability to print usage per parser (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
gobwas authored Jun 9, 2020
1 parent fbb7e82 commit 589bfe5
Show file tree
Hide file tree
Showing 15 changed files with 807 additions and 63 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,14 @@ func main() {

// Then lookup for "config" flag value and try to parse its value as a
// json configuration file.
flagutil.WithParser(&file.Parser{
Lookup: file.FlagLookup("config"),
Syntax: &json.Syntax{},
}),
flagutil.WithParser(
&file.Parser{
Lookup: file.LookupFlag(flags, "config"),
Syntax: new(json.Syntax),
},
// Don't allow to setup "config" flag from file.
flagutil.WithIgnoreByName("config"),
),
)

// Work with received values.
Expand Down
317 changes: 295 additions & 22 deletions flagutil.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package flagutil

import (
"bytes"
"flag"
"fmt"
"os"
"reflect"
"strings"
"time"

"github.com/gobwas/flagutil/parse"
)
Expand All @@ -14,38 +18,56 @@ type Parser interface {
Parse(parse.FlagSet) error
}

type ParseOption func(*config)
type ParserFunc func(parse.FlagSet) error

func WithParser(p Parser) ParseOption {
return func(c *config) {
c.parsers = append(c.parsers, p)
}
func (fn ParserFunc) Parse(fs parse.FlagSet) error {
return fn(fs)
}

func WithIgnoreUndefined() ParseOption {
return func(c *config) {
c.ignoreUndefined = true
}
type Printer interface {
Name(parse.FlagSet) func(*flag.Flag, func(string))
}

type PrinterFunc func(parse.FlagSet) func(*flag.Flag, func(string))

func (fn PrinterFunc) Name(fs parse.FlagSet) func(*flag.Flag, func(string)) {
return fn(fs)
}

type parser struct {
Parser
ignore func(*flag.Flag) bool
}

type config struct {
parsers []Parser
ignoreUndefined bool
parsers []*parser
ignoreUndefined bool
ignoreUsage bool
unquoteUsageMode UnquoteUsageMode
}

func Parse(flags *flag.FlagSet, opts ...ParseOption) (err error) {
var c config
func Parse(flags *flag.FlagSet, opts ...Option) (err error) {
c := config{
unquoteUsageMode: UnquoteDefault,
}
for _, opt := range opts {
opt(&c)
switch x := opt.(type) {
case ParseOption:
x.setupParseConfig(&c)
case PrintOption:
x.setupPrintConfig(&c)
}
}
fs := parse.NewFlagSet(flags,
parse.WithIgnoreUndefined(c.ignoreUndefined),
)
for _, p := range c.parsers {
parse.NextLevel(fs)
parse.Ignore(fs, p.ignore)

if err = p.Parse(fs); err != nil {
if err == flag.ErrHelp {
printUsage(flags)
printUsage(&c, flags)
}
switch flags.ErrorHandling() {
case flag.ContinueOnError:
Expand All @@ -63,17 +85,261 @@ func Parse(flags *flag.FlagSet, opts ...ParseOption) (err error) {
return nil
}

func printUsage(f *flag.FlagSet) {
if f.Usage != nil {
f.Usage()
// PrintDefaults prints parsers aware usage message to flags.Output().
func PrintDefaults(flags *flag.FlagSet, opts ...PrintOption) {
c := config{
unquoteUsageMode: UnquoteDefault,
}
for _, opt := range opts {
opt.setupPrintConfig(&c)
}
printDefaults(&c, flags)
}

func printUsage(c *config, flags *flag.FlagSet) {
if !c.ignoreUsage && flags.Usage != nil {
flags.Usage()
return
}
if name := f.Name(); name == "" {
fmt.Fprintf(f.Output(), "Usage:\n")
if name := flags.Name(); name == "" {
fmt.Fprintf(flags.Output(), "Usage:\n")
} else {
fmt.Fprintf(f.Output(), "Usage of %s:\n", name)
fmt.Fprintf(flags.Output(), "Usage of %s:\n", name)
}
f.PrintDefaults()
printDefaults(c, flags)
}

type UnquoteUsageMode uint8

const (
UnquoteNothing UnquoteUsageMode = 1 << iota >> 1
UnquoteQuoted
UnquoteInferType
UnquoteClean

UnquoteDefault UnquoteUsageMode = UnquoteQuoted | UnquoteInferType
)

func (m UnquoteUsageMode) String() string {
switch m {
case UnquoteNothing:
return "UnquoteNothing"
case UnquoteQuoted:
return "UnquoteQuoted"
case UnquoteInferType:
return "UnquoteInferType"
case UnquoteClean:
return "UnquoteClean"
case UnquoteDefault:
return "UnquoteDefault"
default:
return "<unknown>"
}
}

func (m UnquoteUsageMode) has(x UnquoteUsageMode) bool {
return m&x != 0
}

func printDefaults(c *config, flags *flag.FlagSet) {
fs := parse.NewFlagSet(flags)

var hasNameFunc bool
nameFunc := make([]func(*flag.Flag, func(string)), len(c.parsers))
for i := len(c.parsers) - 1; i >= 0; i-- {
if p, ok := c.parsers[i].Parser.(Printer); ok {
hasNameFunc = true
nameFunc[i] = p.Name(fs)
}
}

var buf bytes.Buffer
flags.VisitAll(func(f *flag.Flag) {
n, _ := buf.WriteString(" ")
for i := len(c.parsers) - 1; i >= 0; i-- {
fn := nameFunc[i]
if fn == nil {
continue
}
if ignore := c.parsers[i].ignore; ignore != nil && ignore(f) {
continue
}
fn(f, func(name string) {
if buf.Len() > n {
buf.WriteString(", ")
}
buf.WriteString(name)
})
}
if buf.Len() == n {
// No name has been given.
// Two cases are possible: no Printer implementation among parsers;
// or some parser intentionally filtered out this flag.
if hasNameFunc {
buf.Reset()
return
}
buf.WriteString(f.Name)
}
name, usage := unquoteUsage(c.unquoteUsageMode, f)
if len(name) > 0 {
buf.WriteString("\n \t")
buf.WriteString(name)
}
buf.WriteString("\n \t")
if len(usage) > 0 {
buf.WriteString(strings.ReplaceAll(usage, "\n", "\n \t"))
buf.WriteString(" (")
}
buf.WriteString("default ")
buf.WriteString(defValue(f))
if len(usage) > 0 {
buf.WriteString(")")
}

buf.WriteByte('\n')
buf.WriteByte('\n')
buf.WriteTo(flags.Output())
})
}

func defValue(f *flag.Flag) string {
v := reflect.ValueOf(f.Value)
repeat:
if v.Kind() == reflect.Ptr {
v = v.Elem()
goto repeat
}
d := f.DefValue
if d == "" {
switch v.Kind() {
case reflect.String:
return `""`
case
reflect.Slice,
reflect.Array:
return `[]`
case
reflect.Struct,
reflect.Map:
return `{}`
}
return "?"
}
if v.Kind() == reflect.String {
return `"` + d + `"`
}
return d
}

// unquoteUsage is the same as flag.UnquoteUsage() with exception that it does
// not infer type of the flag value.
func unquoteUsage(m UnquoteUsageMode, f *flag.Flag) (name, usage string) {
if m == UnquoteNothing {
return "", f.Usage
}
u := f.Usage
i := strings.IndexByte(u, '`')
if i == -1 {
if m.has(UnquoteInferType) {
return inferType(f), f.Usage
}
return "", u
}
j := strings.IndexByte(u[i+1:], '`')
if j == -1 {
if m.has(UnquoteInferType) {
return inferType(f), f.Usage
}
return "", u
}
j += i + 1

switch {
case m.has(UnquoteQuoted):
name = u[i+1 : j]
case m.has(UnquoteInferType):
name = inferType(f)
}

prefix := u[:i]
suffix := u[j+1:]
switch {
case m.has(UnquoteClean):
usage = "" +
strings.TrimRight(prefix, " ") +
" " +
strings.TrimLeft(suffix, " ")

case m.has(UnquoteQuoted):
usage = prefix + name + suffix

default:
usage = f.Usage
}

return
}

func inferType(f *flag.Flag) string {
if f.Value == nil {
return "?"
}
if isBoolFlag(f) {
return "bool"
}

var x interface{}
if g, ok := f.Value.(flag.Getter); ok {
x = g.Get()
} else {
x = f.Value
}
v := reflect.ValueOf(x)

repeat:
switch v.Type() {
case reflect.TypeOf(time.Duration(0)):
return "duration"
}
switch v.Kind() {
case
reflect.Interface,
reflect.Ptr:

v = v.Elem()
goto repeat

case
reflect.String:
return "string"
case
reflect.Float32,
reflect.Float64:
return "float"
case
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64:
return "int"
case
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64:
return "uint"
case
reflect.Slice,
reflect.Array:
return "list"
case
reflect.Map:
return "object"
}
return ""
}

// Subset registers new flag subset with given prefix within given flag
Expand All @@ -95,3 +361,10 @@ func Subset(super *flag.FlagSet, prefix string, setup func(sub *flag.FlagSet)) (
})
return
}

func isBoolFlag(f *flag.Flag) bool {
x, ok := f.Value.(interface {
IsBoolFlag() bool
})
return ok && x.IsBoolFlag()
}
Loading

0 comments on commit 589bfe5

Please sign in to comment.