Repository: oclif/core
Pull request #896 (2023-12-07 18:28:00)

feat: add flagSortOrder to help options
Adds `flagSortOrder` to `helpOptions`

Two options: 
- `alphabetical`: Sort flags alphabetically. All flags with short characters will come first. (Default behavior)
- `none`: Do not sort flags. They will appear in the order in which they were defined on the command.

To use, add `flagSortOrder` to `helpOptions` in your CLI's package.json
```json
"oclif": {
  "helpOptions": {
    "flagSortOrder": "none"
  }
}
```

Fixes https://github.com/oclif/oclif/issues/1214

@W-14626665@

cristiand391: **QA notes**:

✅ no option defined, defaults to sorted
✅ `oclif.helpOptions.flagSortOrder = none`, unsorted
✅ `oclif.helpOptions.flagSortOrder = none`, sorted

🟡 sort order option isn't validated at runtime:
while in TS only `alphabetical` or `none` is supported, this config is read from json so any typos make it render flags as unsorted.

<img width="1341" alt="Screenshot 2023-12-14 at 12 01 08" src="https://github.com/oclif/core/assets/6853656/87bdb2ab-c5a5-426b-aad5-b238a7978c4a">

do you think it's worth validating this at runtime? not sure it should throw or warn (also don't know if oclif validates any psjon settings).

Patch:
diff --git a/src/help/command.ts b/src/help/command.ts
index 4e2c5f1..b226402 100644
--- a/src/help/command.ts
+++ b/src/help/command.ts
@@ -15,6 +15,17 @@ import {HelpFormatter, HelpSection, HelpSectionRenderer} from './formatter'
 // split on any platform, not just the os specific EOL at runtime.
 const POSSIBLE_LINE_FEED = /\r\n|\n/
 
+/**
+ * Determines the sort order of flags. Will default to alphabetical if not set or set to an invalid value.
+ */
+function determineSortOrder(
+  flagSortOrder: HelpFormatter['opts']['flagSortOrder'],
+): NonNullable<HelpFormatter['opts']['flagSortOrder']> {
+  if (flagSortOrder === 'alphabetical') return 'alphabetical'
+  if (flagSortOrder === 'none') return 'none'
+  return 'alphabetical'
+}
+
 export class CommandHelp extends HelpFormatter {
   constructor(
     public command: Command.Loadable,
@@ -218,15 +229,17 @@ export class CommandHelp extends HelpFormatter {
 
   generate(): string {
     const cmd = this.command
-    const flags = sortBy(
-      Object.entries(cmd.flags || {})
-        .filter(([, v]) => !v.hidden)
-        .map(([k, v]) => {
-          v.name = k
-          return v
-        }),
-      (f) => [!f.char, f.char, f.name],
-    )
+    const unsortedFlags = Object.entries(cmd.flags || {})
+      .filter(([, v]) => !v.hidden)
+      .map(([k, v]) => {
+        v.name = k
+        return v
+      })
+
+    const flags =
+      determineSortOrder(this.opts.flagSortOrder) === 'alphabetical'
+        ? sortBy(unsortedFlags, (f) => [!f.char, f.char, f.name])
+        : unsortedFlags
 
     const args = Object.values(ensureArgObject(cmd.args)).filter((a) => !a.hidden)
     const output = compact(
diff --git a/src/interfaces/help.ts b/src/interfaces/help.ts
index f0ca5c0..74c5102 100644
--- a/src/interfaces/help.ts
+++ b/src/interfaces/help.ts
@@ -4,6 +4,13 @@ export interface HelpOptions {
    * Use docopts as the usage. Defaults to true.
    */
   docopts?: boolean
+  /**
+   * Order in which to sort flags in help output. Defaults to `alphabetical`.
+   *
+   * `alphabetical`: Sort flags alphabetically. All flags with short characters will come first.
+   * `none`: Do not sort flags. They will appear in the order in which they were defined on the command.
+   */
+  flagSortOrder?: 'alphabetical' | 'none'
   /**
    * If true, hide command aliases from the root help output. Defaults to false.
    */


Test patch:
diff --git a/test/help/format-command.test.ts b/test/help/format-command.test.ts
index 730dea9..ed035e1 100644
--- a/test/help/format-command.test.ts
+++ b/test/help/format-command.test.ts
@@ -732,3 +732,81 @@ EXAMPLES
     })
   })
 })
+
+describe('formatCommand with flagSortOrder', () => {
+  let config: Config
+
+  before(async () => {
+    config = await Config.load(process.cwd())
+  })
+
+  it('should not sort flags when set to "none"', async () => {
+    const help = new TestHelp(config, {flagSortOrder: 'none'})
+    const cmd = await makeLoadable(
+      makeCommandClass({
+        id: 'apps:create',
+        flags: {
+          cFlag: flags.string({char: 'c'}),
+          aFlag: flags.string({char: 'a'}),
+          bFlag: flags.string({char: 'b'}),
+        },
+      }),
+    )
+
+    const output = help.formatCommand(cmd)
+    expect(output).to.equal(`USAGE
+  $ oclif apps:create [-c <value>] [-a <value>] [-b <value>]
+
+FLAGS
+  -c, --cFlag=<value>
+  -a, --aFlag=<value>
+  -b, --bFlag=<value>`)
+  })
+
+  it('should sort flags alphabetically when flagSortOrder is not set', async () => {
+    const help = new TestHelp(config)
+    const cmd = await makeLoadable(
+      makeCommandClass({
+        id: 'apps:create',
+        flags: {
+          cFlag: flags.string({char: 'c'}),
+          aFlag: flags.string({char: 'a'}),
+          bFlag: flags.string({char: 'b'}),
+        },
+      }),
+    )
+
+    const output = help.formatCommand(cmd)
+    expect(output).to.equal(`USAGE
+  $ oclif apps:create [-c <value>] [-a <value>] [-b <value>]
+
+FLAGS
+  -a, --aFlag=<value>
+  -b, --bFlag=<value>
+  -c, --cFlag=<value>`)
+  })
+
+  it('should sort flags alphabetically when flagSortOrder is invalid', async () => {
+    // @ts-expect-error because we're testing an invalid flagSortOrder
+    const help = new TestHelp(config, {flagSortOrder: 'INVALID'})
+    const cmd = await makeLoadable(
+      makeCommandClass({
+        id: 'apps:create',
+        flags: {
+          cFlag: flags.string({char: 'c'}),
+          aFlag: flags.string({char: 'a'}),
+          bFlag: flags.string({char: 'b'}),
+        },
+      }),
+    )
+
+    const output = help.formatCommand(cmd)
+    expect(output).to.equal(`USAGE
+  $ oclif apps:create [-c <value>] [-a <value>] [-b <value>]
+
+FLAGS
+  -a, --aFlag=<value>
+  -b, --bFlag=<value>
+  -c, --cFlag=<value>`)
+  })
+})