Compare commits
17 Commits
original-w
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
1d98a0707c | |
|
|
c3166ef17b | |
|
|
f43915c735 | |
|
|
b6005e11bb | |
|
|
014afb0c86 | |
|
|
4dec08afbc | |
|
|
ddef1d82dc | |
|
|
e5e1eeec8a | |
|
|
b9e15019c0 | |
|
|
c2ea099bbf | |
|
|
0d2224eba7 | |
|
|
e061fb0e76 | |
|
|
5b3cac5103 | |
|
|
86e16a9855 | |
|
|
1a1b189f7a | |
|
|
cec7d20a14 | |
|
|
216cdcae53 |
|
|
@ -1,2 +1,3 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
|
*.tmp.*
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
module.exports = parse;
|
|
||||||
|
|
||||||
// Inspired a bit by
|
|
||||||
// https://github.com/eveningkid/args-parser/blob/master/parse.js
|
|
||||||
|
|
||||||
function parse(argv) {
|
|
||||||
argv = process.argv.slice(2);
|
|
||||||
let output = {};
|
|
||||||
|
|
||||||
argv.forEach((arg) => {
|
|
||||||
// We'll treat `--long` flags and flags without dash prefixes the same,
|
|
||||||
// so just discard double-dash prefixes
|
|
||||||
if (arg.startsWith('--')) { arg = arg.substring(2); }
|
|
||||||
|
|
||||||
// Handle grouped short flags (eg `ps -aux`)
|
|
||||||
// (Let's not let grouped ones have values) //... FIXME, maybe?
|
|
||||||
if (arg.startsWith('-')) {
|
|
||||||
if (arg.length < 2) {
|
|
||||||
// It's not grouped; just remove the '-' and treat it like the rest, later
|
|
||||||
arg = arg.substring(1);
|
|
||||||
} else {
|
|
||||||
arg = arg.substring(1);
|
|
||||||
let ungrouped = arg.split('');
|
|
||||||
ungrouped.forEach(a => output[a] = true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let split = arg.split('=')
|
|
||||||
if (split.length > 2) { throw new Error(`Too many '=' in argument ${arg}`); }
|
|
||||||
if (split.length === 2) {
|
|
||||||
let [key, value] = split;
|
|
||||||
output[key] = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
output[arg] = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
// - building a structured output
|
|
||||||
// - testing
|
|
||||||
|
|
||||||
|
|
||||||
function Arg({
|
|
||||||
name = 'Flag',
|
|
||||||
shortFlag = null,
|
|
||||||
longFlag = null,
|
|
||||||
requiresValue=false,
|
|
||||||
optional=false
|
|
||||||
}) {
|
|
||||||
this.name = name;
|
|
||||||
this.flags = { short: shortFlag, long: longFlag };
|
|
||||||
this.requiresValue = requiresValue;
|
|
||||||
this.optional = optional;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function Arguments(argumentsObject=[]) {
|
|
||||||
this.args = argumentsObject;
|
|
||||||
|
|
||||||
this.add = (...a) => a.forEach((x) => argumentsObject.push(new Arg(x)));
|
|
||||||
|
|
||||||
this.parse = function() {
|
|
||||||
const argsRequiringValues = this.args.filter((a) => a.requiresValue);
|
|
||||||
const requiredArgs = this.args.filter((a) => !a.optional);
|
|
||||||
|
|
||||||
let argv = process.argv.slice(2);
|
|
||||||
|
|
||||||
// Split up grouped short flags
|
|
||||||
let argvSplit = [].concat(...argv.map((x) => {
|
|
||||||
if (x.startsWith('-') && !x.startsWith('--')) { // short flags
|
|
||||||
let arr = x.substring(1).split('');
|
|
||||||
arr = arr.map((y) => '-' + y);
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
// Check if all the required flags have been included
|
|
||||||
requiredArgs.forEach((a) => {
|
|
||||||
if (!argvSplit.includes('-'+a.flags.short) && !argvSplit.includes('-'+a.flags.long)) {
|
|
||||||
let long = (a.flags.long) ? `--${a.flags.long}, ` : '';
|
|
||||||
throw new Error(`${a.name} flag (-${a.flags.short}${long}) is required`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate...
|
|
||||||
argvSplit.forEach((av, index) => {
|
|
||||||
// Check for attempts to use --flags that don't exist
|
|
||||||
let valid = true;
|
|
||||||
if (av.startsWith('--')) {
|
|
||||||
// TODO: this may be wrong:
|
|
||||||
valid = this.args.filter((a) => a.flags.long === av.substring(2)).length > 0;
|
|
||||||
} else if (av.startsWith('-')) {
|
|
||||||
// TODO: this may be wrong:
|
|
||||||
valid = this.args.filter((a) => a.flags.short === av.substring(1)).length > 0;
|
|
||||||
}
|
|
||||||
if (!valid) { throw new Error(`There is no ${av} option`); }
|
|
||||||
|
|
||||||
// Check if it requires a value...
|
|
||||||
// and if it does, do a cursory check to see if one is likely to be there
|
|
||||||
let shouldCheckValue = argsRequiringValues.filter((a) => {
|
|
||||||
let noDash = av.substring(1);
|
|
||||||
return (a.flags.short === noDash || a.flags.long === noDash);
|
|
||||||
}).length > 0;
|
|
||||||
|
|
||||||
if (shouldCheckValue) {
|
|
||||||
let errorMessage = `Value required for ${av}`;
|
|
||||||
// If there's nothing after this flag, error;
|
|
||||||
// Or if the next item is a flag... error:
|
|
||||||
if (typeof argvSplit[index + 1] === 'undefined') { throw new Error(errorMessage); }
|
|
||||||
else if (argvSplit[index + 1].startsWith('-')) { throw new Error(errorMessage); };
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO return something useful
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Arguments: Arguments,
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
/* For reference:
|
||||||
|
|
||||||
|
- 'options' are flags preceded by one or two dashs:
|
||||||
|
eg. -f, --foo
|
||||||
|
|
||||||
|
- 'option-arguments' are (non-option) strings that follow an option (and go with it)
|
||||||
|
eg. 'filename.txt' in './example --input filename.txt'
|
||||||
|
|
||||||
|
- 'operands' are (non-option) strings that follow an option, but aren't option-arguments
|
||||||
|
eg 'filename.txt' in './example --verbose filename.txt'
|
||||||
|
|
||||||
|
see https://unix.stackexchange.com/questions/364383/confusion-about-changing-meaning-of-arguments-and-options-is-there-an-official
|
||||||
|
and https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/basedefs/V1_chap12.html#tag_12_01
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = class Opter {
|
||||||
|
synonyms = {};
|
||||||
|
definedOptions = {};
|
||||||
|
userOptions = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} shortFlag
|
||||||
|
* @param {string} longFlag
|
||||||
|
* @param {boolean} [required]
|
||||||
|
* @param {boolean} [requiresArgument]
|
||||||
|
* @param {number} [maxArguments]
|
||||||
|
**/
|
||||||
|
addOption(shortFlag, longFlag, required=false, requiresArgument=false, maxArguments=Infinity) {
|
||||||
|
let reqs = { required: required, requiresArgument: requiresArgument, maxArguments: maxArguments };
|
||||||
|
const short = stripDashes(shortFlag);
|
||||||
|
const long = stripDashes(longFlag);
|
||||||
|
this.definedOptions[long] = reqs;
|
||||||
|
this.synonyms[long] = short;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} argv An argv object (`process.argv`)
|
||||||
|
* @returns {Object<string, boolean | Array<string>>}
|
||||||
|
**/
|
||||||
|
parse(argv) {
|
||||||
|
this.userOptions = argvToObject(argv);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Object<string, boolean | Array<string>>}
|
||||||
|
* The user-provided with ther long names as the keys **/
|
||||||
|
let normalizedUserOptions = {};
|
||||||
|
|
||||||
|
Object.keys(this.definedOptions).forEach(longName => {
|
||||||
|
let shortName = this.synonyms[longName];
|
||||||
|
let reqs = this.definedOptions[longName];
|
||||||
|
let providedLong = typeof this.userOptions[longName] !== 'undefined';
|
||||||
|
let providedShort = typeof this.userOptions[shortName] !== 'undefined';
|
||||||
|
let provided = providedLong || providedShort;
|
||||||
|
|
||||||
|
let userArgs = null;
|
||||||
|
// arrays have type 'object':
|
||||||
|
if (typeof this.userOptions[longName] === 'object') { userArgs = this.userOptions[longName]; }
|
||||||
|
if (typeof this.userOptions[shortName] === 'object') { userArgs = this.userOptions[shortName]; }
|
||||||
|
|
||||||
|
if (provided) {
|
||||||
|
normalizedUserOptions[longName] = typeof userArgs === 'object' ? userArgs : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check that required options are provided */
|
||||||
|
if (reqs.required && !provided) {
|
||||||
|
throw new Error(`Missing required option '${longName}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check the number of arguments */
|
||||||
|
if (provided && reqs.requiresArgument) {
|
||||||
|
// Make sure required arguments are provided
|
||||||
|
if (userArgs === null) {
|
||||||
|
throw new Error(`Missing required argument for '${longName}'`);
|
||||||
|
}
|
||||||
|
// And make sure there aren't too many
|
||||||
|
if (userArgs.length > reqs.maxArguments) {
|
||||||
|
throw new Error(`Too many arguments for '${longName}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return normalizedUserOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object.<string, boolean | Array<string>>}
|
||||||
|
**/
|
||||||
|
function argvToObject(argv) {
|
||||||
|
argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
// Label everything in argv as either
|
||||||
|
// an 'option' or an 'arg_or_operand'
|
||||||
|
let mapped = argv.flatMap((a) => {
|
||||||
|
// A single long option
|
||||||
|
if (a.startsWith('--')) {
|
||||||
|
return { arg: a.substring(2), type: 'option' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single short option
|
||||||
|
if (a.startsWith('-') && (a.length === 2)) {
|
||||||
|
return { arg: a.substring(1), type: 'option' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple short options
|
||||||
|
if (a.startsWith('-')) {
|
||||||
|
let splitGroup = a.substring(1).split('');
|
||||||
|
return splitGroup.map((sg) => { return {arg: sg, type: 'option'} });
|
||||||
|
}
|
||||||
|
|
||||||
|
// An option-argument or operand
|
||||||
|
return { arg: a, type: 'arg_or_operand' };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group consecutive option-args/operands
|
||||||
|
let wrapped = mapped.map((a) => [a]);
|
||||||
|
let grouped = [];
|
||||||
|
wrapped.forEach((a, i) => {
|
||||||
|
if (i === 0 || a[0].type === 'option') {
|
||||||
|
grouped.push(a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let prev = grouped.pop();
|
||||||
|
if (prev[0].type === 'option') {
|
||||||
|
grouped.push(prev);
|
||||||
|
grouped.push(a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
grouped.push(prev.concat(a));
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @type {Object<string, boolean | Array<string>>} **/
|
||||||
|
let out = {};
|
||||||
|
grouped.forEach((a, i) => {
|
||||||
|
let next = grouped[i+1];
|
||||||
|
|
||||||
|
if ((a[0].type === 'option')
|
||||||
|
&& next
|
||||||
|
&& (next[0].type === 'arg_or_operand')) {
|
||||||
|
let next_shortened = next.map(n => n.arg);
|
||||||
|
out[a[0].arg] = next_shortened;
|
||||||
|
grouped.splice(i, 1);
|
||||||
|
} else {
|
||||||
|
let pair = {};
|
||||||
|
pair[a[0].arg] = true;
|
||||||
|
a.forEach((b) => {
|
||||||
|
out[b.arg] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripDashes(optionName) {
|
||||||
|
if (optionName.startsWith('--')) return optionName.substring(2);
|
||||||
|
if (optionName.startsWith('-')) return optionName.substring(1);
|
||||||
|
return optionName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
const Opter = require('./opter.js');
|
||||||
|
|
||||||
|
const opter = new Opter();
|
||||||
|
opter.addOption('-i', '--input', true);
|
||||||
|
opter.addOption('-d', '--debug');
|
||||||
|
opter.addOption('-f', '--filename', false, true);
|
||||||
|
let opts = opter.parse(process.argv);
|
||||||
|
|
||||||
|
console.log(opts);
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
const args = require('./arg-parser-simpler.js')(process.argv);
|
|
||||||
|
|
||||||
console.log(args);
|
|
||||||
18
tests.js
18
tests.js
|
|
@ -1,18 +0,0 @@
|
||||||
const test = require('node:test');
|
|
||||||
|
|
||||||
const argParser = require('./arg-parser.js')
|
|
||||||
|
|
||||||
// TODO...
|
|
||||||
|
|
||||||
console.log(argParser);
|
|
||||||
|
|
||||||
let cpuArguments = new argParser.Arguments();
|
|
||||||
cpuArguments.add(
|
|
||||||
{name: 'Filename', shortFlag: 'f', requiresValue: true},
|
|
||||||
{name: 'Clock speed', shortFlag: 'c', longFlag: 'clock', requiresValue: true, optional: true},
|
|
||||||
{name: 'Debug', shortFlag: 'd', longFlag: 'debug', optional: true},
|
|
||||||
{name: 'Pretty-print display', shortFlag: 'p', longFlag: 'pretty', optional: true},
|
|
||||||
{name: 'Single-step', shortFlag: 's', longFlag: 'step', optional: true});
|
|
||||||
|
|
||||||
console.log(cpuArguments); //.parse();
|
|
||||||
console.log(cpuArguments.parse()); //.parse();
|
|
||||||
Loading…
Reference in New Issue