diff --git a/cmd/d2/help.go b/cmd/d2/help.go index 9b304ee34..ccc85332f 100644 --- a/cmd/d2/help.go +++ b/cmd/d2/help.go @@ -28,7 +28,7 @@ Subcommands: %[1]s layout [layout name] - Display long help for a particular layout engine See more docs and the source code at https://oss.terrastruct.com/d2 -`, ms.Name, ms.Opts.Help()) +`, ms.Name, ms.Opts.Defaults()) } func layoutHelp(ctx context.Context, ms *xmain.State) error { diff --git a/lib/xmain/flag_helpers.go b/lib/xmain/flag_helpers.go new file mode 100644 index 000000000..4d17066dc --- /dev/null +++ b/lib/xmain/flag_helpers.go @@ -0,0 +1,45 @@ +// flag_helpers.go are private functions from pflag/flag.go +package xmain + +import "strings" + +func wrap(i, w int, s string) string { + if w == 0 { + return strings.Replace(s, "\n", "\n"+strings.Repeat(" ", i), -1) + } + wrap := w - i + var r, l string + if wrap < 24 { + i = 16 + wrap = w - i + r += "\n" + strings.Repeat(" ", i) + } + if wrap < 24 { + return strings.Replace(s, "\n", r, -1) + } + slop := 5 + wrap = wrap - slop + l, s = wrapN(wrap, slop, s) + r = r + strings.Replace(l, "\n", "\n"+strings.Repeat(" ", i), -1) + for s != "" { + var t string + t, s = wrapN(wrap, slop, s) + r = r + "\n" + strings.Repeat(" ", i) + strings.Replace(t, "\n", "\n"+strings.Repeat(" ", i), -1) + } + return r +} + +func wrapN(i, slop int, s string) (string, string) { + if i+slop > len(s) { + return s, "" + } + w := strings.LastIndexAny(s[:i], " \t\n") + if w <= 0 { + return s, "" + } + nlPos := strings.LastIndex(s[:i], "\n") + if nlPos > 0 && nlPos < w { + return s[:nlPos], s[nlPos+1:] + } + return s[:w], s[w+1:] +} diff --git a/lib/xmain/opts.go b/lib/xmain/opts.go index ed260537f..98d9387ef 100644 --- a/lib/xmain/opts.go +++ b/lib/xmain/opts.go @@ -1,6 +1,7 @@ package xmain import ( + "bytes" "fmt" "io" "strconv" @@ -17,7 +18,7 @@ type Opts struct { env *xos.Env log *cmdlog.Logger - registeredEnvs []string + flagEnv map[string]string } func NewOpts(env *xos.Env, log *cmdlog.Logger, args []string) *Opts { @@ -26,42 +27,106 @@ func NewOpts(env *xos.Env, log *cmdlog.Logger, args []string) *Opts { flags.Usage = func() {} flags.SetOutput(io.Discard) return &Opts{ - Args: args, - Flags: flags, - env: env, - log: log, + Args: args, + Flags: flags, + env: env, + log: log, + flagEnv: make(map[string]string), } } -func (o *Opts) Help() string { - b := &strings.Builder{} - o.Flags.SetOutput(b) - o.Flags.PrintDefaults() +// Mostly copy pasted pasted from pflag.FlagUsagesWrapped +// with modifications for env var +func (o *Opts) Defaults() string { + buf := new(bytes.Buffer) - if len(o.registeredEnvs) > 0 { - b.WriteString("\nYou may persistently set the following as environment variables (flags take precedent):\n") - for i, e := range o.registeredEnvs { - s := fmt.Sprintf("- $%s", e) - if i != len(o.registeredEnvs)-1 { - s += "\n" - } - b.WriteString(s) + var lines []string + + maxlen := 0 + maxEnvLen := 0 + o.Flags.VisitAll(func(flag *pflag.Flag) { + if flag.Hidden { + return } + + line := "" + if flag.Shorthand != "" && flag.ShorthandDeprecated == "" { + line = fmt.Sprintf(" -%s, --%s", flag.Shorthand, flag.Name) + } else { + line = fmt.Sprintf(" --%s", flag.Name) + } + + varname, usage := pflag.UnquoteUsage(flag) + if varname != "" { + line += " " + varname + } + if flag.NoOptDefVal != "" { + switch flag.Value.Type() { + case "string": + line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal) + case "bool": + if flag.NoOptDefVal != "true" { + line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) + } + case "count": + if flag.NoOptDefVal != "+1" { + line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) + } + default: + line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) + } + } + + line += "\x00" + + if len(line) > maxlen { + maxlen = len(line) + } + + if e, ok := o.flagEnv[flag.Name]; ok { + line += fmt.Sprintf("$%s", e) + } + + line += "\x01" + + if len(line) > maxEnvLen { + maxEnvLen = len(line) + } + + line += usage + if flag.Value.Type() == "string" { + line += fmt.Sprintf(" (default %q)", flag.DefValue) + } else { + line += fmt.Sprintf(" (default %s)", flag.DefValue) + } + if len(flag.Deprecated) != 0 { + line += fmt.Sprintf(" (DEPRECATED: %s)", flag.Deprecated) + } + + lines = append(lines, line) + }) + + for _, line := range lines { + sidx1 := strings.Index(line, "\x00") + sidx2 := strings.Index(line, "\x01") + spacing1 := strings.Repeat(" ", maxlen-sidx1) + spacing2 := strings.Repeat(" ", (maxEnvLen-maxlen)-sidx2+sidx1) + fmt.Fprintln(buf, line[:sidx1], spacing1, line[sidx1+1:sidx2], spacing2, wrap(maxEnvLen+3, 0, line[sidx2+1:])) } - return b.String() + return buf.String() } -func (o *Opts) getEnv(k string) string { +func (o *Opts) getEnv(flag, k string) string { if k != "" { - o.registeredEnvs = append(o.registeredEnvs, k) + o.flagEnv[flag] = k return o.env.Getenv(k) } return "" } func (o *Opts) Int64(envKey, flag, shortFlag string, defaultVal int64, usage string) (*int64, error) { - if env := o.getEnv(envKey); env != "" { + if env := o.getEnv(flag, envKey); env != "" { envVal, err := strconv.ParseInt(env, 10, 64) if err != nil { return nil, fmt.Errorf(`invalid environment variable %s. Expected int64. Found "%v".`, envKey, envVal) @@ -73,7 +138,7 @@ func (o *Opts) Int64(envKey, flag, shortFlag string, defaultVal int64, usage str } func (o *Opts) String(envKey, flag, shortFlag string, defaultVal, usage string) *string { - if env := o.getEnv(envKey); env != "" { + if env := o.getEnv(flag, envKey); env != "" { defaultVal = env } @@ -81,7 +146,7 @@ func (o *Opts) String(envKey, flag, shortFlag string, defaultVal, usage string) } func (o *Opts) Bool(envKey, flag, shortFlag string, defaultVal bool, usage string) (*bool, error) { - if env := o.getEnv(envKey); env != "" { + if env := o.getEnv(flag, envKey); env != "" { if !boolyEnv(env) { return nil, fmt.Errorf(`invalid environment variable %s. Expected bool. Found "%s".`, envKey, env) }