Repository: oclif/core (ts)

Issue:
Option to customize "<value>" in docopts help usage strings
**Is your feature request related to a problem? Please describe.**
I am working on a CLI that currently has a lot of manually written usage strings (seen in help menus) for commands. I want to remove them to allow Oclif's docopts functionality to autogenerate them, but this would force the removal of helpful information only the manual formatting could contain.

Example: 
```
USAGE
    $ mycli add -i <input-files>...
```
would be autogenerated as
```
USAGE
    $ mycli add -i <value>
```

This "`<value>`" seems to be a hardcoded value, even when changing the flag's `helpValue` property. It also does not include the `...` to indicate multiple inputs are allowed. The [docopts site](http://docopt.org/) uses these notations quite a bit it seems. 

**Describe the solution you'd like**
I would like an optional flag property (separate from the current `helpValue`) that can control this value, which could default to `'<value>'`. It would be used in autogenerating the docopts for the usage strings in the help menus. Perhaps this sort of feature could be extended to positional arguments, but that is not really my main concern. 

I'd appreciate any help or guidance you could provide on this issue. 

Pull request:
feat: added option to specify example flag value in docopts
* feat: added option to specify example flag value in docopts

* feat: combined extra flag property with helpValue

Merge #1091 
Fixes #1088 

External unit tests are expected to fail since plugins have been migrated to v4 beta

Patch:
diff --git a/src/help/command.ts b/src/help/command.ts
index 199acff..a5d0ac9 100644
--- a/src/help/command.ts
+++ b/src/help/command.ts
@@ -166,12 +166,11 @@ export class CommandHelp extends HelpFormatter {
     }
 
     if (flag.type === 'option') {
-      let value = flag.helpValue || (this.opts.showFlagNameInTitle ? flag.name : '<value>')
-      if (!flag.helpValue && flag.options) {
-        value = showOptions || this.opts.showFlagOptionsInTitle ? `${flag.options.join('|')}` : '<option>'
-      }
-
-      if (flag.multiple) value += '...'
+      let value = DocOpts.formatUsageType(
+        flag,
+        this.opts.showFlagNameInTitle ?? false,
+        this.opts.showFlagOptionsInTitle ?? showOptions,
+      )
       if (!value.includes('|')) value = chalk.underline(value)
       label += `=${value}`
     }
diff --git a/src/help/docopts.ts b/src/help/docopts.ts
index 1cd80b4..dcd9ffb 100644
--- a/src/help/docopts.ts
+++ b/src/help/docopts.ts
@@ -72,6 +72,26 @@ export class DocOpts {
       })
   }
 
+  public static formatUsageType(flag: Command.Flag.Any, showFlagName: boolean, showOptions: boolean): string {
+    if (flag.type !== 'option') return ''
+
+    let helpValues: string[]
+    if (flag.helpValue) {
+      // if there is a given helpValue, use it
+      helpValues = typeof flag.helpValue === 'string' ? [flag.helpValue] : flag.helpValue
+    } else if (flag.options) {
+      // if there are options, show them if wanted
+      helpValues = [showOptions ? flag.options.join('|') : '<option>']
+    } else if (showFlagName) {
+      helpValues = [flag.name]
+    } else {
+      // default to <value>
+      helpValues = ['<value>']
+    }
+
+    return helpValues.map((v) => `${v}${flag.multiple ? '...' : ''}`).join(' ')
+  }
+
   public static generate(cmd: Command.Loadable): string {
     return new DocOpts(cmd).toString()
   }
@@ -96,7 +116,7 @@ export class DocOpts {
         ...this.flagList.map((flag) => {
           const name = flag.char ? `-${flag.char}` : `--${flag.name}`
           if (flag.type === 'boolean') return name
-          return `${name}=<value>`
+          return `${name}=${DocOpts.formatUsageType(flag, false, true)}`
         }),
       )
     }
