Repository: oclif/core
Pull request #184 (2021-06-10 04:21:00)

feat: help improvements and customizability
### Command and Flag Summary
Oclif will use the first line of `command.description` as the "summary" which is used as the first line on command help as well as in the command list. This is clever, but can also be hidden and confusing. There is now a `summary` property on command and flag, which if provided, will be used as the first line on command help and in the command list. It will also be repeated in the `DESCRIPTION` section of help for increased usability.

The summary property should be one line.

### Flag Descriptions
Multiline flag descriptions can be hard to read in a wrapped list view. Oclif will render the list different if there is one descriptions is more than 4 lines, but at that point it may be too late. Not only that, it can still be useful to have a short, easy to digest flag list, with more description down below. Now the `summary` property on flag, if provided, will be used in the in the flag list and the description will be put in an extended section down below.

```
FLAGS
  -e, --target-env=<value>...  [default: dsfr] Environment name or alias to open.
  ...

DESCRIPTION
   ...

EXAMPLES
  ...

FLAG DESCRIPTIONS
  -e, --target-env=<value>...

    Specify the login user or alias that’s associated with the environment. For scratch orgs, the login user is generated
    by the command that created the scratch org. You can also set an alias for the scratch org when you create it.

    For Dev Hubs, sandboxes, and production orgs, specify the alias you set when you logged into the org with "sf login".
```

