♻ Split main procedure into smaller functions
This commit is contained in:
parent
7b757e5377
commit
4d689564a4
6 changed files with 174 additions and 132 deletions
15
src/IOption.ts
Normal file
15
src/IOption.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* A command line option
|
||||
*/
|
||||
export default interface IOption {
|
||||
/** The default value to use, if this argument is missing. If set to `undefined`, this argument is considered to be required and the process will terminate whenever it is missing. */
|
||||
default?: string;
|
||||
/** The short version option as an alias for the long option, must be a single character. Can be set to an empty string or `undefined` if there should be no short option. */
|
||||
short?: string;
|
||||
/** A human readable description of this option, for display in the usage message. Not displayed if `undefined` or an empty string. */
|
||||
description?: string;
|
||||
/** A function that verifies if the given value is a valid value for this option. If set to `undefined`, any value is accepted for this option. */
|
||||
verify?: (value: string) => boolean;
|
||||
/** A function returning a custom error message that is shown if the verify function returns false. If the function is `undefined`, or it returns `undefined` or an empty string, a default error message is shown instead. */
|
||||
message?: (value: string) => string;
|
||||
}
|
164
src/index.ts
164
src/index.ts
|
@ -1,20 +1,7 @@
|
|||
import failWithError from './failWithError';
|
||||
|
||||
/**
|
||||
* A command line option
|
||||
*/
|
||||
interface IOption {
|
||||
/** The default value to use, if this argument is missing. If set to `undefined`, this argument is considered to be required and the process will terminate whenever it is missing. */
|
||||
default?: string;
|
||||
/** The short version option as an alias for the long option, must be a single character. Can be set to an empty string or `undefined` if there should be no short option. */
|
||||
short?: string;
|
||||
/** A human readable description of this option, for display in the usage message. Not displayed if `undefined` or an empty string. */
|
||||
description?: string;
|
||||
/** A function that verifies if the given value is a valid value for this option. If set to `undefined`, any value is accepted for this option. */
|
||||
verify?: (value: string) => boolean;
|
||||
/** A function returning a custom error message that is shown if the verify function returns false. If the function is `undefined`, or it returns `undefined` or an empty string, a default error message is shown instead. */
|
||||
message?: (value: string) => string;
|
||||
}
|
||||
import initState from './initState';
|
||||
import IOption from './IOption';
|
||||
import runPreParseHook from './runPreParseHook';
|
||||
import verifyAndStoreOptionValue from './verifyAndStoreOptionValue';
|
||||
|
||||
/**
|
||||
* Checks the command line arguments against the given specification, and returns the arguments as a JavaScript object, as well as a failure function that prints the usage instructions before terminating.
|
||||
|
@ -26,102 +13,15 @@ export default function parseArguments(
|
|||
spec: { [key: string]: IOption },
|
||||
preParseHook?: (args: string[], fail: (message: string) => void) => (string[] | undefined),
|
||||
): { fail: (errorMessage: string) => void, args: { [key: string]: string } } {
|
||||
//The parsed arguments that are returned by this function
|
||||
const outputArgs: { [key: string]: string } = {};
|
||||
|
||||
//Create a lookup table to find a long option when given the short option, and to check if all required options are set
|
||||
const shortToLongLookup: { [key: string]: string } = {};
|
||||
const requiredOptions: { [key: string]: boolean } = {};
|
||||
for (const longOption in spec) {
|
||||
if (spec.hasOwnProperty(longOption)) {
|
||||
if (longOption === '') {
|
||||
failWithError('', `The long option may not be an empty string. This is a bug in the source code of the script that you tried to call.`);
|
||||
}
|
||||
if (!longOption.match(/^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
failWithError('', `The long option "${longOption}" must only contain alpha-numeric characters, optionally separated by hyphens. This is a bug in the source code of the script that you tried to call.`);
|
||||
}
|
||||
|
||||
const shortOption = spec[longOption].short;
|
||||
if (shortOption !== undefined && shortOption !== '') {
|
||||
if (shortOption.length > 1) {
|
||||
failWithError('', `Short options must only be one character long but the short option "${shortOption}" for "${longOption}" was ${shortOption.length} characters long. This is a bug in the source code of the script that you tried to call.`);
|
||||
}
|
||||
shortToLongLookup[shortOption] = longOption;
|
||||
}
|
||||
|
||||
//If a default value is given, use the default value (it can be overridden later), otherwise mark argument as being required
|
||||
const defaultValue = spec[longOption].default;
|
||||
if (defaultValue !== undefined) {
|
||||
outputArgs[longOption] = defaultValue;
|
||||
} else {
|
||||
requiredOptions[longOption] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
|
||||
let argumentOption = '';
|
||||
|
||||
const usage = `./${process.argv[1].substr(process.argv[1].lastIndexOf('/') + 1)} [options]\nOPTIONS\n${
|
||||
Object.entries(spec).map(([key, {default: defaultValue, short, description}]) => (
|
||||
` ${(defaultValue === undefined) ? '' : '['}${(short !== undefined && short !== '') ? `-${short}, ` : ''}--${key} <value>${(defaultValue === undefined) ? '' : `], defaults to "${defaultValue}"`}${(description !== undefined && description !== '') ? `\n ${description}` : ''}`
|
||||
)).join('\n')
|
||||
}`;
|
||||
|
||||
const failFunction = failWithError.bind(null, usage);
|
||||
|
||||
//The original arguments from the command line
|
||||
let args = process.argv.slice(2);
|
||||
|
||||
//If defined, run the pre-parse hook to modify the arguments
|
||||
if (preParseHook !== undefined) {
|
||||
try {
|
||||
const modifiedArgs = preParseHook(args, failFunction);
|
||||
if (modifiedArgs !== undefined) {
|
||||
args = modifiedArgs;
|
||||
}
|
||||
} catch (error) {
|
||||
failFunction(`Could not run pre-parse hook, encountered error: ${String(error)}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseValue(argumentValue: string) {
|
||||
//Verify value for correctness
|
||||
const verifyFunc = spec[argumentOption].verify;
|
||||
const messageFunc = spec[argumentOption].message;
|
||||
try {
|
||||
if (verifyFunc !== undefined && !verifyFunc(argumentValue)) {
|
||||
if (messageFunc !== undefined) {
|
||||
try {
|
||||
const customMessage = messageFunc(argumentValue);
|
||||
if (customMessage !== undefined && customMessage !== '') {
|
||||
failFunction(customMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
failFunction(`Invalid value set for option "${argumentOption}": "${argumentValue}". Could not show custom error message: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
failFunction(`Invalid value set for option "${argumentOption}": "${argumentValue}".`);
|
||||
}
|
||||
} catch (error) {
|
||||
failFunction(`Invalid value set for option "${argumentOption}", the validation of "${argumentValue}" failed: ${String(error)}`);
|
||||
}
|
||||
|
||||
//Remember the value, and if it is a required argument, mark that we have encountered it
|
||||
outputArgs[argumentOption] = argumentValue;
|
||||
if (requiredOptions[argumentOption] === false) {
|
||||
requiredOptions[argumentOption] = true;
|
||||
}
|
||||
argumentOption = '';
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
//Initialize state
|
||||
const { outputArgs, shortToLongLookup, requiredOptions, failFunction } = initState(spec);
|
||||
const args = runPreParseHook({ args: process.argv.slice(2), preParseHook, fail: failFunction });
|
||||
|
||||
//Iterate through all command line arguments. When we have read both name and value, verify the argument for correctness.
|
||||
//Show error if a name has no value afterwards, or a value has no name in front of it.
|
||||
let option = '';
|
||||
for (const arg of args) {
|
||||
if (argumentOption === '') {
|
||||
if (option === '') {
|
||||
/** We expect a name, must be one of:
|
||||
* - `--name value`
|
||||
* - `--name=value`
|
||||
|
@ -129,53 +29,53 @@ export default function parseArguments(
|
|||
* - `-n=value`
|
||||
*/
|
||||
if (arg === '-' || arg === '--') {
|
||||
//TODO: what about the -- option? skip parsing of all following options?
|
||||
failFunction(`Empty option "-" or "--" is not supported.`);
|
||||
} else if (arg.startsWith('--')) { //long argument
|
||||
argumentOption = arg.substr(2);
|
||||
let argumentValue;
|
||||
option = arg.substr(2);
|
||||
let value;
|
||||
//If there's a =, immediately read the value
|
||||
if (argumentOption.indexOf('=') !== -1) {
|
||||
[argumentOption, argumentValue] = argumentOption.split('=', 2);
|
||||
if (option.indexOf('=') !== -1) {
|
||||
[option, value] = option.split('=', 2);
|
||||
}
|
||||
//Check that argument name is valid
|
||||
if (spec[argumentOption] === undefined) {
|
||||
failFunction(`Unknown option "--${argumentOption}".`);
|
||||
if (spec[option] === undefined) {
|
||||
failFunction(`Unknown option "--${option}".`);
|
||||
}
|
||||
//If value was provided, check that value is correct and remove name for next loop iteration
|
||||
if (argumentValue !== undefined) {
|
||||
parseValue(argumentValue);
|
||||
argumentOption = '';
|
||||
if (value !== undefined) {
|
||||
verifyAndStoreOptionValue({option, value, verify: spec[option].verify, message: spec[option].message, fail: failFunction, outputArgs, requiredOptions});
|
||||
option = '';
|
||||
}
|
||||
} else if (arg.startsWith('-')) { //short argument
|
||||
argumentOption = arg.substr(1);
|
||||
let argumentValue;
|
||||
option = arg.substr(1);
|
||||
let value;
|
||||
//If there's a =, immediately read the value
|
||||
if (argumentOption.indexOf('=') !== -1) {
|
||||
[argumentOption, argumentValue] = argumentOption.split('=', 2);
|
||||
if (option.indexOf('=') !== -1) {
|
||||
[option, value] = option.split('=', 2);
|
||||
}
|
||||
//Check that argument name is valid
|
||||
if (shortToLongLookup[argumentOption] === undefined) {
|
||||
failFunction(`Unknown short option "-${argumentOption}".`);
|
||||
if (shortToLongLookup[option] === undefined) {
|
||||
failFunction(`Unknown short option "-${option}".`);
|
||||
}
|
||||
argumentOption = shortToLongLookup[argumentOption];
|
||||
option = shortToLongLookup[option];
|
||||
//If value was provided, check that value is correct and remove name for next loop iteration
|
||||
if (argumentValue !== undefined) {
|
||||
parseValue(argumentValue);
|
||||
argumentOption = '';
|
||||
if (value !== undefined) {
|
||||
verifyAndStoreOptionValue({option, value, verify: spec[option].verify, message: spec[option].message, fail: failFunction, outputArgs, requiredOptions});
|
||||
option = '';
|
||||
}
|
||||
} else {
|
||||
failFunction(`Arguments must be preceded by an option but there was no option in front of "${arg}".`);
|
||||
}
|
||||
} else { //We expect a value, can be anything
|
||||
parseValue(arg);
|
||||
verifyAndStoreOptionValue({option, value: arg, verify: spec[option].verify, message: spec[option].message, fail: failFunction, outputArgs, requiredOptions});
|
||||
option = '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// argumentName must be cleared to '' after the value is read, so if it is not an empty string, the value was missing
|
||||
if (argumentOption !== '') {
|
||||
failFunction(`Option "${argumentOption}" was not followed by a value.`);
|
||||
if (option !== '') {
|
||||
failFunction(`Option "${option}" was not followed by a value.`);
|
||||
}
|
||||
|
||||
//check if any entry in requiredArguments was not set
|
||||
|
|
49
src/initState.ts
Normal file
49
src/initState.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import failWithError from './failWithError';
|
||||
import IOption from './IOption';
|
||||
|
||||
const fail = failWithError.bind(null, '');
|
||||
|
||||
export default function initState(spec: { [key: string]: IOption }) {
|
||||
//The parsed arguments that are returned by this function
|
||||
const outputArgs: { [key: string]: string } = {};
|
||||
|
||||
//Create a lookup table to find a long option when given the short option, and to check if all required options are set
|
||||
const shortToLongLookup: { [key: string]: string } = {};
|
||||
const requiredOptions: { [key: string]: boolean } = {};
|
||||
for (const longOption in spec) {
|
||||
if (spec.hasOwnProperty(longOption)) {
|
||||
if (longOption === '') {
|
||||
fail(`The long option may not be an empty string. This is a bug in the source code of the script that you tried to call.`);
|
||||
}
|
||||
if (!longOption.match(/^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
fail(`The long option "${longOption}" must only contain alpha-numeric characters, optionally separated by hyphens. This is a bug in the source code of the script that you tried to call.`);
|
||||
}
|
||||
|
||||
const shortOption = spec[longOption].short;
|
||||
if (shortOption !== undefined && shortOption !== '') {
|
||||
if (shortOption.length > 1) {
|
||||
fail(`Short options must only be one character long but the short option "${shortOption}" for "${longOption}" was ${shortOption.length} characters long. This is a bug in the source code of the script that you tried to call.`);
|
||||
}
|
||||
shortToLongLookup[shortOption] = longOption;
|
||||
}
|
||||
|
||||
//If a default value is given, use the default value (it can be overridden later), otherwise mark argument as being required
|
||||
const defaultValue = spec[longOption].default;
|
||||
if (defaultValue !== undefined) {
|
||||
outputArgs[longOption] = defaultValue;
|
||||
} else {
|
||||
requiredOptions[longOption] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const usage = `./${process.argv[1].substr(process.argv[1].lastIndexOf('/') + 1)} [options]\nOPTIONS\n${
|
||||
Object.entries(spec).map(([key, {default: defaultValue, short, description}]) => (
|
||||
` ${(defaultValue === undefined) ? '' : '['}${(short !== undefined && short !== '') ? `-${short}, ` : ''}--${key} <value>${(defaultValue === undefined) ? '' : `], defaults to "${defaultValue}"`}${(description !== undefined && description !== '') ? `\n ${description}` : ''}`
|
||||
)).join('\n')
|
||||
}`;
|
||||
|
||||
const failFunction = failWithError.bind(null, usage);
|
||||
|
||||
return { outputArgs, shortToLongLookup, requiredOptions, failFunction };
|
||||
}
|
17
src/runPreParseHook.ts
Normal file
17
src/runPreParseHook.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
export default function runPreParseHook({ args, preParseHook, fail }: {
|
||||
args: string[],
|
||||
preParseHook?: (args: string[], fail: (message: string) => void) => (string[] | undefined),
|
||||
fail: (errorMessage: string) => void,
|
||||
}): string[] {
|
||||
if (preParseHook === undefined) {
|
||||
return args;
|
||||
}
|
||||
|
||||
try {
|
||||
const modifiedArgs = preParseHook(args, fail);
|
||||
return modifiedArgs !== undefined ? modifiedArgs : args;
|
||||
} catch (error) {
|
||||
fail(`Could not run pre-parse hook, encountered error: ${String(error)}.`);
|
||||
return [];
|
||||
}
|
||||
}
|
17
src/tslint.json
Normal file
17
src/tslint.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"comment-format": false,
|
||||
"indent": [true, "spaces", 2],
|
||||
"max-line-length": false,
|
||||
"no-bitwise": false,
|
||||
"no-console": false,
|
||||
"quotemark": [true, "single"],
|
||||
"object-literal-sort-keys": [true, "match-declaration-order"]
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
44
src/verifyAndStoreOptionValue.ts
Normal file
44
src/verifyAndStoreOptionValue.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import IOption from './IOption';
|
||||
|
||||
export default function verifyAndStoreOptionValue({
|
||||
option,
|
||||
value,
|
||||
verify,
|
||||
message,
|
||||
fail,
|
||||
outputArgs,
|
||||
requiredOptions,
|
||||
}: {
|
||||
option: string,
|
||||
value: string,
|
||||
verify: IOption['verify'],
|
||||
message: IOption['message'],
|
||||
fail: (errorMessage: string) => void,
|
||||
outputArgs: { [key: string]: string },
|
||||
requiredOptions: { [key: string]: boolean },
|
||||
}) {
|
||||
//Verify value for correctness
|
||||
try {
|
||||
if (verify !== undefined && !verify(value)) {
|
||||
if (message !== undefined) {
|
||||
try {
|
||||
const customMessage = message(value);
|
||||
if (customMessage !== undefined && customMessage !== '') {
|
||||
fail(customMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(`Invalid value set for option "${option}": "${value}". Could not show custom error message: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
fail(`Invalid value set for option "${option}": "${value}".`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(`Invalid value set for option "${option}", the validation of "${value}" failed: ${String(error)}`);
|
||||
}
|
||||
|
||||
//Remember the value, and if it is a required argument, mark that we have encountered it
|
||||
outputArgs[option] = value;
|
||||
if (requiredOptions[option] === false) {
|
||||
requiredOptions[option] = true;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue