Skip to content

Commit

Permalink
Add FlagSet.FlagUsagesWrapped(cols) which wraps to the given column (#…
Browse files Browse the repository at this point in the history
…105)

This will allow applications to provide better help text without feeling
constrained by how it will wrap even on 80 column displays etc. This has been a
factor in tickets such as moby/moby#18797.

The basic wrapping algorithm is rather simplistic, it will look for the last
whitespace (space or tab) before the given column and wrap there, indenting the
continuation lines to match the usage text (i.e. aligned after the flag names
themselves), e.g. when applied to `docker ps` wrapping at 70 columns (fairly
narrow):

Options:
[...]
  -f, --filter filter   Filter output based on conditions provided
[...]
  -n, --last int        Show n last created containers
                        (includes all states) (default -1)
  -l, --latest          Show the latest created container
                        (includes all states)

There are two main tweaks to this basic algorithm, first is to actually try and
wrap to a soft limit 5 columns less than requested but allow the line to take
up the full hard width if that prevents pushing a short word at the end of the
string to the next line which looks odd, particuarly for usage which ends with
"(default [])" and wraps the "[])" to the last line.

Second if the display is too narrow, meaning after indentation for the flag
names there is less than 24 columns allowed for the help text (24 chosen just
by my eye and what I thought looked odd) then it will start the help text on
the next line indented to the 16th column (16 chosen so as not to align with
the first character of either the short of long flag name as 8 would, since
that looked strange to me), e.g. wrapping the above example to a rather narrow
45 columns:

Options:
[...]
  -f, --filter filter
                Filter output based on
                conditions provided
[...]
  -n, --last int
                Show n last created
                containers (includes
                all states) (default -1)
  -l, --latest
                Show the latest
                created container
                (includes all states)

If even with starting the help on the next line there is still less than 24
characters of space available for the help text (implying columns < 24 + 16 ==
40) just give up and return the unwrapped version (same as before this change).

Signed-off-by: Ian Campbell <[email protected]>,
  • Loading branch information
Ian Campbell authored and eparis committed Jan 30, 2017
1 parent a9a634f commit 9ff6c69
Showing 1 changed file with 78 additions and 4 deletions.
82 changes: 78 additions & 4 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,9 +487,76 @@ func UnquoteUsage(flag *Flag) (name string, usage string) {
return
}

// FlagUsages Returns a string containing the usage information for all flags in
// the FlagSet
func (f *FlagSet) FlagUsages() string {
// Splits the string `s` on whitespace into an initial substring up to
// `i` runes in length and the remainder. Will go `slop` over `i` if
// that encompasses the entire string (which allows the caller to
// avoid short orphan words on the final line).
func wrapN(i, slop int, s string) (string, string) {
if i+slop > len(s) {
return s, ""
}

w := strings.LastIndexAny(s[:i], " \t")
if w <= 0 {
return s, ""
}

return s[:w], s[w+1:]
}

// Wraps the string `s` to a maximum width `w` with leading indent
// `i`. The first line is not indented (this is assumed to be done by
// caller). Pass `w` == 0 to do no wrapping
func wrap(i, w int, s string) string {
if w == 0 {
return s
}

// space between indent i and end of line width w into which
// we should wrap the text.
wrap := w - i

var r, l string

// Not enough space for sensible wrapping. Wrap as a block on
// the next line instead.
if wrap < 24 {
i = 16
wrap = w - i
r += "\n" + strings.Repeat(" ", i)
}
// If still not enough space then don't even try to wrap.
if wrap < 24 {
return s
}

// Try to avoid short orphan words on the final line, by
// allowing wrapN to go a bit over if that would fit in the
// remainder of the line.
slop := 5
wrap = wrap - slop

// Handle first line, which is indented by the caller (or the
// special case above)
l, s = wrapN(wrap, slop, s)
r = r + l

// Now wrap the rest
for s != "" {
var t string

t, s = wrapN(wrap, slop, s)
r = r + "\n" + strings.Repeat(" ", i) + t
}

return r

}

// FlagUsagesWrapped returns a string containing the usage information
// for all flags in the FlagSet. Wrapped to `cols` columns (0 for no
// wrapping)
func (f *FlagSet) FlagUsagesWrapped(cols int) string {
x := new(bytes.Buffer)

lines := make([]string, 0, len(f.formal))
Expand Down Expand Up @@ -546,12 +613,19 @@ func (f *FlagSet) FlagUsages() string {
for _, line := range lines {
sidx := strings.Index(line, "\x00")
spacing := strings.Repeat(" ", maxlen-sidx)
fmt.Fprintln(x, line[:sidx], spacing, line[sidx+1:])
// maxlen + 2 comes from + 1 for the \x00 and + 1 for the (deliberate) off-by-one in maxlen-sidx
fmt.Fprintln(x, line[:sidx], spacing, wrap(maxlen+2, cols, line[sidx+1:]))
}

return x.String()
}

// FlagUsages returns a string containing the usage information for all flags in
// the FlagSet
func (f *FlagSet) FlagUsages() string {
return f.FlagUsagesWrapped(0)
}

// PrintDefaults prints to standard error the default values of all defined command-line flags.
func PrintDefaults() {
CommandLine.PrintDefaults()
Expand Down

0 comments on commit 9ff6c69

Please sign in to comment.