From 0b477cf7559fe9fa1d76b0288e8701146c14fa1c Mon Sep 17 00:00:00 2001 From: C-3PO Date: Tue, 23 Oct 2018 05:02:11 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20argument=20parser,=20remove?= =?UTF-8?q?=20yargs-parser=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- package-lock.json | 34 -------- package.json | 6 +- src/funcs/parseArguments.ts | 162 ++++++++++++++++++++++++++++++++++++ src/getManifest.ts | 20 ++--- src/getPatchZip.ts | 37 ++------ src/getSolidpkg.ts | 32 ++----- src/tslint.json | 3 +- 8 files changed, 190 insertions(+), 110 deletions(-) create mode 100644 src/funcs/parseArguments.ts diff --git a/README.md b/README.md index 599cb34..9264b89 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -This repository contains command-line tools for handling patches. It uses the functions exposed by the [ssn](/swtor/ssn) library and makes them available for use on the shell. +This repository contains command line tools for handling patches. It uses the functions exposed by the [ssn](/swtor/ssn) library and makes them available for use on the shell. # Installation For this tool to work, `tsc` and `tslint` must be globally available, e.g. by running: @@ -24,8 +24,8 @@ complete -W 'assets_swtor_de_de assets_swtor_en_us assets_swtor_fr_fr assets_swt # Usage ```bash -dist/getManifest.js assets_swtor_main -dist/getSolidPkg.js assets_swtor_main 126 127 +dist/getManifest.js --product assets_swtor_main +dist/getSolidPkg.js --product assets_swtor_main --from 126 --to 127 dist/getPatchZip.js --product assets_swtor_main --from 126 --to 127 dist/installPatch.js assets_swtor_main 126 127 swtor_install assets_swtor_main 126 127 diff --git a/package-lock.json b/package-lock.json index 3e86d40..9653b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,31 +9,6 @@ "integrity": "sha512-3TUHC3jsBAB7qVRGxT6lWyYo2v96BMmD2PTcl47H25Lu7UXtFH/2qqmKiVrnel6Ne//0TFYf6uvNX+HW2FRkLQ==", "dev": true }, - "@types/yargs": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.1.tgz", - "integrity": "sha512-UVjo2oH79aRNcsDlFlnQ/iJ67Jd7j6uSg7jUJP/RZ/nUjAh5ElmnwlD5K/6eGgETJUgCHkiWn91B8JjXQ6ubAw==", - "dev": true - }, - "@types/yargs-parser": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-11.0.0.tgz", - "integrity": "sha512-ZfKjiy9zRHQWWRiQLeushSHE2IgHJ8U/OaABsNodkuufZoWED7Plz6egREZiE8dfrTAnrD8Rw/kUh2S6Iu1W0w==", - "dev": true, - "requires": { - "@types/yargs": "*" - } - }, - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -58,15 +33,6 @@ "requires": { "sax": "^1.2.4" } - }, - "yargs-parser": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.0.0.tgz", - "integrity": "sha512-dvsafRjM45h79WOTvS/dP35Sb31SlGAKz6tFjI97kGC4MJFBuzTZY6TTYHrz0QSMQdkyd8Y+RsOMLr+JY0nPFQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } } diff --git a/package.json b/package.json index 59595d6..4444d58 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,9 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "ssn": "git+https://git.jedipedia.net/swtor/ssn.git", - "yargs-parser": "^11.0.0" + "ssn": "git+https://git.jedipedia.net/swtor/ssn.git" }, "devDependencies": { - "@types/node": "^10.12.0", - "@types/yargs-parser": "^11.0.0" + "@types/node": "^10.12.0" } } diff --git a/src/funcs/parseArguments.ts b/src/funcs/parseArguments.ts new file mode 100644 index 0000000..07cf2b1 --- /dev/null +++ b/src/funcs/parseArguments.ts @@ -0,0 +1,162 @@ +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; +} + +/** + * 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. + * If an error is found, outputs an error message and terminates with a non-zero exit code. + * @param spec A specification of what arguments are expected. + */ +export default function parseArguments(spec: { [key: string]: IOption }): { fail: (errorMessage: string) => void, args: { [key: string]: string } } { + //The original arguments from the command line + const args = process.argv.slice(2); + //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 = `node ${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} ${(defaultValue === undefined) ? '' : `], defaults to "${defaultValue}"`}${(description !== undefined && description !== '') ? `\n ${description}` : ''}` + )).join('\n') + }`; + + const failFunction = failWithError.bind(null, usage); + + function parseValue(argumentValue: string) { + //Verify value for correctness + const verifyFunc = spec[argumentOption].verify; + try { + if (verifyFunc !== undefined && !verifyFunc(argumentValue)) { + failFunction(`the argument "${argumentOption}" has an invalid value "${argumentValue}"`); + } + } catch (error) { + failFunction(`Error during verification; the argument "${argumentOption}" has an invalid value "${argumentValue}"`); + } + + //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 = ''; + } + + //--------------------------------------------------------------- + + //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. + for (const arg of args) { + if (argumentOption === '') { + /** We expect a name, must be one of: + * - `--name value` + * - `--name=value` + * - `-n value` + * - `-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; + //If there's a =, immediately read the value + if (argumentOption.indexOf('=') !== -1) { + [argumentOption, argumentValue] = argumentOption.split('=', 2); + } + //Check that argument name is valid + if (spec[argumentOption] === undefined) { + failFunction(`Unknown option "--${argumentOption}".`); + } + //If value was provided, check that value is correct and remove name for next loop iteration + if (argumentValue !== undefined) { + parseValue(argumentValue); + argumentOption = ''; + } + } else if (arg.startsWith('-')) { //short argument + argumentOption = arg.substr(1); + let argumentValue; + //If there's a =, immediately read the value + if (argumentOption.indexOf('=') !== -1) { + [argumentOption, argumentValue] = argumentOption.split('=', 2); + } + //Check that argument name is valid + if (shortToLongLookup[argumentOption] === undefined) { + failFunction(`Unknown short option "-${argumentOption}".`); + } + argumentOption = shortToLongLookup[argumentOption]; + //If value was provided, check that value is correct and remove name for next loop iteration + if (argumentValue !== undefined) { + parseValue(argumentValue); + argumentOption = ''; + } + } 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); + } + + } + + // 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.`); + } + + //check if any entry in requiredArguments was not set + for (const optName in requiredOptions) { + if (spec.hasOwnProperty(optName) && requiredOptions[optName] === false) { + failFunction(`Missing option "${optName}" even though it is required.`); + } + } + + return { + fail: failFunction, + args: outputArgs, + }; +} diff --git a/src/getManifest.ts b/src/getManifest.ts index df944c0..0e31524 100644 --- a/src/getManifest.ts +++ b/src/getManifest.ts @@ -1,23 +1,15 @@ #!/usr/bin/env node import { getManifest, IManifest, Product, verifyProductName } from 'ssn'; -import failWithError from './funcs/failWithError'; +import parseArguments from './funcs/parseArguments'; -const failFunction = failWithError.bind(null, 'node dist/getManifest.js '); - -if (process.argv.length !== 3) { - failFunction(`Expected 1 argument but ${process.argv.length - 2} arguments were supplied.`); -} - -//Check that product name is valid -const product = process.argv[2]; -if (!verifyProductName(product)) { - failFunction(`"${product} is not a valid product name.`); -} +const { args, fail } = parseArguments({ + product: { short: 'p', description: 'Name of the product we want to get the manifest on', verify: verifyProductName }, +}); //Get manifest and write output to stdout -getManifest(product as Product).then((output: IManifest) => { +getManifest(args.product as Product).then((output: IManifest) => { process.stdout.write(JSON.stringify(output)); }).catch((error) => { - failFunction(error); + fail(error); }); diff --git a/src/getPatchZip.ts b/src/getPatchZip.ts index 24225a8..78d191b 100644 --- a/src/getPatchZip.ts +++ b/src/getPatchZip.ts @@ -1,40 +1,17 @@ #!/usr/bin/env node import { getPatchZip, ISsnFileEntry, Product, verifyProductName } from 'ssn'; -import * as yargsParser from 'yargs-parser'; -import failWithError from './funcs/failWithError'; +import parseArguments from './funcs/parseArguments'; -const failFunction = failWithError.bind(null, 'node dist/getPatchZip.js --product --from --to '); - -const args = yargsParser(process.argv.slice(2), { string: ['product', 'from', 'to'] }); - -//Check that product name is valid -if (args.product === undefined) { - failFunction(`product is a required argument but it was not specified.`); -} -if (!verifyProductName(args.product)) { - failFunction(`"${args.product.substring(0, 300)}" is not a valid product name.`); -} - -//Check that from is a valid number -if (args.from === undefined) { - failFunction(`from is a required argument but it was not specified.`); -} -if (!args.from.match(/^(-1|0|[1-9][0-9]{0,2})$/)) { - failFunction(`from value "${args.from.substring(0, 300)}" is not a valid integer; it must be in range [-1, 999].`); -} - -//Check that to is a valid number -if (args.to === undefined) { - failFunction(`to is a required argument but it was not specified.`); -} -if (!args.to.match(/^(0|[1-9][0-9]{0,2})$/)) { - failFunction(`to value "${args.to.substring(0, 300)}" is not a valid integer; it must be in range [0, 999].`); -} +const { args, fail } = parseArguments({ + product: { short: 'p', description: 'Name of the product we want to get the patch info on', verify: verifyProductName }, + from: { short: 'f', description: 'Number of the from release', verify: (str) => str.match(/^(-1|0|[1-9][0-9]{0,2})$/) !== null }, + to: { short: 't', description: 'Number of the to release', verify: (str) => str.match(/^(0|[1-9][0-9]{0,2})$/) !== null }, +}); //Get the .zip file from this patch and write output to stdout getPatchZip(args.product as Product, Number(args.from), Number(args.to)).then((output: ISsnFileEntry[]) => { process.stdout.write(JSON.stringify(output)); }).catch((error) => { - failFunction(error); + fail(error); }); diff --git a/src/getSolidpkg.ts b/src/getSolidpkg.ts index 051a033..3969abf 100644 --- a/src/getSolidpkg.ts +++ b/src/getSolidpkg.ts @@ -1,33 +1,17 @@ #!/usr/bin/env node import { getSolidpkg, ISolidSimple, Product, verifyProductName } from 'ssn'; -import failWithError from './funcs/failWithError'; +import parseArguments from './funcs/parseArguments'; -const failFunction = failWithError.bind(null, 'node dist/getSolidpkg.js '); - -if (process.argv.length !== 5) { - failFunction(`Expected 3 arguments but ${process.argv.length - 2} arguments were supplied.`); -} - -//Check that product name is valid -const product = process.argv[2]; -if (!verifyProductName(product)) { - failFunction(`"${product.substring(0, 300)}" is not a valid product name.`); -} - -//Check that from and to are valid numbers -const from = process.argv[3]; -const to = process.argv[4]; -if (!from.match(/^(-1|0|[1-9][0-9]{0,2})$/)) { - failFunction(`from value "${from.substring(0, 300)}" is not a valid integer; it must be in range [-1, 999].`); -} -if (!to.match(/^(0|[1-9][0-9]{0,2})$/)) { - failFunction(`to value "${to.substring(0, 300)}" is not a valid integer; it must be in range [0, 999].`); -} +const { args, fail } = parseArguments({ + product: { short: 'p', description: 'Name of the product we want to get the solidpkg on', verify: verifyProductName }, + from: { short: 'f', description: 'Number of the from release', verify: (str) => str.match(/^(-1|0|[1-9][0-9]{0,2})$/) !== null }, + to: { short: 't', description: 'Number of the to release', verify: (str) => str.match(/^(0|[1-9][0-9]{0,2})$/) !== null }, +}); //Get solidpkg and write output to stdout -getSolidpkg(product as Product, Number(from), Number(to)).then((output: ISolidSimple) => { +getSolidpkg(args.product as Product, Number(args.from), Number(args.to)).then((output: ISolidSimple) => { process.stdout.write(JSON.stringify(output)); }).catch((error) => { - failFunction(error); + fail(error); }); diff --git a/src/tslint.json b/src/tslint.json index 6a41014..7cd3c2b 100644 --- a/src/tslint.json +++ b/src/tslint.json @@ -10,7 +10,8 @@ "max-line-length": false, "no-bitwise": false, "no-console": false, - "quotemark": [true, "single"] + "quotemark": [true, "single"], + "object-literal-sort-keys": [true, "match-declaration-order"] }, "rulesDirectory": [] }