✨ Add argument parser, remove yargs-parser dependency
This commit is contained in:
parent
1b0d87d40f
commit
0b477cf755
8 changed files with 190 additions and 110 deletions
|
@ -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
|
||||
|
|
34
package-lock.json
generated
34
package-lock.json
generated
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
162
src/funcs/parseArguments.ts
Normal file
162
src/funcs/parseArguments.ts
Normal file
|
@ -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} <value>${(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,
|
||||
};
|
||||
}
|
|
@ -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 <product>');
|
||||
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -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 <product> --from <from> --to <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);
|
||||
});
|
||||
|
|
|
@ -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 <product> <from> <to>');
|
||||
|
||||
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);
|
||||
});
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue