Comparing version 0.1.2 to 0.1.3
@@ -5,2 +5,20 @@ # commandos Change Log | ||
## [0.1.4] - Jan 3, 2018 | ||
### New | ||
Allow taking non-option argument(s) as option value by setting __nonOption__ property in option definition. | ||
## [0.1.3] - Jan 2, 2018 | ||
### Fixed | ||
Fixed the bug taht Error *None of the option groups matched* throwed if no named options found even if there is at least one option group requiring no named options. | ||
## [0.1.2] - Dec 30, 2017 | ||
### New | ||
Global setting `ignoreInvalidArgument` added. | ||
## [0.1.1] - Dec 30, 2017 | ||
@@ -7,0 +25,0 @@ |
@@ -15,3 +15,3 @@ { | ||
"name": "commandos", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"main": "index.js", | ||
@@ -18,0 +18,0 @@ "keywords": [ |
242
parse.js
@@ -39,3 +39,3 @@ 'use strict'; | ||
function parseColumn(desc, catcher) { | ||
function parseColumn(desc) { | ||
let column = { | ||
@@ -49,2 +49,3 @@ name: null, | ||
overwrite: undefined, | ||
nonOption: undefined, | ||
}; | ||
@@ -55,2 +56,77 @@ | ||
desc = desc.replace(/(^|\s)\[(.+)\](\s|$)/, (content) => { | ||
let nonOption = RegExp.$2.trim(); | ||
if (!/^([^:]+)(:(.*))?$/.test(nonOption)) { | ||
throw new Error(`invalid nonOption definition: ${nonOption}`); | ||
} | ||
let positions = RegExp.$1.trim().split(/[,\s]+/); | ||
let valueDef = RegExp.$3.trim(); | ||
// --------------------------- | ||
// 生成位置匹配函数。 | ||
positions = positions.map(position => { | ||
let fn; | ||
if (position == '*') { | ||
return fn = () => true; | ||
} | ||
if (/^\d+$/.test(position)) { | ||
position = parseInt(position); | ||
return fn = (index) => index == position; | ||
} | ||
if (/^(>|>=|<|<=|=)(\d+)$/.test(position)) { | ||
position = parseInt(RegExp.$2); | ||
switch (RegExp.$1) { | ||
case '>' : return fn = (index) => index > position; | ||
case '>=' : return fn = (index) => index >= position; | ||
case '<' : return fn = (index) => index < position; | ||
case '<=' : return fn = (index) => index <= position; | ||
case '=' : return fn = (index) => index == position; | ||
} | ||
} | ||
throw new Error(`invalid nonOption definition: ${nonOption}`); | ||
}); | ||
// 最终的位置匹配函数。 | ||
let indexValidator = (index) => { | ||
let valid = true; | ||
for (let i = 0; valid && i < positions.length; i++) { | ||
valid = valid && positions[i](index); | ||
} | ||
return valid; | ||
}; | ||
// --------------------------- | ||
// 生成值匹配函数。 | ||
let valueValidator = null; | ||
if (valueDef == '') { | ||
valueValidator = () => true; | ||
} | ||
else if (/^=\*(.+)$/.test(valueDef)) { | ||
let v = RegExp.$1.trim().toLowerCase(); | ||
valueValidator = (value) => v == value.toLowerCase(); | ||
} | ||
else if (/^=(.+)$/.test(valueDef)) { | ||
let v = RegExp.$1.trim(); | ||
valueValidator = (value) => v == value; | ||
} | ||
else if (/^~\*(.+)$/.test(valueDef)) { | ||
let re = new RegExp(RegExp.$1.trim(), 'i'); | ||
valueValidator = (value) => re.test(value); | ||
} | ||
else if (/^~(.+)$/.test(valueDef)) { | ||
let re = new RegExp(RegExp.$1.trim()); | ||
valueValidator = (value) => re.test(value); | ||
} | ||
else { | ||
throw new Error(`invalid nonOption definition: ${nonOption}`); | ||
} | ||
// --------------------------- | ||
// 生成完整的非选项参数匹配函数。 | ||
column.nonOption = (value, index) => indexValidator(index) && valueValidator(value); | ||
// 位置替补定义语句已完成其使命。 | ||
// 注意须用一个空格替换,以免将可能的前后片断粘连在一起。 | ||
return ' '; | ||
}); | ||
desc = desc.replace(/\s*\([^)]+\)/g, (content) => { | ||
@@ -156,2 +232,20 @@ let index = inParentheses.length; | ||
if (typeof column.nonOption != 'undefined') { | ||
if (typeof column.nonOption == 'number') { | ||
let pos = column.nonOption; | ||
column.nonOption = (index, value) => index === pos; | ||
} | ||
else if (column.nonOption == 'string') { | ||
let text = column.nonOption; | ||
column.nonOption = (index, value) => value == text; | ||
} | ||
else if (column.nonOption instanceof RegExp) { | ||
let re = column.nonOption; | ||
column.nonOption = (index, value) => re.text(value); | ||
} | ||
else if (typeof column.nonOption != 'function') { | ||
throw new Error(`invalid option's nonOption property: $column.nonOption`); | ||
} | ||
} | ||
column.assignable = ifUndefined(column.assignable, true); | ||
@@ -181,3 +275,3 @@ column.nullable = ifUndefined(column.nullable, true); | ||
let parsedOptions = {}; | ||
let names_notation_cache = {}; | ||
for (let I = 0; I < def.options.length; I++) { | ||
@@ -193,2 +287,3 @@ const column = def.options[I]; | ||
const names_notation = names.map(name => (name.length > 1 ? '--' : '-') + name).join(', '); | ||
names_notation_cache[column.name] = names_notation; | ||
@@ -222,3 +317,53 @@ let found = false; | ||
} | ||
} | ||
if (raw.options.length) { | ||
if (def.explicit) { | ||
let names_notation = raw.options.map(option => (option.name.length > 1 ? '--' : '-') + option.name); | ||
throw new Error(`unknown options: ${names_notation}`); | ||
} else { | ||
while (raw.options.length) { | ||
let option = raw.options[0]; | ||
parsedOptions[option.name] = consumeOption(0); | ||
} | ||
} | ||
} | ||
// 在依据选项定义的 nonOption 属性消费余项之前,需要先删除已被其他选项显式占用的余项。 | ||
parsedOptions.$ = raw.$.filter(v => v !== null); | ||
for (let I = 0; I < def.options.length; I++) { | ||
const column = def.options[I]; | ||
const names_notation = names_notation_cache[column.name]; | ||
let found = parsedOptions.hasOwnProperty(column.name); | ||
let value = parsedOptions[column.name]; | ||
// 消费余项。 | ||
if (!found && column.nonOption) { | ||
value = column.multiple ? [] : null; | ||
let matchedIndexes = []; | ||
for (let i = 0, $i; i < parsedOptions.$.length; i++) { | ||
$i = parsedOptions.$[i]; | ||
if ($i === null) continue; | ||
if (column.nonOption($i, i)) { | ||
// 将匹配项中余项数组中剥离。 | ||
parsedOptions.$[i] = null; | ||
found = true; | ||
// 如果选项支持重复项,则继续尝试匹配,否则终止。 | ||
if (column.multiple) { | ||
value.push($i); | ||
} | ||
else { | ||
value = column.assignable ? $i : true; | ||
break; | ||
} | ||
} | ||
} | ||
if (found) { | ||
parsedOptions[column.name] = value; | ||
} | ||
} | ||
if (found && !column.nullable && typeof value == 'boolean') { | ||
@@ -241,16 +386,5 @@ throw new Error(`option need to be valued: ${names_notation}`); | ||
if (raw.options.length) { | ||
if (def.explicit) { | ||
let names_notation = raw.options.map(option => (option.name.length > 1 ? '--' : '-') + option.name); | ||
throw new Error(`unknown options: ${names_notation}`); | ||
} else { | ||
while (raw.options.length) { | ||
let option = raw.options[0]; | ||
parsedOptions[option.name] = consumeOption(0); | ||
} | ||
} | ||
} | ||
// 注意:因为选项可能依据 nonOption 属性又消费了一轮余项,因此这里有必要再筛选一次。 | ||
parsedOptions.$ = raw.$.filter(v => v !== null); | ||
parsedOptions.$ = raw.$.filter(v => v != null); | ||
return parsedOptions; | ||
@@ -353,4 +487,4 @@ } | ||
caseSensitive: true, | ||
catcher: null, | ||
explicit: false, | ||
catcher: null, | ||
groups: null, | ||
@@ -365,49 +499,43 @@ options: [], | ||
let throws = (fn) => { | ||
try { | ||
return fn(); | ||
} catch (ex) { | ||
if (def.catcher) def.catcher(ex); | ||
else throw ex; | ||
} | ||
}; | ||
let parsedOptions = null; | ||
try { | ||
let raw = parseRaw(args, def); | ||
if (def.groups && def.groups.length) { | ||
let reasons = []; | ||
let maxMatching = -1; | ||
for (let i = 0; i < def.groups.length; i++) { | ||
let rawcopy = safeClone(raw); | ||
let parsed = null; | ||
let matching = 0; | ||
def.options = def.groups[i].map(parseColumn); | ||
try { | ||
parsed = parseOptions(rawcopy, def); | ||
let raw = throws(() => parseRaw(args, def)); | ||
let parsedOptions = null; | ||
if (def.groups && def.groups.length) { | ||
let reasons = []; | ||
let maxMatching = 0; | ||
for (let i = 0; i < def.groups.length; i++) { | ||
let rawcopy = safeClone(raw); | ||
let parsed = null; | ||
let matching = 0; | ||
def.options = throws(() => def.groups[i].map(parseColumn)); | ||
try { | ||
parsed = parseOptions(rawcopy, def); | ||
// 取匹配度最高的选项组。 | ||
def.options.forEach(option => { | ||
if (parsed.hasOwnProperty(option.name)) matching++; | ||
}); | ||
if (matching > maxMatching) { | ||
maxMatching = matching; | ||
parsedOptions = parsed; | ||
// 取匹配度最高的选项组。 | ||
def.options.forEach(option => { | ||
if (parsed.hasOwnProperty(option.name)) matching++; | ||
}); | ||
if (matching > maxMatching) { | ||
maxMatching = matching; | ||
parsedOptions = parsed; | ||
} | ||
} catch (ex) { | ||
reasons.push(ex); | ||
} | ||
} catch (ex) { | ||
reasons.push(ex); | ||
} | ||
} | ||
if (!parsedOptions) { | ||
let error = new Error('None of the option groups matched'); | ||
error.reasons = reasons; | ||
if (def.catcher) def.catcher(error); | ||
else throw error; | ||
if (!parsedOptions) { | ||
let error = new Error('None of the option groups matched'); | ||
error.reasons = reasons; | ||
throw error; | ||
} | ||
} | ||
else { | ||
def.options = def.options.map(parseColumn); | ||
parsedOptions = parseOptions(raw, def); | ||
} | ||
} catch(ex) { | ||
if (def.catcher) def.catcher(ex); | ||
else throw ex; | ||
} | ||
else { | ||
def.options = throws(() => def.options.map(parseColumn)); | ||
parsedOptions = throws(() => parseOptions(raw, def)); | ||
} | ||
return parsedOptions; | ||
@@ -414,0 +542,0 @@ } |
@@ -21,2 +21,5 @@ # commandos | ||
* [API](#api) | ||
* [Go Advanced](#go-advanced) | ||
* [ODL, Option Definition Language](#odl-option-definition-language) | ||
* [Take Non-option Argument As Option Value](#take-non-option-argument-as-option-value) | ||
* [Examples](#examples) | ||
@@ -164,2 +167,5 @@ * [Why commandos](#why-commandos) | ||
* __nonOption__ *number | string | RegExp | Function* OPTIONAL | ||
If named option not found, the matching non-option argument(s) will be taken as the value of the option. See [Take Non-option Argument As Option Value](#take-non-option-argument-as-option-value) for details. | ||
* __nullable__ *boolean* DEFAULT `true` OPTIONAL | ||
@@ -177,4 +183,10 @@ When we say some option is NOT __nullable__, we mean it SHOULD NOT appear in the command line without being followed by some value. | ||
It can also be a string according to private syntax looks like [column definition in MySQL](https://dev.mysql.com/doc/refman/8.0/en/create-table.html): | ||
It can also be a string according to private syntax looks like [column definition in MySQL](https://dev.mysql.com/doc/refman/8.0/en/create-table.html). For convenience, it is hereinafter referred to as [__ODL__(Option Definition Language)](#odl-option-definition-language). | ||
## Go Advanced | ||
### ODL, Option Definition Language | ||
ODL is a tiny language used to define option. It is an easy alternative for option define object. E.g. | ||
```javascript | ||
@@ -188,2 +200,5 @@ // * The option is named "version", or "v" in short. The first name is formal. | ||
// * If named option not offered, the first non-argument will be used as value of option "action". | ||
'--action [0:~* (start|stop|restart)]' | ||
// * The first word is regarded as formal name of the option. | ||
@@ -194,2 +209,37 @@ // * Alias "v" and "edition" are also acceptable. | ||
Keywords in ODL is case-insensitive: | ||
* [] | ||
* ALIAS | ||
* ASSIGNABLE | ||
* CASE_SENSITIVE | ||
* CASE_INSENSITIVE | ||
* COMMENT | ||
* DEFAULT | ||
* MULTIPLE | ||
* NULLALBE | ||
* OVERWRITE | ||
* REQUIRED | ||
### Take Non-option Argument As Option Value | ||
To make command line more flexiable, __commandos.parse__ allows, by setting __nonOption__ in *definition of an option*, to take non-option argument(s) as option value while named option not found. Property __nonOption__ is overloaded with following types: | ||
* __nonOption__ *number* | ||
* __nonOption__ *string* | ||
* __nonOption__ *RegExp* | ||
* __nonOption__ *Function*(value, index) | ||
In ODL, delimiters `[]` is used to define the nonOption property: | ||
```javascript | ||
// * Fixed position of non-option argument. | ||
// * Fixed value. | ||
'--help [0:=* help] NOT ASSIGNABLE' | ||
// * Any position. | ||
// * Use regular expression (case-insensitive) to validate the arguments. | ||
'--action [*:~* (start|stop|restart)]' | ||
// * Position range. | ||
'--name [>1]' | ||
``` | ||
## Examples | ||
@@ -199,3 +249,7 @@ | ||
* [commandos.parse](./test/parse.js) | ||
* [commandos.parse: basic usage](./test/parse.basic-usage.js) | ||
* [commandos.parse: global settings](./test/parse.global-settings.js) | ||
* [commandos.parse: option settings](./test/parse.option-settings.js) | ||
* [commandos.parse: take non-option argument as option value](./test/parse.option-nonoption.js) | ||
* [commandos.parse: option groups](./test/parse.option-groups.js) | ||
@@ -202,0 +256,0 @@ ## Why *commandos* |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
44374
10
880
270
1