Repository: oclif/core
Pull request #470 (2022-08-16 15:36:33)

feat: all oclif flag types support custom parsers
before: integer, directory, and file flags allowed you to pass a `parse` function, but ignored that (overwriting with its own parse function)

This change will
1. run the flags' built-in parse function, if any
2. run the passed-in parse function, if any

the `url` flag type did not previously accept a `parse` function, and therefore has no changes


Patch:
diff --git a/src/parser/flags.ts b/src/parser/flags.ts
index 78b5189..840a03e 100644
--- a/src/parser/flags.ts
+++ b/src/parser/flags.ts
@@ -50,7 +50,7 @@ export function integer(opts: Partial<OptionFlag<number>> & {min?: number; max?:
         throw new Error(`Expected an integer greater than or equal to ${opts.min} but received: ${input}`)
       if (opts.max !== undefined && num > opts.max)
         throw new Error(`Expected an integer less than or equal to ${opts.max} but received: ${input}`)
-      return num
+      return opts.parse ? opts.parse(input, 1) : num
     },
   })()
 }
@@ -62,7 +62,14 @@ export function directory(opts?: { exists?: boolean } & Partial<OptionFlag<strin
 export function directory(opts: { exists?: boolean } & Partial<OptionFlag<string>> = {}): OptionFlag<string> | OptionFlag<string[]> | OptionFlag<string | undefined> | OptionFlag<string[] | undefined> {
   return build<string>({
     ...opts,
-    parse: async (input: string) => opts.exists ? dirExists(input) : input,
+    parse: async (input: string) => {
+      if (opts.exists) {
+        // 2nd "context" arg is required but unused
+        return opts.parse ? opts.parse(await dirExists(input), true) : dirExists(input)
+      }
+
+      return opts.parse ? opts.parse(input, true) : input
+    },
   })()
 }
 
@@ -73,7 +80,14 @@ export function file(opts?: { exists?: boolean } & Partial<OptionFlag<string>>):
 export function file(opts: { exists?: boolean } & Partial<OptionFlag<string>> = {}): OptionFlag<string> | OptionFlag<string[]> | OptionFlag<string | undefined> | OptionFlag<string[] | undefined> {
   return build<string>({
     ...opts,
-    parse: async (input: string) => opts.exists ? fileExists(input) : input,
+    parse: async (input: string) => {
+      if (opts.exists) {
+        // 2nd "context" arg is required but unused
+        return opts.parse ? opts.parse(await fileExists(input), true) : fileExists(input)
+      }
+
+      return opts.parse ? opts.parse(input, true) : input
+    },
   })()
 }
 


Test patch:
diff --git a/test/parser/parse.test.ts b/test/parser/parse.test.ts
index 9bf45cd..0f8e813 100644
--- a/test/parser/parse.test.ts
+++ b/test/parser/parse.test.ts
@@ -1,5 +1,5 @@
 /* eslint-disable max-nested-callbacks */
-import {expect} from 'chai'
+import {assert, expect} from 'chai'
 import * as fs from 'fs'
 
 import {flags, parse} from '../../src/parser'
@@ -481,6 +481,32 @@ See more help with --help`)
         })
       })
     })
+
+    describe('custom parse functions', () => {
+      const testIntPass = 6
+      const testIntFail = 7
+      const customParseException = 'NOT_OK'
+      const validateEvenNumberString =  async (input:string) => Number.parseInt(input, 10) % 2 === 0 ? Number.parseInt(input, 10) : assert.fail(customParseException)
+      it('accepts custom parse that passes', async () => {
+        const out = await parse([`--int=${testIntPass}`], {
+          flags: {int: flags.integer({parse: validateEvenNumberString})},
+        })
+        expect(out.flags).to.deep.include({int: testIntPass})
+      })
+
+      it('accepts custom parse that fails', async () => {
+        try {
+          const out = await parse([`--int=${testIntFail}`], {
+            flags: {int: flags.integer({parse: validateEvenNumberString})},
+          })
+          throw new Error(`Should have thrown an error ${JSON.stringify(out)}`)
+        } catch (error_) {
+          const error = error_ as Error
+          expect(error.message).to.equal(
+            customParseException)
+        }
+      })
+    })
   })
 
   it('--no-color', async () => {
@@ -1142,6 +1168,32 @@ See more help with --help`)
             `${testDir} exists but is not a directory`)
         }
       })
+      describe('custom parse functions', () => {
+        const customParseException = 'NOT_OK'
+        it('accepts custom parse that passes', async () => {
+          existsStub.returns(true)
+          statStub.returns({isDirectory: () => true})
+          const out = await parse([`--dir=${testDir}`], {
+            flags: {dir: directory({exists: true, parse: async input => input.includes('some') ? input : assert.fail(customParseException)})},
+          })
+          expect(out.flags).to.deep.include({dir: testDir})
+        })
+
+        it('accepts custom parse that fails', async () => {
+          existsStub.returns(true)
+          statStub.returns({isDirectory: () => true})
+          try {
+            const out = await parse([`--dir=${testDir}`], {
+              flags: {dir: directory({exists: true, parse: async input => input.includes('NOT_THERE') ? input : assert.fail(customParseException)})},
+            })
+            throw new Error(`Should have thrown an error ${JSON.stringify(out)}`)
+          } catch (error_) {
+            const error = error_ as Error
+            expect(error.message).to.equal(
+              customParseException)
+          }
+        })
+      })
     })
 
     describe('file', () => {
@@ -1193,6 +1245,32 @@ See more help with --help`)
           expect(error.message).to.equal(`${testFile} exists but is not a file`)
         }
       })
+      describe('custom parse functions', () => {
+        const customParseException = 'NOT_OK'
+        it('accepts custom parse that passes', async () => {
+          existsStub.returns(true)
+          statStub.returns({isFile: () => true})
+          const out = await parse([`--dir=${testFile}`], {
+            flags: {dir: file({exists: false, parse: async input => input.includes('some') ? input : assert.fail(customParseException)})},
+          })
+          expect(out.flags).to.deep.include({dir: testFile})
+        })
+
+        it('accepts custom parse that fails', async () => {
+          existsStub.returns(true)
+          statStub.returns({isFile: () => true})
+          try {
+            const out = await parse([`--dir=${testFile}`], {
+              flags: {dir: file({exists: true, parse: async input => input.includes('NOT_THERE') ? input : assert.fail(customParseException)})},
+            })
+            throw new Error(`Should have thrown an error ${JSON.stringify(out)}`)
+          } catch (error_) {
+            const error = error_ as Error
+            expect(error.message).to.equal(
+              customParseException)
+          }
+        })
+      })
     })
   })
 })