Generates man pages from an ff.Command tree. Each command's flags, subcommands, and help text are extracted automatically. Flag entries include the type placeholder and default value that ff itself computes — --port STRING (default: 8080) appears in the man page exactly as it does in -h output.
go get github.com/StevenACoffman/mango-ffPass your root ff.Command to NewManPage after parsing, then call Build to render the roff output.
A --man flag on the root flag set lets users request the man page at runtime. ff separates parsing from execution — cmd.Parse sets flag values, cmd.Run calls command logic — so checking --man between the two means all flag values are resolved but no command has executed yet. The --man flag itself appears in the generated man page, which is correct: every configurable knob should be documented.
import (
"context"
"fmt"
"os"
mff "github.com/StevenACoffman/mango-ff"
"github.com/muesli/roff"
"github.com/peterbourgon/ff/v4"
)
func main() {
fs := ff.NewFlagSet("myapp")
man := fs.BoolLong("man", "print man page to stdout and exit")
// ... define other flags and subcommands ...
cmd := &ff.Command{
Name: "myapp",
ShortHelp: "does something useful",
Flags: fs,
// Subcommands: []*ff.Command{...},
}
if err := cmd.Parse(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if *man {
manPage, err := mff.NewManPage(1, cmd)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
manPage = manPage.
WithSection("Authors", "Your Name <https://github.com/you/myapp>").
WithSection("Copyright", "Released under MIT license.")
fmt.Print(manPage.Build(roff.NewDocument()))
os.Exit(0)
}
if err := cmd.Run(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}NewManPage takes cmd.ShortHelp as the one-line description and cmd.LongHelp as the long description. If ShortHelp is empty, the first line of LongHelp is used as a fallback.
WriteManPage combines NewManPage and Build in a single call. Use it when you do not need to add custom sections:
if *man {
if err := mff.WriteManPage(1, cmd, os.Stdout, roff.NewDocument()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.Exit(0)
}Use NewManPage when you need to chain WithSection calls on the result.
If you are not using ff.Command — for example, a simple tool with no subcommands — use the visitor functions, which mirror the pattern from mango-pflag:
import (
"fmt"
mff "github.com/StevenACoffman/mango-ff"
"github.com/muesli/mango"
"github.com/muesli/roff"
"github.com/peterbourgon/ff/v4"
)
func main() {
fs := ff.NewFlagSet("mytool")
// ... define flags ...
manPage := mango.NewManPage(1, "mytool", "does something useful").
WithLongDescription("mytool is a demonstration of mango-ff.")
if err := fs.WalkFlags(mff.FFlagVisitor(manPage)); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(manPage.Build(roff.NewDocument()))
}Use PopulateManPage when you want full control over the mango.ManPage — for example, to provide a description that differs from cmd.ShortHelp, or to write the long description by hand:
manPage := mango.NewManPage(1, cmd.Name, "custom one-line description").
WithLongDescription("Extended description written by hand.").
WithSection("Environment", "MYAPP_TOKEN API token (overrides --token)").
WithSection("See Also", "myapp-serve(1), myapp-config(1)")
if err := mff.PopulateManPage(manPage, cmd); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(manPage.Build(roff.NewDocument()))| Function | Description |
|---|---|
NewManPage(section, cmd) |
Build a *mango.ManPage from an ff.Command tree; returns an error if subcommand names collide |
WriteManPage(section, cmd, w, b) |
Build and write to an io.Writer in one call; use when no custom sections are needed |
PopulateManPage(m, cmd) |
Walk an ff.Command tree into a pre-constructed *mango.ManPage |
AddCommand(parent, cmd) |
Recursively add one ff.Command subtree to a mango.Command |
FFlagVisitor(m) |
Visitor for FlagSet.WalkFlags — adds flags to manPage.Root |
FFlagCommandVisitor(c) |
Visitor for FlagSet.WalkFlags — adds flags to a specific mango.Command |
ff computes a type placeholder and a default value for every flag; mango-ff forwards both into the man page entry without any extra work from the caller:
| Flag definition | Man page entry |
|---|---|
fs.Bool('v', "verbose", "log verbose output") |
-v, --verbose |
fs.String('p', "port", "8080", "HTTP port") |
-p, --port STRING (default: 8080) |
fs.String('f', "file", "", "config file path") |
-f, --file STRING |
fs.BoolLongDefault("feature", true, "enable X") |
--feature BOOL (default: true) |
ff suppresses trivial placeholders and defaults: default-false bool flags (Bool, BoolLong, BoolShort) produce neither, and empty string defaults are omitted. A bool flag defined with BoolDefault/BoolLongDefault and a true default is not trivial — it renders with a BOOL placeholder and a (default: true) annotation so readers know the flag is active unless explicitly negated with --feature=false. See the ff documentation for the full flag API.
Each subcommand section in the man page shows only the flags defined at that level, not the ones inherited from parent flag sets. This matches what a user sees when running myapp subcmd -h.
Internally, ff's WalkFlags traverses the full parent chain, so without filtering a subcommand would repeat all root flags. mango-ff filters by flag set identity (f.GetFlags() == ownFlagSet) to show each flag exactly once, in the section where it was defined.
- ff/v4 — flags-first CLI configuration: one registered flag is parsed from CLI args, environment variables, and config files, and described in
-houtput - mango — man page generator for Go
- mango-pflag — mango adapter for pflag
- mango-kong — mango adapter for Kong
- roff — roff document builder used by mango