diff --git a/README.md b/README.md index aff6de2..608f13a 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,35 @@ If an option is mandatory, set the `isRequired` property to `true`. For optional If you want to give a dedicated name to the value, you can set it using the `parameterName` property. This sometimes makes sense, to e.g. show off that a parameter is not just a `string`, but a `url` or another domain-specific concept. -Last but not least, you can define whether an option is the default option of a command by setting the `defaultOption` property to `true`. In this case you can skip the option's flag, and just provide its value. +You can also define whether an option is the default option of a command by setting the `defaultOption` property to `true`. In this case you can skip the option's flag, and just provide its value. + +Last but not least, you may specify a `validate` function for an option definition. Inside this function you are free to do whatever you need to do to ensure that the option's given value is valid. However, if you throw an exception from within this function, command-line-interface aborts the command's execution, and shows an error message: + +```javascript +const hello = { + name: 'hello', + description: 'Say hello on the command line.', + + optionDefinitions: [ + { + name: 'name', + description: 'The name to use.', + type: 'string', + alias: 'n', + defaultValue: 'Jane', + validate (value) { + if (value.length > 20) { + throw new Error('The name must be less than 20 characters.'); + } + } + } + ], + + handle ({ options }) { + console.log(`Hello ${options.name}!`); + } +}; +``` ### Implementing sub-commands @@ -210,7 +238,7 @@ await runCli({ // ... }, - optionInvalid ({ optionDefinition }) { + optionInvalid ({ optionDefinition, reason }) { // ... }, diff --git a/lib/Handlers.ts b/lib/Handlers.ts index 9772fe2..1c66551 100644 --- a/lib/Handlers.ts +++ b/lib/Handlers.ts @@ -11,8 +11,9 @@ export interface Handlers { ancestors: string[]; }) => void; - optionInvalid: ({ optionDefinition }: { + optionInvalid: ({ optionDefinition, reason }: { optionDefinition: OptionDefinition; + reason: string; }) => void; optionMissing: ({ optionDefinition }: { diff --git a/lib/elements/OptionDefinition.ts b/lib/elements/OptionDefinition.ts index 916c06c..b1709c4 100644 --- a/lib/elements/OptionDefinition.ts +++ b/lib/elements/OptionDefinition.ts @@ -8,4 +8,5 @@ export interface OptionDefinition { defaultValue?: any; parameterName?: string; isRequired?: boolean; + validate?: (value: any) => void; } diff --git a/lib/runCli.ts b/lib/runCli.ts index 3dd1518..d58230d 100644 --- a/lib/runCli.ts +++ b/lib/runCli.ts @@ -19,9 +19,9 @@ const runCli = async function ({ rootCommand, argv, handlers = {}}: { // eslint-disable-next-line no-console console.error(`Unknown command '${unknownCommandName}'. Did you mean '${recommendedCommandName}'?`); }, - optionInvalid ({ optionDefinition }): void { + optionInvalid ({ reason }): void { // eslint-disable-next-line no-console - console.error(`Option '${optionDefinition.name}' must be a ${optionDefinition.type}.`); + console.error(reason); }, optionMissing ({ optionDefinition }): void { // eslint-disable-next-line no-console diff --git a/lib/runCliRecursive.ts b/lib/runCliRecursive.ts index bb5956f..a33ba73 100644 --- a/lib/runCliRecursive.ts +++ b/lib/runCliRecursive.ts @@ -56,7 +56,7 @@ const runCliRecursive = async function ({ // eslint-disable-next-line unicorn/no-process-exit return process.exit(1); case 'EOPTIONINVALID': { - handlers.optionInvalid({ optionDefinition: ex.data.optionDefinition }); + handlers.optionInvalid({ optionDefinition: ex.data.optionDefinition, reason: ex.message }); // eslint-disable-next-line unicorn/no-process-exit return process.exit(1); diff --git a/lib/validateOptions.ts b/lib/validateOptions.ts index 7493a93..21c32bb 100644 --- a/lib/validateOptions.ts +++ b/lib/validateOptions.ts @@ -37,6 +37,16 @@ const validateOptions = function ({ options, optionDefinitions }: { throw new errors.InvalidOperation(); } } + + if (optionDefinition.validate) { + try { + optionDefinition.validate(value); + } catch (ex) { + throw new errors.OptionInvalid(ex.message, { + data: { optionDefinition } + }); + } + } } }; diff --git a/test/integration/runCliTests.ts b/test/integration/runCliTests.ts index d4425b1..86e72c1 100644 --- a/test/integration/runCliTests.ts +++ b/test/integration/runCliTests.ts @@ -426,6 +426,18 @@ suite('Cli', (): void => { assert.that(stderr).is.containing(`Die Option 'number' muss eine Zahl sein.`); assert.that((process.exit as unknown as SinonStub).calledWith(1)).is.true(); }); + + test('displays validator exception message.', async (): Promise => { + const command: string[] = [ 'number', '--number', '601' ]; + + await runCli({ rootCommand: extendedVariousCli, argv: command }); + + const { stderr, stdout } = stop(); + + assert.that(stdout).is.equalTo(''); + assert.that(stderr).is.containing(`Number must not be bigger than 500.`); + assert.that((process.exit as unknown as SinonStub).calledWith(1)).is.true(); + }); }); suite('various.required command', (): void => { diff --git a/test/shared/examples/various/commands/number.ts b/test/shared/examples/various/commands/number.ts index 56e7f18..d571ff1 100644 --- a/test/shared/examples/various/commands/number.ts +++ b/test/shared/examples/various/commands/number.ts @@ -9,7 +9,13 @@ const number: Command = { name: 'number', type: 'number', alias: 'n', - isRequired: true + description: 'Must not be bigger than 500.', + isRequired: true, + validate (value): void { + if (value > 500) { + throw new Error('Number must not be bigger than 500.'); + } + } } ], handle ({ options }): void {