### Flag Help Groups
Help for commands with a large number of flags can be hard to read. It can be hard to pick out related flags. Or it can be hard to pick out flags that may not be directly related to the command, such as flags added by a CLI or a particular plugin. For example, `--json` or [table flags](https://github.com/oclif/cli-ux/#clitable). 

Flag help groups help solve this. 

```
FLAGS
  --[no-]opt  my boolean optional flag

TABLE FLAGS
  -x, --extended          show extra columns
  --columns=columns       only show provided columns (comma-separated)
  --csv                   output is csv format [alias: --output=csv]
  --filter=filter         filter property by partial string matching, ex: name=foo
  --hidden                show hidden commands
  --no-header             hide table header from output
  --no-truncate           do not truncate output to fit screen
  --output=csv|json|yaml  output in a more machine friendly format
  --sort=sort             property to sort by (prepend '-' for descending)

CLI FLAGS
  --json  format output as json
```

### Refactor help and extend customizability
There are time where a CLI may just want to add an extra help section or change a section heading in the command help. Or maybe the CLI wants to override the help options. With oclif@1, a CLI could really only override the `Help` class which did not provide any helpful methods for command help or options. Now, the `CommandHelp` class is loaded from the `Help` class so it can be extended. Also, help options can be defined in `pjson.oclif.helpOptions`. See `test/help/show-customized-help.test.ts`.

In addition, code like `wrap(body, this.opts.maxWidth - spacing, {hard: true, trim: false}` and creating a section with `[bold(header), indent(body, 2)].join('\n')` was called all over the place. These have been abstracted into a base class which all help classes now extend, providing those utilities to custom help classes as well.

### Example Descriptions
Having a list of examples with no descriptions is not very useful, which the existing example structure encourages. Consumers can add descriptions themselves but it is less than idea.

```javascript
Flag.string({
  description: 'my flag',
  examples: [
    'happy path\n<%= config.bin %> <%= command.id %> --flag'
    'run with foo bar\n<%= config.bin %> <%= command.id %> --flag --foo=bar'
  ]
})
```

Now, examples can be an object `{description, command}` which will be formatted appropriately. If a sting is provided, it will try to figure our what is a description and what is a command to format in a more readable output.
```
EXAMPLES
  happy path:

    $ bin command --flag

  run with foo bar:

    $ bin command --flag --foo=bar
```


amphro: 1. You can't have `\n` in markdown since markdown is just text. Just hit enter, or if you want to use `\n` use a js or ts file instead. 
2. I agree. Fixed and added a test. 
3. Suggestions? I changed to "GLOBAL FLAGS" but I'm impartial. 
4. That is exactly how it is done. See how the --json flag 
```
  alias: Flags.string({
      description: messages.getMessage('alias'),
      helpGroup: 'foo',
  }),
  clientid: Flags.string({
      description: messages.getMessage('clientId'),
      helpGroup: 'bar',
  }),
```

Patch:
diff --git a/src/command.ts b/src/command.ts
index 81406fd..54c3de8 100644
--- a/src/command.ts
+++ b/src/command.ts
@@ -30,27 +30,33 @@ process.stdout.on('error', (err: any) => {
 export default abstract class Command {
   static _base = `${pjson.name}@${pjson.version}`
 
-  /** A command ID, used mostly in error or verbose reporting */
+  /** A command ID, used mostly in error or verbose reporting. */
   static id: string
 
-  // to-do: Confirm unused?
-  static title: string | undefined
-
   /**
    * The tweet-sized description for your class, used in a parent-commands
-   * sub-command listing and as the header for the command help
+   * sub-command listing and as the header for the command help.
+   */
+  static summary?: string;
+
+  /**
+   * A full description of how to use the command.
+   *
+   * If no summary, the first line of the description will be used as the summary.
    */
   static description: string | undefined
 
-  /** hide the command from help? */
+  /** Hide the command from help? */
   static hidden: boolean
 
-  /** An override string (or strings) for the default usage documentation */
+  /**
+   * An override string (or strings) for the default usage documentation.
+   */
   static usage: string | string[] | undefined
 
   static help: string | undefined
 
-  /** An array of aliases for this command */
+  /** An array of aliases for this command. */
   static aliases: string[] = []
 
   /** When set to false, allows a variable amount of arguments */
@@ -63,8 +69,21 @@ export default abstract class Command {
 
   static plugin: Interfaces.Plugin | undefined
 
-  /** An array of example strings to show at the end of the command's help */
-  static examples: string[] | undefined
+  /**
+   * An array of examples to show at the end of the command's help.
+   *
+   * IF only a string is provided, it will try to look for a line that starts
+   * with the cmd.bin as the example command and the rest as the description.
+   * If found, the command will be formatted appropriately.
+   *
+   * ```
+   * EXAMPLES:
+   *   A description of a particular use case.
+   *
+   *     $ <%= config.bin => command flags
+   * ```
+   */
+  static examples: Interfaces.Example[]
 
   static parserOptions = {}
 
@@ -96,6 +115,7 @@ export default abstract class Command {
   private static globalFlags = {
     json: Flags.boolean({
       description: 'format output as json',
+      helpGroup: 'GLOBAL',
     }),
   }
 
diff --git a/src/config/config.ts b/src/config/config.ts
index e409358..61fc4d3 100644
--- a/src/config/config.ts
+++ b/src/config/config.ts
@@ -338,7 +338,7 @@ export class Config implements IConfig {
       while (parts.length) {
         const name = parts.join(':')
         if (name && !topics.find(t => t.name === name)) {
-          topics.push({name, description: c.description})
+          topics.push({name, description: c.summary || c.description})
         }
         parts.pop()
       }
@@ -489,10 +489,12 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
         name,
         type: flag.type,
         char: flag.char,
+        summary: flag.summary,
         description: flag.description,
         hidden: flag.hidden,
         required: flag.required,
         helpLabel: flag.helpLabel,
+        helpGroup: flag.helpGroup,
         allowNo: flag.allowNo,
       }
     } else {
@@ -500,11 +502,14 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
         name,
         type: flag.type,
         char: flag.char,
+        summary: flag.summary,
         description: flag.description,
         hidden: flag.hidden,
         required: flag.required,
         helpLabel: flag.helpLabel,
         helpValue: flag.helpValue,
+        helpGroup: flag.helpGroup,
+        multiple: flag.multiple,
         options: flag.options,
         // eslint-disable-next-line no-await-in-loop
         default: typeof flag.default === 'function' ? await flag.default({options: {}, flags: {}}) : flag.default,
@@ -525,6 +530,7 @@ export async function toCached(c: Command.Class, plugin?: IPlugin): Promise<Comm
 
   return {
     id: c.id,
+    summary: c.summary,
     description: c.description,
     usage: c.usage,
     pluginName: plugin && plugin.name,
diff --git a/src/help/command.ts b/src/help/command.ts
index 06e6d99..c893eff 100644
--- a/src/help/command.ts
+++ b/src/help/command.ts
@@ -1,15 +1,18 @@
 import * as Chalk from 'chalk'
-import indent = require('indent-string')
 import stripAnsi = require('strip-ansi')
 
-import {renderList} from './list'
 import {castArray, compact, sortBy} from '../util'
-import {template} from './util'
 import * as Interfaces from '../interfaces'
+import {Example} from '../interfaces/command'
+import {HelpFormatter} from './formatter'
+
+// Don't use os.EOL because we need to ensure that a string
+// written on any platform, that may use \r\n or \n, will be
+// split on any platform, not just the os specific EOL at runtime.
+const POSSIBLE_LINE_FEED = /\r\n|\n/
 
 const {
   underline,
-  bold,
 } = Chalk
 let {
   dim,
@@ -19,13 +22,15 @@ if (process.env.ConEmuANSI === 'ON') {
   dim = Chalk.gray
 }
 
-const wrap = require('wrap-ansi')
-
-export default class CommandHelp {
-  render: (input: string) => string
+export type HelpSection = {header: string; body: string | [string, string | undefined][] | undefined} | undefined;
+export type HelpSectionRenderer = (data: {cmd: Interfaces.Command; flags: Interfaces.Command.Flag[]; args: Interfaces.Command.Arg[]}, header: string) => HelpSection[] | string | undefined;
 
-  constructor(public command: Interfaces.Command, public config: Interfaces.Config, public opts: Interfaces.HelpOptions) {
-    this.render = template(this)
+export class CommandHelp extends HelpFormatter {
+  constructor(
+    public command: Interfaces.Command,
+    public config: Interfaces.Config,
+    public opts: Interfaces.HelpOptions) {
+    super(config, opts)
   }
 
   generate(): string {
@@ -36,28 +41,92 @@ export default class CommandHelp {
       v.name = k
       return v
     }), f => [!f.char, f.char, f.name])
+
     const args = (cmd.args || []).filter(a => !a.hidden)
-    let output = compact([
-      this.usage(flags),
-      this.args(args),
-      this.flags(flags),
-      this.description(),
-      this.aliases(cmd.aliases),
-      this.examples(cmd.examples || (cmd as any).example),
-    ]).join('\n\n')
-    if (this.opts.stripAnsi) output = stripAnsi(output)
+    const output = compact(this.sections().map(({header, generate}) => {
+      const body = generate({cmd, flags, args}, header)
+      // Generate can return a list of sections
+      if (Array.isArray(body)) {
+        return body.map(helpSection => helpSection && helpSection.body && this.section(helpSection.header, helpSection.body)).join('\n\n')
+      }
+      return body && this.section(header, body)
+    })).join('\n\n')
     return output
   }
 
+  protected groupFlags(flags: Interfaces.Command.Flag[]) {
+    const mainFlags: Interfaces.Command.Flag[] = []
+    const flagGroups: {[index: string]: Interfaces.Command.Flag[]} = {}
+
+    for (const flag of flags) {
+      const group = flag.helpGroup
+
+      if (group) {
+        if (!flagGroups[group]) flagGroups[group] = []
+        flagGroups[group].push(flag)
+      } else {
+        mainFlags.push(flag)
+      }
+    }
+    return {mainFlags, flagGroups}
+  }
+
+  protected sections(): Array<{header: string; generate: HelpSectionRenderer}> {
+    return [
+      {
+        header: this.opts.usageHeader || 'USAGE',
+        generate: ({flags}) => this.usage(flags),
+      },
+      {
+        header: 'ARGUMENTS',
+        generate: ({args}, header) => [{header, body: this.args(args)}],
+      },
+      {
+        header: 'FLAGS',
+        generate: ({flags}, header) => {
+          const {mainFlags, flagGroups} = this.groupFlags(flags)
+
+          const flagSections: HelpSection[] = []
+          const mainFlagBody = this.flags(mainFlags)
+
+          if (mainFlagBody) flagSections.push({header, body: mainFlagBody})
+
+          Object.entries(flagGroups).forEach(([name, flags]) => {
+            const body = this.flags(flags)
+            if (body) flagSections.push({header: `${name.toUpperCase()} ${header}`, body})
+          })
+
+          return compact<HelpSection>(flagSections)
+        },
+      },
+      {
+        header: 'DESCRIPTION',
+        generate: () => this.description(),
+      },
+      {
+        header: 'ALIASES',
+        generate: ({cmd}) => this.aliases(cmd.aliases),
+      },
+      {
+        header: 'EXAMPLES',
+        generate: ({cmd}) => {
+          const examples = cmd.examples || (cmd as any).example
+          return this.examples(examples)
+        },
+      },
+      {
+        header: 'FLAG DESCRIPTIONS',
+        generate: ({flags}) => this.flagsDescriptions(flags),
+      },
+    ]
+  }
+
   protected usage(flags: Interfaces.Command.Flag[]): string {
     const usage = this.command.usage
     const body = (usage ? castArray(usage) : [this.defaultUsage(flags)])
     .map(u => `$ ${this.config.bin} ${u}`.trim())
     .join('\n')
-    return [
-      bold('USAGE'),
-      indent(wrap(this.render(body), this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-    ].join('\n')
+    return this.wrap(body)
   }
 
   protected defaultUsage(_: Interfaces.Command.Flag[]): string {
@@ -69,45 +138,88 @@ export default class CommandHelp {
 
   protected description(): string | undefined {
     const cmd = this.command
-    const description = cmd.description && this.render(cmd.description).split('\n').slice(1).join('\n')
-    if (!description) return
-    return [
-      bold('DESCRIPTION'),
-      indent(wrap(description.trim(), this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-    ].join('\n')
+
+    let description: string[] | undefined
+
+    if (this.opts.hideCommandSummaryInDescription) {
+      description = (cmd.description || '').split(POSSIBLE_LINE_FEED).slice(1)
+    } else if (cmd.description) {
+      description = [
+        ...(cmd.summary || '').split(POSSIBLE_LINE_FEED),
+        ...(cmd.description || '').split(POSSIBLE_LINE_FEED),
+      ]
+    }
+    if (description) {
+      // Lines separated with only one newline or more than 2 can be hard to read in the terminal.
+      // Always separate by two newlines.
+      return this.wrap(compact(description).join('\n\n'))
+    }
   }
 
   protected aliases(aliases: string[] | undefined): string | undefined {
     if (!aliases || aliases.length === 0) return
     const body = aliases.map(a => ['$', this.config.bin, a].join(' ')).join('\n')
-    return [
-      bold('ALIASES'),
-      indent(wrap(body, this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-    ].join('\n')
+    return body
   }
 
-  protected examples(examples: string[] | undefined | string): string | undefined {
+  protected examples(examples: Example[] | undefined | string): string | undefined {
     if (!examples || examples.length === 0) return
-    const body = castArray(examples).map(a => this.render(a)).join('\n')
-    return [
-      bold('EXAMPLE' + (examples.length > 1 ? 'S' : '')),
-      indent(wrap(body, this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-    ].join('\n')
+
+    const formatIfCommand = (example: string): string => {
+      example = this.render(example)
+      if (example.startsWith(this.config.bin)) return dim(`$ ${example}`)
+      if (example.startsWith(`$ ${this.config.bin}`)) return dim(example)
+      return example
+    }
+
+    const isCommand = (example: string) => stripAnsi(formatIfCommand(example)).startsWith(`$ ${this.config.bin}`)
+
+    const body = castArray(examples).map(a => {
+      let description
+      let command
+      if (typeof a === 'string') {
+        const lines = a
+        .split(POSSIBLE_LINE_FEED)
+        .filter(line => Boolean(line))
+        // If the example is <description>\n<command> then format correctly
+        if (lines.length === 2 && !isCommand(lines[0]) && isCommand(lines[1])) {
+          description = lines[0]
+          command = lines[1]
+        } else {
+          return lines.map(line => formatIfCommand(line)).join('\n')
+        }
+      } else {
+        description = a.description
+        command = a.command
+      }
+
+      const multilineSeparator =
+        this.config.platform === 'win32' ?
+          this.config.shell.includes('powershell') ? '`' : '^' :
+          '\\'
+
+      // The command will be indented in the section, which is also indented
+      const finalIndentedSpacing = this.indentSpacing * 2
+      // First indent keeping room for escaped newlines
+      const multilineCommand = this.indent(this.wrap(formatIfCommand(command), finalIndentedSpacing + 4))
+      // Then add the escaped newline
+      .split(POSSIBLE_LINE_FEED).join(` ${multilineSeparator}\n  `)
+
+      return `${this.wrap(description, finalIndentedSpacing)}\n\n${multilineCommand}`
+    }).join('\n\n')
+    return body
   }
 
-  protected args(args: Interfaces.Command['args']): string | undefined {
+  protected args(args: Interfaces.Command['args']): [string, string | undefined][] | undefined {
     if (args.filter(a => a.description).length === 0) return
-    const body = renderList(args.map(a => {
+
+    return args.map(a => {
       const name = a.name.toUpperCase()
       let description = a.description || ''
       if (a.default) description = `[default: ${a.default}] ${description}`
       if (a.options) description = `(${a.options.join('|')}) ${description}`
       return [name, description ? dim(description) : undefined]
-    }), {stripAnsi: this.opts.stripAnsi, maxWidth: this.opts.maxWidth - 2})
-    return [
-      bold('ARGUMENTS'),
-      indent(body, 2),
-    ].join('\n')
+    })
   }
 
   protected arg(arg: Interfaces.Command['args'][0]): string {
@@ -116,44 +228,72 @@ export default class CommandHelp {
     return `[${name}]`
   }
 
-  protected flags(flags: Interfaces.Command.Flag[]): string | undefined {
-    if (flags.length === 0) return
-    const body = renderList(flags.map(flag => {
-      let left = flag.helpLabel
-
-      if (!left) {
-        const label = []
-        if (flag.char) label.push(`-${flag.char[0]}`)
-        if (flag.name) {
-          if (flag.type === 'boolean' && flag.allowNo) {
-            label.push(`--[no-]${flag.name.trim()}`)
-          } else {
-            label.push(`--${flag.name.trim()}`)
-          }
+  protected flagHelpLabel(flag: Interfaces.Command.Flag, showOptions = false) {
+    let label = flag.helpLabel
+
+    if (!label) {
+      const labels = []
+      if (flag.char) labels.push(`-${flag.char[0]}`)
+      if (flag.name) {
+        if (flag.type === 'boolean' && flag.allowNo) {
+          labels.push(`--[no-]${flag.name.trim()}`)
+        } else {
+          labels.push(`--${flag.name.trim()}`)
         }
-        left = label.join(', ')
       }
+      label = labels.join(', ')
+    }
 
-      if (flag.type === 'option') {
-        let value = flag.helpValue || flag.name
-        if (!flag.helpValue && flag.options) {
-          value = flag.options.join('|')
-        }
-        if (!value.includes('|')) value = underline(value)
-        left += `=${value}`
+    if (flag.type === 'option') {
+      let value = flag.helpValue || (this.opts.showFlagNameInTitle ? flag.name : '<value>')
+      if (!flag.helpValue && flag.options) {
+        if (showOptions || this.opts.showFlagOptionsInTitle) value = `${flag.options.join('|')}`
+        else value = '<option>'
       }
+      if (flag.multiple) value += '...'
+      if (!value.includes('|')) value = underline(value)
+      label += `=${value}`
+    }
+    return label
+  }
 
-      let right = flag.description || ''
+  protected flags(flags: Interfaces.Command.Flag[]): [string, string | undefined][] | undefined {
+    if (flags.length === 0) return
+
+    return flags.map(flag => {
+      const left = this.flagHelpLabel(flag)
+
+      let right = flag.summary || flag.description || ''
       if (flag.type === 'option' && flag.default) {
         right = `[default: ${flag.default}] ${right}`
       }
       if (flag.required) right = `(required) ${right}`
 
+      if (flag.type === 'option' && flag.options && !flag.helpValue && !this.opts.showFlagOptionsInTitle) {
+        right += `\n<options: ${flag.options.join('|')}>`
+      }
+
       return [left, dim(right.trim())]
-    }), {stripAnsi: this.opts.stripAnsi, maxWidth: this.opts.maxWidth - 2})
-    return [
-      bold('OPTIONS'),
-      indent(body, 2),
-    ].join('\n')
+    })
+  }
+
+  protected flagsDescriptions(flags: Interfaces.Command.Flag[]): string | undefined {
+    const flagsWithExtendedDescriptions = flags.filter(flag => flag.summary && flag.description)
+    if (flagsWithExtendedDescriptions.length === 0) return
+
+    const body = flagsWithExtendedDescriptions.map(flag => {
+      // Guaranteed to be set because of the filter above, but make ts happy
+      const summary = flag.summary || ''
+      let flagHelp = this.flagHelpLabel(flag, true)
+      if (flagHelp.length + summary.length + 2 < this.opts.maxWidth) {
+        flagHelp += '  ' + summary
+      } else {
+        flagHelp += '\n\n' + this.indent(this.wrap(summary, this.indentSpacing * 2))
+      }
+      return `${flagHelp}\n\n${this.indent(this.wrap(flag.description || '', this.indentSpacing * 2))}`
+    }).join('\n\n')
+
+    return body
   }
 }
+export default CommandHelp
diff --git a/src/help/formatter.ts b/src/help/formatter.ts
new file mode 100644
index 0000000..61a200a
--- /dev/null
+++ b/src/help/formatter.ts
@@ -0,0 +1,176 @@
+import * as Chalk from 'chalk'
+import indent = require('indent-string')
+import stripAnsi = require('strip-ansi')
+
+import * as Interfaces from '../interfaces'
+import {stdtermwidth} from '../screen'
+import {template} from './util'
+
+const width = require('string-width')
+const widestLine = require('widest-line')
+
+const wrap = require('wrap-ansi')
+const {
+  bold,
+} = Chalk
+
+export class HelpFormatter {
+  indentSpacing = 2
+
+  /**
+   * Takes a string and replaces `<%= prop =>` with the value of prop, where prop is anything on
+   * `config=Interfaces.Config` or `opts=Interface.HelpOptions`.
+   *
+   * ```javascript
+   * `<%= config.bin =>` // will resolve to the bin defined in `pjson.oclif`.
+   * ```
+   */
+  render: (input: string) => string
+
+  constructor(config: Interfaces.Config, opts: Partial<Interfaces.HelpOptions> = {}) {
+    this.config = config
+    this.opts = {maxWidth: stdtermwidth, ...opts}
+    this.render = template(this)
+  }
+
+  protected config: Interfaces.Config
+
+  protected opts: Interfaces.HelpOptions
+
+  /**
+   * Wrap text according to `opts.maxWidth` which is typically set to the terminal width. All text
+   * will be rendered before bring wrapped, otherwise it could mess up the lengths.
+   *
+   * A terminal will automatically wrap text, so this method is primarily used for indented
+   * text. For indented text, specify the indentation so it is taken into account during wrapping.
+   *
+   * Here is an example of wrapping with indentation.
+   * ```
+   * <------ terminal window width ------>
+   * <---------- no indentation --------->
+   * This is my text that will be wrapped
+   * once it passes maxWidth.
+   *
+   * <- indent -><------ text space ----->
+   *             This is my text that will
+   *             be wrapped once it passes
+   *             maxWidth.
+   *
+   * <-- indent not taken into account ->
+   *             This is my text that will
+   * be wrapped
+   *             once it passes maxWidth.
+   * ```
+   * @param body the text to wrap
+   * @param spacing the indentation size to subtract from the terminal width
+   * @returns the formatted wrapped text
+   */
+  public wrap(body: string, spacing = this.indentSpacing): string {
+    return wrap(this.render(body), this.opts.maxWidth - spacing, {hard: true})
+  }
+
+  /**
+   * Indent by `this.indentSpacing`. The text should be wrap based on terminal width before indented.
+   *
+   * In order to call indent multiple times on the same set or text, the caller must wrap based on
+   * the number of times the text has been indented. For example.
+   *
+   * ```javascript
+   * const body = `main line\n${indent(wrap('indented example line', 4))}`
+   * const header = 'SECTION'
+   * console.log(`${header}\n${indent(wrap(body))}`
+   * ```
+   * will output
+   * ```
+   * SECTION
+   *   main line
+   *     indented example line
+   * ```
+   *
+   * If the terminal width was 24 and the `4` was not provided in the first wrap, it would like like the following.
+   * ```
+   * SECTION
+   *   main line
+   *     indented example
+   *   line
+   * ```
+   * @param body the text to indent
+   * @param spacing the final number of spaces this text will be indented
+   * @return the formatted indented text
+   */
+  public indent(body: string, spacing = this.indentSpacing): string {
+    return indent(body, spacing)
+  }
+
+  public renderList(input: (string | undefined)[][], opts: {indentation: number; multiline?: boolean; stripAnsi?: boolean; spacer?: string}): string {
+    if (input.length === 0) {
+      return ''
+    }
+    const renderMultiline = () => {
+      let output = ''
+      for (let [left, right] of input) {
+        if (!left && !right) continue
+        if (left) {
+          if (opts.stripAnsi) left = stripAnsi(left)
+          output += this.wrap(left.trim(), opts.indentation)
+        }
+        if (right) {
+          if (opts.stripAnsi) right = stripAnsi(right)
+          output += '\n'
+          output += this.indent(this.wrap(right.trim(), opts.indentation + 2), 4)
+        }
+        output += '\n\n'
+      }
+      return output.trim()
+    }
+    if (opts.multiline) return renderMultiline()
+    const maxLength = widestLine(input.map(i => i[0]).join('\n'))
+    let output = ''
+    let spacer = opts.spacer || '\n'
+    let cur = ''
+    for (const [left, r] of input) {
+      let right = r
+      if (cur) {
+        output += spacer
+        output += cur
+      }
+      cur = left || ''
+      if (opts.stripAnsi) cur = stripAnsi(cur)
+      if (!right) {
+        cur = cur.trim()
+        continue
+      }
+      if (opts.stripAnsi) right = stripAnsi(right)
+      right = this.wrap(right.trim(), opts.indentation + maxLength + 2)
+
+      const [first, ...lines] = right!.split('\n').map(s => s.trim())
+      cur += ' '.repeat(maxLength - width(cur) + 2)
+      cur += first
+      if (lines.length === 0) {
+        continue
+      }
+      // if we start putting too many lines down, render in multiline format
+      if (lines.length > 4) return renderMultiline()
+      // if spacer is not defined, separate all rows with extra newline
+      if (!opts.spacer) spacer = '\n\n'
+      cur += '\n'
+      cur += this.indent(lines.join('\n'), maxLength + 2)
+    }
+    if (cur) {
+      output += spacer
+      output += cur
+    }
+    return output.trim()
+  }
+
+  public section(header: string, body: string | [string, string | undefined][]) {
+    // Always render template strings with the provided render function before wrapping and indenting
+    body = Array.isArray(body) ? body.map(([left, right]) => ([this.render(left), right && this.render(right)])) : this.render(body)
+
+    const output = [
+      bold(header),
+      this.indent(Array.isArray(body) ? this.renderList(body, {stripAnsi: this.opts.stripAnsi, indentation: 2}) : body),
+    ].join('\n')
+    return this.opts.stripAnsi ? stripAnsi(output) : output
+  }
+}
diff --git a/src/help/index.ts b/src/help/index.ts
index 768d68d..1d041a9 100644
--- a/src/help/index.ts
+++ b/src/help/index.ts
@@ -1,23 +1,16 @@
-import * as Chalk from 'chalk'
-import indent = require('indent-string')
 import stripAnsi = require('strip-ansi')
 
 import * as Interfaces from '../interfaces'
 import {error} from '../errors'
 import CommandHelp from './command'
-import {renderList} from './list'
 import RootHelp from './root'
-import {stdtermwidth} from '../screen'
 import {compact, sortBy, uniqBy} from '../util'
-import {standardizeIDFromArgv, template} from './util'
+import {standardizeIDFromArgv} from './util'
+import {HelpFormatter} from './formatter'
 
+export {CommandHelp} from './command'
 export {standardizeIDFromArgv, loadHelpClass} from './util'
 
-const wrap = require('wrap-ansi')
-const {
-  bold,
-} = Chalk
-
 function getHelpSubject(args: string[]): string | undefined {
   for (const arg of args) {
     if (arg === '--') return
@@ -27,17 +20,12 @@ function getHelpSubject(args: string[]): string | undefined {
   }
 }
 
-export abstract class HelpBase {
+export abstract class HelpBase extends HelpFormatter {
   constructor(config: Interfaces.Config, opts: Partial<Interfaces.HelpOptions> = {}) {
+    super(config, opts)
     if (!config.topicSeparator) config.topicSeparator = ':' // back-support @oclif/config
-    this.config = config
-    this.opts = {maxWidth: stdtermwidth, ...opts}
   }
 
-  protected config: Interfaces.Config
-
-  protected opts: Interfaces.HelpOptions
-
   /**
    * Show help, used in multi-command CLIs
    * @param args passed into your command, useful for determining which type of help to display
@@ -53,7 +41,7 @@ export abstract class HelpBase {
 }
 
 export class Help extends HelpBase {
-  render: (input: string) => string
+  protected CommandHelpClass: typeof CommandHelp = CommandHelp
 
   /*
    * _topics is to work around Interfaces.topics mistakenly including commands that do
@@ -90,7 +78,6 @@ export class Help extends HelpBase {
 
   constructor(config: Interfaces.Config, opts: Partial<Interfaces.HelpOptions> = {}) {
     super(config, opts)
-    this.render = template(this)
   }
 
   public async showHelp(argv: string[]) {
@@ -127,8 +114,8 @@ export class Help extends HelpBase {
     const subTopics = this.sortedTopics.filter(t => t.name.startsWith(name + ':') && t.name.split(':').length === depth + 1)
     const subCommands = this.sortedCommands.filter(c => c.id.startsWith(name + ':') && c.id.split(':').length === depth + 1)
 
-    const title = command.description && this.render(command.description).split('\n')[0]
-    if (title) console.log(title + '\n')
+    const summary = this.summary(command)
+    if (summary) console.log(summary + '\n')
     console.log(this.formatCommand(command))
     console.log('')
 
@@ -197,47 +184,52 @@ export class Help extends HelpBase {
       command.id = command.id.replace(/:/g, this.config.topicSeparator)
       command.aliases = command.aliases && command.aliases.map(a => a.replace(/:/g, this.config.topicSeparator))
     }
-    const help = new CommandHelp(command, this.config, this.opts)
+    const help = new this.CommandHelpClass(command, this.config, this.opts)
     return help.generate()
   }
 
   protected formatCommands(commands: Interfaces.Command[]): string {
     if (commands.length === 0) return ''
 
-    const body = renderList(commands.map(c => {
+    const body = this.renderList(commands.map(c => {
       if (this.config.topicSeparator !== ':') c.id = c.id.replace(/:/g, this.config.topicSeparator)
       return [
         c.id,
-        c.description && this.render(c.description.split('\n')[0]),
+        this.summary(c),
       ]
     }), {
       spacer: '\n',
       stripAnsi: this.opts.stripAnsi,
-      maxWidth: this.opts.maxWidth - 2,
+      indentation: 2,
     })
 
-    return [
-      bold('COMMANDS'),
-      indent(body, 2),
-    ].join('\n')
+    return this.section('COMMANDS', body)
+  }
+
+  protected summary(c: Interfaces.Command): string | undefined {
+    if (c.summary) return this.render(c.summary.split('\n')[0])
+
+    return c.description && this.render(c.description).split('\n')[0]
+  }
+
+  protected description(c: Interfaces.Command): string {
+    const description = this.render(c.description || '')
+    if (c.summary) {
+      return description
+    }
+    return description.split('\n').slice(1).join('\n')
   }
 
   protected formatTopic(topic: Interfaces.Topic): string {
     let description = this.render(topic.description || '')
-    const title = description.split('\n')[0]
+    const summary = description.split('\n')[0]
     description = description.split('\n').slice(1).join('\n')
     let topicID = `${topic.name}:COMMAND`
     if (this.config.topicSeparator !== ':') topicID = topicID.replace(/:/g, this.config.topicSeparator)
     let output = compact([
-      title,
-      [
-        bold('USAGE'),
-        indent(wrap(`$ ${this.config.bin} ${topicID}`, this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-      ].join('\n'),
-      description && ([
-        bold('DESCRIPTION'),
-        indent(wrap(description, this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-      ].join('\n')),
+      summary,
+      this.section(this.opts.usageHeader || 'USAGE', `$ ${this.config.bin} ${topicID}`),
+      description && this.section('DESCRIPTION', this.wrap(description)),
     ]).join('\n\n')
     if (this.opts.stripAnsi) output = stripAnsi(output)
     return output + '\n'
@@ -245,7 +237,7 @@ export class Help extends HelpBase {
 
   protected formatTopics(topics: Interfaces.Topic[]): string {
     if (topics.length === 0) return ''
-    const body = renderList(topics.map(c => {
+    const body = this.renderList(topics.map(c => {
       if (this.config.topicSeparator !== ':') c.name = c.name.replace(/:/g, this.config.topicSeparator)
       return [
         c.name,
@@ -254,12 +246,9 @@ export class Help extends HelpBase {
     }), {
       spacer: '\n',
       stripAnsi: this.opts.stripAnsi,
-      maxWidth: this.opts.maxWidth - 2,
+      indentation: 2,
     })
-    return [
-      bold('TOPICS'),
-      indent(body, 2),
-    ].join('\n')
+    return this.section('TOPICS', body)
   }
 
   /**
diff --git a/src/help/list.ts b/src/help/list.ts
deleted file mode 100644
index dac7908..0000000
--- a/src/help/list.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import indent = require('indent-string')
-import stripAnsi = require('strip-ansi')
-
-const width = require('string-width')
-const wrap = require('wrap-ansi')
-
-const widestLine = require('widest-line')
-
-export function renderList(input: (string | undefined)[][], opts: {maxWidth: number; multiline?: boolean; stripAnsi?: boolean; spacer?: string}): string {
-  if (input.length === 0) {
-    return ''
-  }
-  const renderMultiline = () => {
-    let output = ''
-    for (let [left, right] of input) {
-      if (!left && !right) continue
-      if (left) {
-        if (opts.stripAnsi) left = stripAnsi(left)
-        output += wrap(left.trim(), opts.maxWidth, {hard: true, trim: false})
-      }
-      if (right) {
-        if (opts.stripAnsi) right = stripAnsi(right)
-        output += '\n'
-        output += indent(wrap(right.trim(), opts.maxWidth - 2, {hard: true, trim: false}), 4)
-      }
-      output += '\n\n'
-    }
-    return output.trim()
-  }
-  if (opts.multiline) return renderMultiline()
-  const maxLength = widestLine(input.map(i => i[0]).join('\n'))
-  let output = ''
-  let spacer = opts.spacer || '\n'
-  let cur = ''
-  for (const [left, r] of input) {
-    let right = r
-    if (cur) {
-      output += spacer
-      output += cur
-    }
-    cur = left || ''
-    if (opts.stripAnsi) cur = stripAnsi(cur)
-    if (!right) {
-      cur = cur.trim()
-      continue
-    }
-    if (opts.stripAnsi) right = stripAnsi(right)
-    right = wrap(right.trim(), opts.maxWidth - (maxLength + 2), {hard: true, trim: false})
-    // right = wrap(right.trim(), screen.stdtermwidth - (maxLength + 4), {hard: true, trim: false})
-    const [first, ...lines] = right!.split('\n').map(s => s.trim())
-    cur += ' '.repeat(maxLength - width(cur) + 2)
-    cur += first
-    if (lines.length === 0) {
-      continue
-    }
-    // if we start putting too many lines down, render in multiline format
-    if (lines.length > 4) return renderMultiline()
-    // if spacer is not defined, separate all rows with extra newline
-    if (!opts.spacer) spacer = '\n\n'
-    cur += '\n'
-    cur += indent(lines.join('\n'), maxLength + 2)
-  }
-  if (cur) {
-    output += spacer
-    output += cur
-  }
-  return output.trim()
-}
diff --git a/src/help/root.ts b/src/help/root.ts
index 193cfbe..795a0cf 100644
--- a/src/help/root.ts
+++ b/src/help/root.ts
@@ -1,21 +1,12 @@
-import * as Chalk from 'chalk'
-import indent = require('indent-string')
 import stripAnsi = require('strip-ansi')
 
 import {compact} from '../util'
-import {template} from './util'
 import * as Interfaces from '../interfaces'
+import {HelpFormatter} from './formatter'
 
-const wrap = require('wrap-ansi')
-const {
-  bold,
-} = Chalk
-
-export default class RootHelp {
-  render: (input: string) => string
-
+export default class RootHelp extends HelpFormatter {
   constructor(public config: Interfaces.Config, public opts: Interfaces.HelpOptions) {
-    this.render = template(this)
+    super(config, opts)
   }
 
   root(): string {
@@ -33,10 +24,7 @@ export default class RootHelp {
   }
 
   protected usage(): string {
-    return [
-      bold('USAGE'),
-      indent(wrap(`$ ${this.config.bin} [COMMAND]`, this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-    ].join('\n')
+    return this.section(this.opts.usageHeader || 'USAGE', this.wrap(`$ ${this.config.bin} [COMMAND]`))
   }
 
   protected description(): string | undefined {
@@ -44,16 +32,10 @@ export default class RootHelp {
     description = this.render(description)
     description = description.split('\n').slice(1).join('\n')
     if (!description) return
-    return [
-      bold('DESCRIPTION'),
-      indent(wrap(description, this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-    ].join('\n')
+    return this.section('DESCRIPTION', this.wrap(description))
   }
 
   protected version(): string {
-    return [
-      bold('VERSION'),
-      indent(wrap(this.config.userAgent, this.opts.maxWidth - 2, {trim: false, hard: true}), 2),
-    ].join('\n')
+    return this.section('VERSION', this.wrap(this.config.userAgent))
   }
 }
diff --git a/src/interfaces/command.ts b/src/interfaces/command.ts
index 96b3fcf..db6dc32 100644
--- a/src/interfaces/command.ts
+++ b/src/interfaces/command.ts
@@ -1,14 +1,57 @@
 import {Config, LoadOptions} from './config'
-import {Plugin} from './plugin'
-import {ArgInput, FlagInput} from './parser'
+import {ArgInput, BooleanFlagProps, FlagInput, OptionFlagProps} from './parser'
 
-export interface Command {
+export type Example = string | {
+  description: string;
+  command: string;
+}
+
+export interface CommandProps {
+  /** A command ID, used mostly in error or verbose reporting. */
   id: string;
+
+  /** Hide the command from help? */
   hidden: boolean;
+
+  /** An array of aliases for this command. */
   aliases: string[];
+
+  /**
+   * The tweet-sized description for your class, used in a parent-commands
+   * sub-command listing and as the header for the command help.
+   */
+  summary?: string;
+
+  /**
+   * A full description of how to use the command.
+   *
+   * If no summary, the first line of the description will be used as the summary.
+   */
   description?: string;
+
+  /**
+   * An override string (or strings) for the default usage documentation.
+   */
   usage?: string | string[];
-  examples?: string[];
+
+  /**
+   * An array of examples to show at the end of the command's help.
+   *
+   * IF only a string is provide, it will try to look for a line that starts
+   * with the cmd.bin as the example command and the rest as the description.
+   * If found, the command will be formatted appropriately.
+   *
+   * ```
+   * EXAMPLES:
+   *   A description of a particular use case.
+   *
+   *     $ <%= config.bin => command flags
+   * ```
+   */
+  examples?: Example[];
+}
+
+export interface Command extends CommandProps {
   type?: string;
   pluginName?: string;
   pluginType?: string;
@@ -29,38 +72,17 @@ export namespace Command {
   export type Flag = Flag.Boolean | Flag.Option
 
   export namespace Flag {
-    export interface Boolean {
-      type: 'boolean';
-      name: string;
-      required?: boolean;
-      char?: string;
-      hidden?: boolean;
-      description?: string;
-      helpLabel?: string;
-      allowNo?: boolean;
-    }
-    export interface Option {
-      type: 'option';
-      name: string;
-      required?: boolean;
-      char?: string;
-      hidden?: boolean;
-      description?: string;
-      helpLabel?: string;
-      helpValue?: string;
+    // We can't use "export OptionFlagProps as Option" in the namespace export.
+    // eslint-disable-next-line @typescript-eslint/no-empty-interface
+    export interface Boolean extends BooleanFlagProps {}
+    // eslint-disable-next-line @typescript-eslint/no-empty-interface
+    export interface Option extends OptionFlagProps {
       default?: string;
-      options?: string[];
     }
   }
 
-  export interface Base {
+  export interface Base extends CommandProps {
     _base: string;
-    id: string;
-    hidden: boolean;
-    aliases: string[];
-    description?: string;
-    usage?: string | string[];
-    examples?: string[];
   }
 
   export interface Class extends Base {
diff --git a/src/interfaces/help.ts b/src/interfaces/help.ts
index 07cc5d1..5c07168 100644
--- a/src/interfaces/help.ts
+++ b/src/interfaces/help.ts
@@ -2,4 +2,30 @@ export interface HelpOptions {
   all?: boolean;
   maxWidth: number;
   stripAnsi?: boolean;
+  /**
+   * By default, option values on flags are shown in the flag's description. This is because
+   * long options list ruin the formatting of help. If a CLI knows all commands will not
+   * do this, it can be turned off at a help level using this property. An individual flag
+   * can set this using `flag.helpValue=options.join('|')`.
+   */
+  showFlagOptionsInTitle?: boolean;
+  /**
+   * By default, titles show flag values as `<value>`. Some CLI developers may prefer titles
+   * to show the flag name as the value. i.e. `--myflag=myflag` instead of `--myflag=<value>`.
+   * An individual flag can set this using `flag.helpValue=flag.name`.
+   */
+  showFlagNameInTitle?: boolean;
+  /**
+   * By default, the command summary is show at the top of the help and as the first line in
+   * the command description. Repeating the summary in the command description improves readability
+   * especially for long command help output. If there is no `command.summary`, the first line of
+   * the description is treated as the summary. Some CLIs, especially with very simple commands, may
+   * not want the duplication.
+   */
+  hideCommandSummaryInDescription?: boolean;
+  /**
+   * Use USAGE, but some may want to use USAGE as used in gnu man pages. See help recommendations at
+   * http://www.gnu.org/software/help2man/#--help-recommendations
+   */
+  usageHeader?: string;
 }
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
index cff89b4..0090f09 100644
--- a/src/interfaces/index.ts
+++ b/src/interfaces/index.ts
@@ -1,6 +1,6 @@
 export {AlphabetLowercase, AlphabetUppercase} from './alphabet'
 export {Config, ArchTypes, PlatformTypes, LoadOptions} from './config'
-export {Command} from './command'
+export {Command, Example} from './command'
 export {OclifError, PrettyPrintableError} from './errors'
 export {HelpOptions} from './help'
 export {Hook, HookKeyOrOptions, Hooks} from './hooks'
diff --git a/src/interfaces/parser.ts b/src/interfaces/parser.ts
index d50ddb3..806ed0c 100644
--- a/src/interfaces/parser.ts
+++ b/src/interfaces/parser.ts
@@ -84,13 +84,46 @@ export type DefaultContext<T> = {
 
 export type Default<T> = T | ((context: DefaultContext<T>) => Promise<T>)
 
-export type FlagBase<T, I> = {
+export type FlagProps = {
   name: string;
   char?: AlphabetLowercase | AlphabetUppercase;
+  /**
+   * A short summary of flag usage to show in the flag list.
+   * If not provided, description will be used.
+   */
+  summary?: string;
+  /**
+   * A description of flag usage. If summary is provided, the description
+   * is assumed to be a longer description and will be shown in a separate
+   * section within help.
+   */
   description?: string;
+  /**
+   * The flag label to show in help. Defaults to "[-<char>] --<name>" where -<char> is
+   * only displayed if the char is defined.
+   */
   helpLabel?: string;
+  /**
+   * Shows this flag in a separate list in the help.
+   */
+  helpGroup?: string;
   hidden?: boolean;
   required?: boolean;
+}
+
+export type BooleanFlagProps = FlagProps & {
+  type: 'boolean';
+  allowNo: boolean;
+}
+
+export type OptionFlagProps = FlagProps & {
+  type: 'option';
+  helpValue?: string;
+  options?: string[];
+  multiple: boolean;
+}
+
+export type FlagBase<T, I> = FlagProps & {
   dependsOn?: string[];
   exclusive?: string[];
   exactlyOne?: string[];
@@ -101,22 +134,15 @@ export type FlagBase<T, I> = {
   parse(input: I, context: any): Promise<T>;
 }
 
-export type BooleanFlag<T> = FlagBase<T, boolean> & {
-  type: 'boolean';
-  allowNo: boolean;
+export type BooleanFlag<T> = FlagBase<T, boolean> & BooleanFlagProps & {
   /**
    * specifying a default of false is the same not specifying a default
    */
-  default?: Default<boolean>;
+   default?: Default<boolean>;
 }
-
-export type OptionFlag<T> = FlagBase<T, string> & {
-  type: 'option';
-  helpValue?: string;
+export type OptionFlag<T> = FlagBase<T, string> & OptionFlagProps & {
   default?: Default<T | undefined>;
-  multiple: boolean;
   input: string[];
-  options?: string[];
 }
 
 export type Definition<T> = {
diff --git a/src/interfaces/pjson.ts b/src/interfaces/pjson.ts
index e450418..9ce15e7 100644
--- a/src/interfaces/pjson.ts
+++ b/src/interfaces/pjson.ts
@@ -1,3 +1,5 @@
+import {HelpOptions} from './help'
+
 export interface PJSON {
   [k: string]: any;
   dependencies?: {[name: string]: string};
@@ -12,7 +14,6 @@ export namespace PJSON {
     version: string;
     oclif: PJSON['oclif'] & {
       schema?: number;
-      title?: string;
       description?: string;
       topicSeparator?: ':' | ' ';
       hooks?: { [name: string]: (string | string[]) };
@@ -21,6 +22,7 @@ export namespace PJSON {
       plugins?: string[];
       devPlugins?: string[];
       helpClass?: string;
+      helpOptions?: HelpOptions;
       aliases?: { [name: string]: string | null };
       repositoryPrefix?: string;
       update: {
diff --git a/src/main.ts b/src/main.ts
index ce01e7e..6fa512e 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -53,7 +53,7 @@ export async function run(argv = process.argv.slice(2), options?: Interfaces.Loa
       return true
     })
     const Help = await loadHelpClass(config)
-    const help = new Help(config)
+    const help = new Help(config, config.pjson.helpOptions)
     await help.showHelp(argv)
     return
   }
diff --git a/src/parser/help.ts b/src/parser/help.ts
index a00f301..f2e9a4a 100644
--- a/src/parser/help.ts
+++ b/src/parser/help.ts
@@ -22,7 +22,7 @@ export function flagUsage(flag: Flag<any>, options: FlagUsageOptions = {}): [str
 
   const usage = flag.type === 'option' ? ` ${flag.name.toUpperCase()}` : ''
 
-  let description: string | undefined = flag.description || ''
+  let description: string | undefined = flag.summary || flag.description || ''
   if (options.displayRequired && flag.required) description = `(required) ${description}`
   description = description ? m.chalk.dim(description) : undefined
 


Test patch:
diff --git a/test/command/command.test.ts b/test/command/command.test.ts
index 213a743..9d799a5 100644
--- a/test/command/command.test.ts
+++ b/test/command/command.test.ts
@@ -218,154 +218,6 @@ describe('command', () => {
     })
   })
 
-  // describe('version', () => {
-  //   fancy
-  //   .stdout()
-  //   .add('config', () => Config.load())
-  //   .do(async () => {
-  //     await Command.run(['--version'], root)
-  //   })
-  //   .catch(/EEXIT: 0/)
-  //   .it('shows version', ctx => {
-  //     expect(ctx.stdout).to.equal(`${ctx.config.userAgent}\n`)
-  //   })
-  // })
-
-  //   describe('help', () => {
-  //     fancy
-  //     .stdout()
-  //     .do(() => {
-  //       class CMD extends Command {
-  //         static flags = {help: flags.help()}
-  //       }
-  //       return CMD.run(['--help'], root)
-  //     })
-  //     .catch(/EEXIT: 0/)
-  //     .it('--help', ctx => {
-  //       expect(ctx.stdout).to.equal(`test command
-
-  // USAGE
-  //   $ oclif
-
-  // OPTIONS
-  //   --help  show CLI help
-
-  // `)
-  //     })
-
-  //     fancy
-  //     .stdout()
-  //     .do(async () => {
-  //       class CMD extends Command {}
-  //       await CMD.run(['--help'], root)
-  //     })
-  //     .catch(/EEXIT: 0/)
-  //     .it('--help', ctx => {
-  //       // expect(process.exitCode).to.equal(0)
-  //       expect(ctx.stdout).to.equal(`test command
-
-  // USAGE
-  //   $ oclif
-
-  // `)
-  //     })
-
-  //     fancy
-  //     .stdout()
-  //     .add('config', async () => {
-  //       const config: TestHelpClassConfig = await Config.load()
-  //       config.pjson.oclif.helpClass = 'help-class-does-not-exist'
-  //       return config
-  //     })
-  //     .do(async ({config}) => {
-  //       class CMD extends Command {
-  //         config = config
-  //       }
-  //       await CMD.run(['--help'])
-  //     })
-  //     .catch((error: Error) => expect(error.message).to.contain('Unable to load configured help class "help-class-does-not-exist", failed with message:\n'))
-  //     .it('shows useful error message when configured help class cannot be loaded')
-
-  //     describe('from a help class', () => {
-  //       fancy
-  //       .stdout()
-  //       .add('config', async () => {
-  //         const config: TestHelpClassConfig = await Config.load()
-  //         config.pjson.oclif.helpClass = './test/command/helpers/test-help-in-lib/lib/test-help-plugin'
-  //         return config
-  //       })
-  //       .do(async ({config}) => {
-  //         class CMD extends Command {
-  //           static id = 'test-command-for-help-plugin'
-
-  //           config = config
-  //         }
-  //         await CMD.run(['--help'])
-  //       })
-  //       .catch(/EEXIT: 0/)
-  //       .it('-h via a plugin in lib dir (compiled to js)', ctx => {
-  //         expect(ctx.stdout).to.equal('hello from test-help-plugin #showCommandHelp in the lib folder and in compiled javascript\n')
-  //         expect(ctx.config.showCommandHelpSpy!.getCalls().length).to.equal(1)
-  //         expect(ctx.config.showHelpSpy!.getCalls().length).to.equal(0)
-  //         const [Command, Topics] = ctx.config.showCommandHelpSpy!.firstCall.args
-  //         expect(Command.id).to.deep.equal('test-command-for-help-plugin')
-  //         expect(Topics).to.be.an('array')
-  //       })
-
-  //       fancy
-  //       .stdout()
-  //       .add('config', async () => {
-  //         const config: TestHelpClassConfig = await Config.load()
-  //         config.pjson.oclif.helpClass = './test/command/helpers/test-help-in-src/src/test-help-plugin'
-  //         return config
-  //       })
-  //       .do(async ({config}) => {
-  //         class CMD extends Command {
-  //           static id = 'test-command-for-help-plugin'
-
-  //           config = config
-  //         }
-  //         await CMD.run(['--help'])
-  //       })
-  //       .catch(/EEXIT: 0/)
-  //       .it('-h via a plugin in src dir (source in ts)', ctx => {
-  //         expect(ctx.stdout).to.equal('hello from test-help-plugin #showCommandHelp\n')
-  //         expect(ctx.config.showCommandHelpSpy!.getCalls().length).to.equal(1)
-  //         expect(ctx.config.showHelpSpy!.getCalls().length).to.equal(0)
-  //         const [Command, Topics] = ctx.config.showCommandHelpSpy!.firstCall.args
-  //         expect(Command.id).to.deep.equal('test-command-for-help-plugin')
-  //         expect(Topics).to.be.an('array')
-  //       })
-
-  //       fancy
-  //       .stdout()
-  //       .add('config', async () => {
-  //         const config: TestHelpClassConfig = await Config.load()
-  //         config.pjson.oclif.helpClass = './test/command/helpers/test-help-in-src/src/test-help-plugin'
-  //         return config
-  //       })
-  //       .do(async ({config}) => {
-  //         class CMD extends Command {
-  //           static id = 'test-command-for-help-plugin'
-
-  //           config = config
-
-  //           static flags = {help: flags.help()}
-  //         }
-  //         return CMD.run(['--help'])
-  //       })
-  //       .catch(/EEXIT: 0/)
-  //       .it('--help via a plugin in src dir (source in ts)', ctx => {
-  //         expect(ctx.stdout).to.equal('hello from test-help-plugin #showCommandHelp\n')
-  //         expect(ctx.config.showCommandHelpSpy!.getCalls().length).to.equal(1)
-  //         expect(ctx.config.showHelpSpy!.getCalls().length).to.equal(0)
-  //         const [Command, Topics] = ctx.config.showCommandHelpSpy!.firstCall.args
-  //         expect(Command.id).to.deep.equal('test-command-for-help-plugin')
-  //         expect(Topics).to.be.an('array')
-  //       })
-  //     })
-  //   })
-
   describe('.log()', () => {
     fancy
     .stdout()
diff --git a/test/help/fixtures/fixtures.ts b/test/help/fixtures/fixtures.ts
index f76160a..68236f4 100644
--- a/test/help/fixtures/fixtures.ts
+++ b/test/help/fixtures/fixtures.ts
@@ -6,8 +6,9 @@ import {Topic} from '../../../src/interfaces'
 export class AppsCreate extends Command {
   static id = 'apps:create'
 
-  static description = `Create an app
-  this only shows up in command help under DESCRIPTION`;
+  static summary = 'Create an app'
+
+  static description = 'this only shows up in command help under DESCRIPTION';
 
   static disableJsonFlag = true;
 
@@ -38,8 +39,22 @@ export class AppsDestroy extends Command {
 export class AppsIndex extends Command {
   static id = 'apps'
 
+  static summary = 'List all apps (app index command)';
+
+  static flags: {};
+
+  static args = [];
+
+  async run() {
+    'run'
+  }
+}
+
+export class AppsIndexWithDesc extends Command {
+  static id = 'apps'
+
   static description = `List all apps (app index command)
-  this only shows up in command help under DESCRIPTION`;
+this only shows up in command help under DESCRIPTION`;
 
   static flags: {};
 
diff --git a/test/help/format-command-with-options.test.ts b/test/help/format-command-with-options.test.ts
new file mode 100644
index 0000000..73f38b4
--- /dev/null
+++ b/test/help/format-command-with-options.test.ts
@@ -0,0 +1,440 @@
+import {expect, test as base} from '@oclif/test'
+import stripAnsi = require('strip-ansi')
+
+import {Command as Base, Flags as flags, Interfaces, toCached} from '../../src'
+import {Help, CommandHelp} from '../../src/help'
+
+const g: any = global
+g.oclif.columns = 80
+
+class Command extends Base {
+  static disableJsonFlag = true
+
+  async run() {
+    return null
+  }
+}
+
+// Allow overriding section headers
+class TestCommandHelp extends CommandHelp {
+  protected sections() {
+    const sections = super.sections()
+    const flagSection = sections.find(section => section.header === 'FLAGS')
+    if (flagSection) flagSection.header = 'OPTIONS'
+    return sections
+  }
+}
+
+// extensions to expose method as public for testing
+class TestHelp extends Help {
+  CommandHelpClass = TestCommandHelp
+
+  constructor(config: Interfaces.Config, opts: Partial<Interfaces.HelpOptions> = {}) {
+    super(config, opts)
+    this.opts.showFlagNameInTitle = true
+    this.opts.showFlagOptionsInTitle = true
+    this.opts.hideCommandSummaryInDescription = true
+  }
+
+  public formatCommand(command: Interfaces.Command) {
+    return super.formatCommand(command)
+  }
+}
+
+const test = base
+.loadConfig()
+.add('help', ctx => new TestHelp(ctx.config as any))
+.register('commandHelp', (command?: any) => ({
+  async run(ctx: {help: TestHelp; commandHelp: string; expectation: string}) {
+    const cached = await toCached(command!, {} as any)
+    const help = ctx.help.formatCommand(cached)
+    if (process.env.TEST_OUTPUT === '1') {
+      console.log(help)
+    }
+    ctx.commandHelp = stripAnsi(help).split('\n').map(s => s.trimRight()).join('\n')
+    ctx.expectation = 'has commandHelp'
+  },
+}))
+
+describe('formatCommand', () => {
+  test
+  .commandHelp(class extends Command {
+      static id = 'apps:create'
+
+      static aliases = ['app:init', 'create']
+
+      static description = `first line
+multiline help`
+
+      static args = [{name: 'app_name', description: 'app to use'}]
+
+      static flags = {
+        app: flags.string({char: 'a', hidden: true}),
+        foo: flags.string({char: 'f', description: 'foobar'.repeat(18)}),
+        force: flags.boolean({description: 'force  it '.repeat(15)}),
+        ss: flags.boolean({description: 'newliney\n'.repeat(4)}),
+        remote: flags.string({char: 'r'}),
+        label: flags.string({char: 'l', helpLabel: '-l'}),
+      }
+  })
+  .it('handles multi-line help output', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create [APP_NAME]
+
+ARGUMENTS
+  APP_NAME  app to use
+
+OPTIONS
+  -f, --foo=foo        foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo
+                       barfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar
+
+  -l=label
+
+  -r, --remote=remote
+
+  --force              force  it force  it force  it force  it force  it force
+                       it force  it force  it force  it force  it force  it
+                       force  it force  it force  it force  it
+
+  --ss                 newliney
+                       newliney
+                       newliney
+                       newliney
+
+DESCRIPTION
+  multiline help
+
+ALIASES
+  $ oclif app:init
+  $ oclif create`))
+
+  describe('arg and flag multiline handling', () => {
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static description = 'description of apps:create'
+
+        static aliases = ['app:init', 'create']
+
+        static args = [{name: 'app_name', description: 'app to use'.repeat(35)}]
+
+        static flags = {
+          app: flags.string({char: 'a', hidden: true}),
+          foo: flags.string({char: 'f', description: 'foobar'.repeat(15)}),
+          force: flags.boolean({description: 'force  it '.repeat(15)}),
+          ss: flags.boolean({description: 'newliney\n'.repeat(4)}),
+          remote: flags.string({char: 'r'}),
+        }
+    })
+    .it('show args and flags side by side when their output do not exceed 4 lines ', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create [APP_NAME]
+
+ARGUMENTS
+  APP_NAME
+      app to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
+      useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp
+      to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
+      useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp
+      to useapp to useapp to useapp to useapp to use
+
+OPTIONS
+  -f, --foo=foo        foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo
+                       barfoobarfoobarfoobarfoobarfoobar
+
+  -r, --remote=remote
+
+  --force              force  it force  it force  it force  it force  it force
+                       it force  it force  it force  it force  it force  it
+                       force  it force  it force  it force  it
+
+  --ss                 newliney
+                       newliney
+                       newliney
+                       newliney
+
+ALIASES
+  $ oclif app:init
+  $ oclif create`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static description = 'description of apps:create'
+
+        static aliases = ['app:init', 'create']
+
+        static args = [{name: 'app_name', description: 'app to use'.repeat(35)}]
+
+        static flags = {
+          app: flags.string({char: 'a', hidden: true}),
+          foo: flags.string({char: 'f', description: 'foobar'.repeat(20)}),
+          force: flags.boolean({description: 'force  it '.repeat(29)}),
+          ss: flags.boolean({description: 'newliney\n'.repeat(5)}),
+          remote: flags.string({char: 'r'}),
+        }
+    })
+    .it('shows stacked args and flags when the lines exceed 4', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create [APP_NAME]
+
+ARGUMENTS
+  APP_NAME
+      app to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
+      useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp
+      to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
+      useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp
+      to useapp to useapp to useapp to useapp to use
+
+OPTIONS
+  -f, --foo=foo
+      foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob
+      arfoobarfoobarfoobarfoobarfoobarfoobarfoobar
+
+  -r, --remote=remote
+
+  --force
+      force  it force  it force  it force  it force  it force  it force  it force
+      it force  it force  it force  it force  it force  it force  it force  it
+      force  it force  it force  it force  it force  it force  it force  it force
+      it force  it force  it force  it force  it force  it force  it
+
+  --ss
+      newliney
+      newliney
+      newliney
+      newliney
+      newliney
+
+ALIASES
+  $ oclif app:init
+  $ oclif create`))
+  })
+
+  describe('description', () => {
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static description = 'description of apps:create\nthese values are after and will show up in the command description'
+
+        static aliases = ['app:init', 'create']
+
+        static args = [{name: 'app_name', description: 'app to use'}]
+
+        static flags = {
+          force: flags.boolean({description: 'forces'}),
+        }
+    })
+    .it('outputs command description with values after a \\n newline character', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create [APP_NAME]
+
+ARGUMENTS
+  APP_NAME  app to use
+
+OPTIONS
+  --force  forces
+
+DESCRIPTION
+  these values are after and will show up in the command description
+
+ALIASES
+  $ oclif app:init
+  $ oclif create`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static description = 'root part of the description\nThe <%= config.bin %> CLI has <%= command.id %>'
+    })
+    .it('renders template string from description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create
+
+DESCRIPTION
+  The oclif CLI has apps:create`))
+  })
+
+  describe(('flags'), () => {
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static flags = {
+          myenum: flags.string({
+            options: ['a', 'b', 'c'],
+          }),
+        }
+    })
+    .it('outputs flag enum', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create
+
+OPTIONS
+  --myenum=a|b|c`))
+
+    test
+    .commandHelp(class extends Command {
+      static id = 'apps:create'
+
+      static args = [
+        {name: 'arg1', default: '.'},
+        {name: 'arg2', default: '.', description: 'arg2 desc'},
+        {name: 'arg3', description: 'arg3 desc'},
+      ]
+
+      static flags = {
+        flag1: flags.string({default: '.'}),
+        flag2: flags.string({default: '.', description: 'flag2 desc'}),
+        flag3: flags.string({description: 'flag3 desc'}),
+      }
+    }).it('outputs with default flag options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create [ARG1] [ARG2] [ARG3]
+
+ARGUMENTS
+  ARG1  [default: .]
+  ARG2  [default: .] arg2 desc
+  ARG3  arg3 desc
+
+OPTIONS
+  --flag1=flag1  [default: .]
+  --flag2=flag2  [default: .] flag2 desc
+  --flag3=flag3  flag3 desc`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static flags = {
+          opt: flags.boolean({allowNo: true}),
+        }
+    })
+    .it('outputs with with no options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create
+
+OPTIONS
+  --[no-]opt`))
+  })
+
+  describe('args', () =>  {
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static args = [
+          {name: 'arg1', description: 'Show the options', options: ['option1', 'option2']},
+        ]
+    })
+    .it('outputs with arg options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create [ARG1]
+
+ARGUMENTS
+  ARG1  (option1|option2) Show the options`))
+  })
+
+  describe('usage', () => {
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static usage = '<%= config.bin %> <%= command.id %> usage'
+    })
+    .it('outputs usage with templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif apps:create usage`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static usage = ['<%= config.bin %>', '<%= command.id %> usage']
+    })
+    .it('outputs usage arrays with templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif
+  $ oclif apps:create usage`))
+
+    test
+    .commandHelp(class extends Command {
+      static id = 'apps:create'
+
+      static usage = undefined
+    })
+    .it('defaults usage when not specified', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create`))
+  })
+
+  describe('examples', () => {
+    test
+    .commandHelp(class extends Command {
+        static examples = ['it handles a list of examples', 'more example text']
+    })
+    .it('outputs multiple examples', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif
+
+EXAMPLES
+  it handles a list of examples
+
+  more example text`))
+
+    test
+    .commandHelp(class extends Command {
+        static examples = ['it handles a single example']
+    })
+    .it('outputs a single example', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif
+
+EXAMPLES
+  it handles a single example`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = ['the bin is <%= config.bin %>', 'the command id is <%= command.id %>']
+    })
+    .it('outputs examples using templates', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  the bin is oclif
+
+  the command id is oclif:command`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = ['<%= config.bin %> <%= command.id %> --help']
+    })
+    .it('formats if command', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  $ oclif oclif:command --help`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = ['Prints out help.\n<%= config.bin %> <%= command.id %> --help']
+    })
+    .it('formats if command with description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  Prints out help.
+
+    $ oclif oclif:command --help`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = [{description: 'Prints out help.', command: '<%= config.bin %> <%= command.id %> --help'}]
+    })
+    .it('formats example object', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  Prints out help.
+
+    $ oclif oclif:command --help`))
+  })
+})
diff --git a/test/help/format-command.test.ts b/test/help/format-command.test.ts
index 5bc5d4a..fe2f703 100644
--- a/test/help/format-command.test.ts
+++ b/test/help/format-command.test.ts
@@ -62,26 +62,29 @@ multiline help`
 ARGUMENTS
   APP_NAME  app to use
 
-OPTIONS
-  -f, --foo=foo        foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo
-                       barfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar
+FLAGS
+  -f, --foo=<value>     foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfo
+                        obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar
 
-  -l=label
+  -l=<value>
 
-  -r, --remote=remote
+  -r, --remote=<value>
 
-  --force              force  it force  it force  it force  it force  it force
-                       it force  it force  it force  it force  it force  it
-                       force  it force  it force  it force  it
+  --force               force  it force  it force  it force  it force  it force
+                        it force  it force  it force  it force  it force  it
+                        force  it force  it force  it force  it
 
-  --json               format output as json
+  --ss                  newliney
+                        newliney
+                        newliney
+                        newliney
 
-  --ss                 newliney
-                       newliney
-                       newliney
-                       newliney
+GLOBAL FLAGS
+  --json  format output as json
 
 DESCRIPTION
+  first line
+
   multiline help
 
 ALIASES
@@ -113,27 +116,31 @@ ALIASES
 ARGUMENTS
   APP_NAME
       app to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
-       useapp to useapp to useapp to useapp to useapp to useapp to useapp to
       useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp
-       to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
-      useapp to useapp to useapp to useapp to useapp to use
+      to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
+      useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp
+      to useapp to useapp to useapp to useapp to use
 
-OPTIONS
-  -f, --foo=foo        foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo
-                       barfoobarfoobarfoobarfoobarfoobar
+FLAGS
+  -f, --foo=<value>     foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfo
+                        obarfoobarfoobarfoobarfoobarfoobar
 
-  -r, --remote=remote
+  -r, --remote=<value>
 
-  --force              force  it force  it force  it force  it force  it force
-                       it force  it force  it force  it force  it force  it
-                       force  it force  it force  it force  it
+  --force               force  it force  it force  it force  it force  it force
+                        it force  it force  it force  it force  it force  it
+                        force  it force  it force  it force  it
 
-  --json               format output as json
+  --ss                  newliney
+                        newliney
+                        newliney
+                        newliney
 
-  --ss                 newliney
-                       newliney
-                       newliney
-                       newliney
+GLOBAL FLAGS
+  --json  format output as json
+
+DESCRIPTION
+  description of apps:create
 
 ALIASES
   $ oclif app:init
@@ -163,26 +170,23 @@ ALIASES
 ARGUMENTS
   APP_NAME
       app to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
-       useapp to useapp to useapp to useapp to useapp to useapp to useapp to
       useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp
-       to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
-      useapp to useapp to useapp to useapp to useapp to use
+      to useapp to useapp to useapp to useapp to useapp to useapp to useapp to
+      useapp to useapp to useapp to useapp to useapp to useapp to useapp to useapp
+      to useapp to useapp to useapp to useapp to use
 
-OPTIONS
-  -f, --foo=foo
+FLAGS
+  -f, --foo=<value>
       foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob
       arfoobarfoobarfoobarfoobarfoobarfoobarfoobar
 
-  -r, --remote=remote
+  -r, --remote=<value>
 
   --force
       force  it force  it force  it force  it force  it force  it force  it force
-       it force  it force  it force  it force  it force  it force  it force  it
+      it force  it force  it force  it force  it force  it force  it force  it
       force  it force  it force  it force  it force  it force  it force  it force
-       it force  it force  it force  it force  it force  it force  it
-
-  --json
-      format output as json
+      it force  it force  it force  it force  it force  it force  it
 
   --ss
       newliney
@@ -191,11 +195,44 @@ OPTIONS
       newliney
       newliney
 
+GLOBAL FLAGS
+  --json  format output as json
+
+DESCRIPTION
+  description of apps:create
+
 ALIASES
   $ oclif app:init
   $ oclif create`))
   })
 
+  describe('summary', () => {
+    test
+    .commandHelp(class extends Command {
+      static id = 'test:summary'
+
+      static summary = 'one line summary'
+    })
+    .it('no description header if only a summary', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif test:summary`))
+
+    test
+    .commandHelp(class extends Command {
+      static id = 'test:summary'
+
+      static summary = 'one line summary'
+
+      static description = 'description that is much longer than the summary'
+    })
+    .it('outputs the summary at the top of the help and description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif test:summary
+
+DESCRIPTION
+  one line summary
+
+  description that is much longer than the summary`))
+  })
+
   describe('description', () => {
     test
     .commandHelp(class extends Command {
@@ -217,11 +254,15 @@ ALIASES
 ARGUMENTS
   APP_NAME  app to use
 
-OPTIONS
+FLAGS
   --force  forces
-  --json   format output as json
+
+GLOBAL FLAGS
+  --json  format output as json
 
 DESCRIPTION
+  description of apps:create
+
   these values are after and will show up in the command description
 
 ALIASES
@@ -240,9 +281,30 @@ ALIASES
   $ oclif apps:create
 
 DESCRIPTION
+  root part of the description
+
   The oclif CLI has apps:create`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static description = 'root part of the description\r\nusing both carriage \nreturn and new line'
+
+        static disableJsonFlag = true
+    })
+    .it('splits on carriage return and new lines', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create
+
+DESCRIPTION
+  root part of the description
+
+  using both carriage
+
+  return and new line`))
   })
 
+  const myEnumValues = ['a', 'b', 'c']
   describe(('flags'), () => {
     test
     .commandHelp(class extends Command {
@@ -251,13 +313,36 @@ DESCRIPTION
         static disableJsonFlag = true
 
         static flags = {
-          myenum: flags.string({options: ['a', 'b', 'c']}),
+          myenum: flags.string({
+            description: 'the description',
+            options: myEnumValues,
+          }),
         }
     })
     .it('outputs flag enum', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
   $ oclif apps:create
 
-OPTIONS
+FLAGS
+  --myenum=<option>  the description
+                     <options: a|b|c>`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static disableJsonFlag = true
+
+        static flags = {
+          myenum: flags.string({
+            options: myEnumValues,
+            helpValue: myEnumValues.join('|'),
+          }),
+        }
+    })
+    .it('outputs flag enum with helpValue', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create
+
+FLAGS
   --myenum=a|b|c`))
 
     test
@@ -285,10 +370,10 @@ ARGUMENTS
   ARG2  [default: .] arg2 desc
   ARG3  arg3 desc
 
-OPTIONS
-  --flag1=flag1  [default: .]
-  --flag2=flag2  [default: .] flag2 desc
-  --flag3=flag3  flag3 desc`))
+FLAGS
+  --flag1=<value>  [default: .]
+  --flag2=<value>  [default: .] flag2 desc
+  --flag3=<value>  flag3 desc`))
 
     test
     .commandHelp(class extends Command {
@@ -303,8 +388,89 @@ OPTIONS
     .it('outputs with with no options', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
   $ oclif apps:create
 
-OPTIONS
+FLAGS
   --[no-]opt`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static disableJsonFlag = true
+
+        static flags = {
+          opt: flags.string({
+            summary: 'one line summary',
+            description: 'multiline\ndescription',
+          }),
+        }
+    })
+    .it('outputs flag summary and description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create
+
+FLAGS
+  --opt=<value>  one line summary
+
+FLAG DESCRIPTIONS
+  --opt=<value>  one line summary
+
+    multiline
+    description`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static disableJsonFlag = true
+
+        static flags = {
+          opt: flags.string({
+            summary: 'one line summary',
+            description: 'single line description',
+          }),
+        }
+    })
+    .it('outputs flag summary and single line description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create
+
+FLAGS
+  --opt=<value>  one line summary
+
+FLAG DESCRIPTIONS
+  --opt=<value>  one line summary
+
+    single line description`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'apps:create'
+
+        static disableJsonFlag = true
+
+        static flags = {
+          opt: flags.string({
+            summary: 'one line summary'.repeat(15),
+            description: 'single line description',
+          }),
+        }
+    })
+    .it('outputs long flag summary and single line description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif apps:create
+
+FLAGS
+  --opt=<value>  one line summaryone line summaryone line summaryone line
+                 summaryone line summaryone line summaryone line summaryone line
+                 summaryone line summaryone line summaryone line summaryone line
+                 summaryone line summaryone line summaryone line summary
+
+FLAG DESCRIPTIONS
+  --opt=<value>
+
+    one line summaryone line summaryone line summaryone line summaryone line
+    summaryone line summaryone line summaryone line summaryone line summaryone
+    line summaryone line summaryone line summaryone line summaryone line
+    summaryone line summary
+
+    single line description`))
   })
 
   describe('args', () =>  {
@@ -371,6 +537,7 @@ ARGUMENTS
 
 EXAMPLES
   it handles a list of examples
+
   more example text`))
 
     test
@@ -382,7 +549,7 @@ EXAMPLES
     .it('outputs a single example', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
   $ oclif
 
-EXAMPLE
+EXAMPLES
   it handles a single example`))
 
     test
@@ -398,6 +565,84 @@ EXAMPLE
 
 EXAMPLES
   the bin is oclif
+
   the command id is oclif:command`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = ['<%= config.bin %> <%= command.id %> --help']
+    })
+    .it('formats if command', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  $ oclif oclif:command --help`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = ['Prints out help.\n<%= config.bin %> <%= command.id %> --help']
+    })
+    .it('formats if command with description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  Prints out help.
+
+    $ oclif oclif:command --help`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = [{description: 'Prints out help.', command: '<%= config.bin %> <%= command.id %> --help'}]
+    })
+    .it('formats example object', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  Prints out help.
+
+    $ oclif oclif:command --help`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = [{description: 'force  it '.repeat(15), command: '<%= config.bin %> <%= command.id %> --help'}]
+    })
+    .it('formats example object with long description', (ctx: any) => expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  force  it force  it force  it force  it force  it force  it force  it force
+  it force  it force  it force  it force  it force  it force  it force  it
+
+    $ oclif oclif:command --help`))
+
+    test
+    .commandHelp(class extends Command {
+        static id = 'oclif:command'
+
+        static examples = [{description: 'Prints out help.', command: '<%= config.bin %> <%= command.id %> ' + 'force  it '.repeat(15)}]
+    })
+    .it('formats example object with long command', (ctx: any) => {
+      const multilineSeparator =
+        ctx.config.platform === 'win32' ?
+          ctx.config.shell.includes('powershell') ? '`' : '^' :
+          '\\'
+      expect(ctx.commandHelp).to.equal(`USAGE
+  $ oclif oclif:command
+
+EXAMPLES
+  Prints out help.
+
+    $ oclif oclif:command force  it force  it force  it force  it force  it ${multilineSeparator}
+      force  it force  it force  it force  it force  it force  it force  it ${multilineSeparator}
+      force  it force  it force  it`)
+    })
   })
 })
diff --git a/test/help/show-customized-help.test.ts b/test/help/show-customized-help.test.ts
new file mode 100644
index 0000000..c9fe0f7
--- /dev/null
+++ b/test/help/show-customized-help.test.ts
@@ -0,0 +1,208 @@
+import {expect, test as base} from '@oclif/test'
+import {stub, SinonStub} from 'sinon'
+import * as path from 'path'
+
+import {CommandHelp, Help} from '../../src/help'
+import {AppsIndexWithDesc, AppsDestroy, AppsCreate, AppsTopic, AppsAdminTopic, AppsAdminAdd} from './fixtures/fixtures'
+import {Interfaces, Config} from '../../src'
+
+const g: any = global
+g.oclif.columns = 80
+
+// Allow overriding section headers
+class TestCommandHelp extends CommandHelp {
+  protected sections() {
+    const sectionsToRemove = ['DESCRIPTION']
+    let sections = super.sections()
+
+    sections = sections.filter(section => !sectionsToRemove.includes(section.header))
+
+    sections.push({
+      header: 'CUSTOM',
+      generate: () => `my custom section
+
+${this.indent(this.wrap('force  it '.repeat(29)))}`,
+    })
+    return sections
+  }
+}
+
+class TestHelp extends Help {
+  CommandHelpClass = TestCommandHelp
+
+  public config: any;
+
+  constructor(config: Interfaces.Config, opts: Partial<Interfaces.HelpOptions> = {}) {
+    super(config, opts)
+    this.opts.usageHeader = 'SYNOPSIS'
+  }
+
+  summary(c: Interfaces.Command): string {
+    // This will essentially ignore the summary
+    return this.wrap(c.description || '')
+  }
+
+  public async showRootHelp() {
+    return super.showRootHelp()
+  }
+
+  public async showTopicHelp(topic: Interfaces.Topic) {
+    return super.showTopicHelp(topic)
+  }
+}
+
+const test = base
+.register('setupHelp', () => ({
+  async run(ctx: { help: TestHelp; stubs: { [k: string]: SinonStub }}) {
+    ctx.stubs = {
+      showRootHelp: stub(TestHelp.prototype, 'showRootHelp').resolves(),
+      showTopicHelp: stub(TestHelp.prototype, 'showTopicHelp').resolves(),
+      showCommandHelp: stub(TestHelp.prototype, 'showCommandHelp').resolves(),
+    }
+
+    // use devPlugins: true to bring in plugins-plugin with topic commands for testing
+    const config = await Config.load({devPlugins: true, root: path.resolve(__dirname, '..')})
+    ctx.help = new TestHelp(config)
+  },
+  finally(ctx) {
+    Object.values(ctx.stubs).forEach(stub  => stub.restore())
+  },
+}))
+.register('makeTopicsWithoutCommand', () => ({
+  async run(ctx: {help: TestHelp; makeTopicOnlyStub: SinonStub}) {
+    // by returning no matching command for a subject, it becomes a topic only
+    // with no corresponding command (in which case the showCommandHelp is shown)
+    ctx.makeTopicOnlyStub = stub(ctx.help.config, 'findCommand').returns(undefined)
+  },
+  finally(ctx) {
+    ctx.makeTopicOnlyStub.restore()
+  },
+}))
+
+describe('showHelp for root', () => {
+  test
+  .loadConfig()
+  .stdout()
+  .do(async ctx => {
+    const config = ctx.config;
+
+    (config as any).plugins = [{
+      commands: [AppsIndexWithDesc, AppsCreate, AppsDestroy],
+      topics: [],
+    }]
+
+    const help = new TestHelp(config as any)
+    await help.showHelp([])
+  })
+  .it('shows a command and topic when the index has siblings', ({stdout, config}) => {
+    expect(stdout.trim()).to.equal(`base library for oclif CLIs
+
+VERSION
+  ${config.userAgent}
+
+SYNOPSIS
+  $ oclif [COMMAND]
+
+TOPICS
+  apps  List all apps (app index command)
+
+COMMANDS
+  apps  List all apps (app index command)
+        this only shows up in command help under DESCRIPTION`)
+  })
+
+  test
+  .loadConfig()
+  .stdout()
+  .do(async ctx => {
+    const config = ctx.config;
+
+    (config as any).plugins = [{
+      commands: [AppsIndexWithDesc],
+      topics: [],
+    }]
+
+    const help = new TestHelp(config as any)
+    await help.showHelp([])
+  })
+  .it('shows a command only when the topic only contains an index', ({stdout, config}) => {
+    expect(stdout.trim()).to.equal(`base library for oclif CLIs
+
+VERSION
+  ${config.userAgent}
+
+SYNOPSIS
+  $ oclif [COMMAND]
+
+COMMANDS
+  apps  List all apps (app index command)
+        this only shows up in command help under DESCRIPTION`)
+  })
+})
+
+describe('showHelp for a command', () => {
+  test
+  .loadConfig()
+  .stdout()
+  .do(async ctx => {
+    const config = ctx.config;
+
+    (config as any).plugins = [{
+      commands: [AppsCreate],
+      topics: [AppsTopic],
+    }]
+
+    const help = new TestHelp(config as any)
+    await help.showHelp(['apps:create'])
+  })
+  .it('shows help for a leaf (or childless) command', ({stdout}) => {
+    expect(stdout.trim()).to.equal(`this only shows up in command help under DESCRIPTION
+
+SYNOPSIS
+  $ oclif apps:create
+
+CUSTOM
+  my custom section
+
+    force  it force  it force  it force  it force  it force  it force  it force
+    it force  it force  it force  it force  it force  it force  it force  it force
+    it force  it force  it force  it force  it force  it force  it force  it force
+    it force  it force  it force  it force  it force  it`)
+  })
+
+  test
+  .loadConfig()
+  .stdout()
+  .do(async ctx => {
+    const config = ctx.config;
+
+    (config as any).plugins = [{
+      commands: [AppsIndexWithDesc, AppsCreate, AppsAdminAdd],
+      topics: [AppsTopic, AppsAdminTopic],
+    }]
+
+    const help = new TestHelp(config as any)
+    await help.showHelp(['apps'])
+  })
+  .it('shows help for a command that has children topics and commands', ({stdout}) => {
+    expect(stdout.trim()).to.equal(`List all apps (app index command)
+this only shows up in command help under DESCRIPTION
+
+SYNOPSIS
+  $ oclif apps
+
+CUSTOM
+  my custom section
+
+    force  it force  it force  it force  it force  it force  it force  it force
+    it force  it force  it force  it force  it force  it force  it force  it force
+    it force  it force  it force  it force  it force  it force  it force  it force
+    it force  it force  it force  it force  it force  it
+
+TOPICS
+  apps:admin  This topic is for the apps topic
+
+COMMANDS
+  apps:create  this only shows up in command help under DESCRIPTION`)
+  })
+})
diff --git a/test/help/show-help.test.ts b/test/help/show-help.test.ts
index 757f298..1d07b65 100644
--- a/test/help/show-help.test.ts
+++ b/test/help/show-help.test.ts
@@ -54,8 +54,8 @@ describe('showHelp for root', () => {
   test
   .loadConfig()
   .stdout()
-  .do(async ctx => {
-    const config = ctx.config;
+  .do(async () => {
+    const config = await Config.load({root: path.resolve(__dirname, '..')});
 
     (config as any).plugins = [{
       commands: [AppsIndex, AppsCreate, AppsDestroy],
@@ -243,6 +243,8 @@ USAGE
   $ oclif apps:create
 
 DESCRIPTION
+  Create an app
+
   this only shows up in command help under DESCRIPTION`)
   })
 
@@ -266,9 +268,6 @@ DESCRIPTION
 USAGE
   $ oclif apps
 
-DESCRIPTION
-  this only shows up in command help under DESCRIPTION
-
 TOPICS
   apps:admin  This topic is for the apps topic
 
diff --git a/test/parser/help.test.ts b/test/parser/help.test.ts
index a621a48..671e091 100644
--- a/test/parser/help.test.ts
+++ b/test/parser/help.test.ts
@@ -1,4 +1,5 @@
 import {expect} from 'chai'
+import stripAnsi = require('strip-ansi')
 
 import * as flags from '../../src/parser/flags'
 import {flagUsages} from '../../src/parser/help'
@@ -13,7 +14,7 @@ describe('flagUsage', () => {
       flags.string({name: 'foo', char: 'f', helpLabel: '-f'}),
       flags.boolean({char: 'g', description: 'goo'}),
     ]
-    expect(flagUsages(f)).to.deep.equal([
+    expect(flagUsages(f).map(([name, desc]) => ([name, desc && stripAnsi(desc)]))).to.deep.equal([
       [' -b, --bar BAR', 'bar'],
       [' -f, --foo FOO', 'desc'],
       [' -f FOO', undefined],