@@ -142,7 +162,7 @@ export class DocOpts {
       // not all flags have short names
       const flagName = flag.char ? `-${flag.char}` : `--${flag.name}`
       if (flag.type === 'option') {
-        type = flag.options ? ` ${flag.options.join('|')}` : ' <value>'
+        type = ` ${DocOpts.formatUsageType(flag, false, true)}`
       }
 
       const element = `${flagName}${type}`
diff --git a/src/interfaces/parser.ts b/src/interfaces/parser.ts
index a1b00ab..92dec3a 100644
--- a/src/interfaces/parser.ts
+++ b/src/interfaces/parser.ts
@@ -206,7 +206,7 @@ export type BooleanFlagProps = FlagProps & {
 
 export type OptionFlagProps = FlagProps & {
   type: 'option'
-  helpValue?: string
+  helpValue?: string | string[]
   options?: readonly string[]
   multiple?: boolean
   /**
@@ -295,10 +295,10 @@ type FlagReturnType<T, R extends ReturnTypeSwitches> = R['requiredOrDefaulted']
       : T[]
     : T
   : R['multiple'] extends true
-  ? [T] extends [Array<unknown>]
-    ? T | undefined
-    : T[] | undefined
-  : T | undefined
+    ? [T] extends [Array<unknown>]
+      ? T | undefined
+      : T[] | undefined
+    : T | undefined
 
 /**
  * FlagDefinition types a function that takes `options` and returns an OptionFlag<T>.


Test patch:
diff --git a/test/help/docopts.test.ts b/test/help/docopts.test.ts
index 47fd2f2..4547490 100644
--- a/test/help/docopts.test.ts
+++ b/test/help/docopts.test.ts
@@ -265,4 +265,41 @@ describe('doc opts', () => {
     } as any)
     expect(usage).to.contain(' (-s <value> -f <value>)')
   })
+
+  it('is uses helpValues as expected', () => {
+    const usage = DocOpts.generate({
+      flags: {
+        testFlag: Flags.url({
+          name: 'testFlag',
+          description: 'test',
+          char: 's',
+          required: true,
+          helpValue: ['<test1>', '<test2>'],
+          multiple: true,
+        }),
+        testFlag2: Flags.string({
+          name: 'testFlag2',
+          description: 'test',
+          char: 'f',
+          required: true,
+          helpValue: '<test3>',
+        }),
+        testFlag3: Flags.string({
+          name: 'testFlag3',
+          description: 'test',
+          char: 'g',
+          required: true,
+          multiple: true,
+        }),
+        testFlag4: Flags.string({
+          name: 'testFlag4',
+          description: 'test',
+          char: 'p',
+          required: true,
+          options: ['option1', 'option2'],
+        }),
+      },
+    } as any)
+    expect(usage).to.contain('-s <test1>... <test2>... -f <test3> -g <value>... -p option1|option2')
+  })
 })
diff --git a/test/help/format-command.test.ts b/test/help/format-command.test.ts
index 5007350..08a088e 100644
--- a/test/help/format-command.test.ts
+++ b/test/help/format-command.test.ts
@@ -339,7 +339,7 @@ FLAGS
                      <options: a|b|c>`)
     })
 
-    it('should output flag enum with helpValue', async () => {
+    it('should output flag enum with string helpValue', async () => {
       const cmd = await makeLoadable(
         makeCommandClass({
           id: 'apps:create',
@@ -360,6 +360,69 @@ FLAGS
   --myenum=a|b|c`)
     })
 
+    it('should output flag enum with array helpValue', async () => {
+      const cmd = await makeLoadable(
+        makeCommandClass({
+          id: 'apps:create',
+          flags: {
+            myenum: flags.string({
+              options: myEnumValues,
+              helpValue: myEnumValues,
+            }),
+          },
+        }),
+      )
+
+      const output = help.formatCommand(cmd)
+      expect(output).to.equal(`USAGE
+  $ oclif apps:create [--myenum a b c]
+
+FLAGS
+  --myenum=a b c`)
+    })
+
+    it('should output string helpValue in usage string', async () => {
+      const cmd = await makeLoadable(
+        makeCommandClass({
+          id: 'apps:create',
+          flags: {
+            files: flags.string({
+              helpValue: '<input-json>|<input-xml>',
+              multiple: true,
+            }),
+          },
+        }),
+      )
+
+      const output = help.formatCommand(cmd)
+      expect(output).to.equal(`USAGE
+  $ oclif apps:create [--files <input-json>|<input-xml>...]
+
+FLAGS
+  --files=<input-json>|<input-xml>...`)
+    })
+
+    it('should output array helpValue in usage string', async () => {
+      const cmd = await makeLoadable(
+        makeCommandClass({
+          id: 'apps:create',
+          flags: {
+            files: flags.string({
+              helpValue: ['<input-json>', '<input-xml>'],
+              multiple: true,
+            }),
+          },
+        }),
+      )
+
+      const output = help.formatCommand(cmd)
+      expect(output).to.equal(`USAGE
+  $ oclif apps:create [--files <input-json>... <input-xml>...]
+
+FLAGS
+  --files=<input-json>... <input-xml>...`)
+    })
+
     it('should output with default flag options', async () => {
       const cmd = await makeLoadable(
         makeCommandClass({