lots of fixes, tweaks and cleanup...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2020-07-06 20:31:20 +03:00
parent 29a6c44923
commit 6c18c1b3a6
2 changed files with 96 additions and 86 deletions

171
argv.js
View File

@ -30,7 +30,6 @@ var object = require('ig-object')
module.STOP = module.STOP =
{doc: 'stop option processing, triggers .stop(..) handlers'} {doc: 'stop option processing, triggers .stop(..) handlers'}
// XXX rename???
module.THEN = module.THEN =
{doc: 'break option processing, triggers .then(..) handlers'} {doc: 'break option processing, triggers .then(..) handlers'}
@ -42,8 +41,8 @@ module.ERROR =
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// helpers... // helpers...
// XXX do we need to remove handlers??? // XXX does this need to be an event???
// XXX does this need to be an event constructor??? // XXX doc...
var afterCallback = function(name){ var afterCallback = function(name){
var attr = '__after_'+ name var attr = '__after_'+ name
return function(func){ return function(func){
@ -86,11 +85,15 @@ var afterCallback = function(name){
// //
// arg: 'VALUE|key', // arg: 'VALUE|key',
// //
// type: 'int',
//
// env: 'VALUE', // env: 'VALUE',
// //
// default: 123, // default: 123,
// //
// required: true, // required: false,
//
// valueRequired: false,
// //
// handler: function(opts, key, value){ // handler: function(opts, key, value){
// ... // ...
@ -157,21 +160,26 @@ var afterCallback = function(name){
// NOTE: essentially this parser is a very basic stack language... // NOTE: essentially this parser is a very basic stack language...
// XXX can we implement the whole thing directly as a stack language??? // XXX can we implement the whole thing directly as a stack language???
// //
// XXX add -about flag??? // XXX need to handle values (and return values) of nested parsers correctly...
// XXX we should be able to set .scriptName by hand... // XXX can we add more prefexes, like '+' and the like???
// XXX might be a good idea to read metadata from package.json // XXX might be a good idea to read metadata from package.json
// XXX handle option types??? // XXX should -help should work for any command?
// XXX --help should work for any command and not just for the nested // ...now only works for nested parsers...
// parser commands... (???)
// ...not sure how to implement this...
// .....or should it be the responsibility of the user defining
// the command???
// XXX should we handle <scriptName>-<command> script calls??? // XXX should we handle <scriptName>-<command> script calls???
// XXX should .options(..), .commands(..) and .handler(..) be: // XXX might be a good idea to use exceptions for ERROR...
// .getOptions(..), .getCommands(..) and .getHandler(..) respectively???
var Parser = var Parser =
module.Parser = module.Parser =
object.Constructor('Parser', { object.Constructor('Parser', {
typeHandlers: {
int: parseInt,
float: parseFloat,
number: function(v){ return new Number(v) },
string: function(v){ return v.toString() },
date: function(v){ return new Date(v) },
// XXX ...
},
}, {
// config... // config...
splitOptions: true, splitOptions: true,
optionPrefix: '-', optionPrefix: '-',
@ -192,13 +200,11 @@ object.Constructor('Parser', {
// output... // output...
// //
// XXX how do we return something from these???
// ...closure?? ...globals?
print: function(...args){ print: function(...args){
console.log(...args) console.log(...args)
return this }, return this },
printError: function(...args){ printError: function(...args){
console.error(...args) console.error(this.scriptName+': Error:', ...args)
return this }, return this },
@ -211,9 +217,6 @@ object.Constructor('Parser', {
// ] // ]
// //
// XXX do we need to output <doc> here??? // XXX do we need to output <doc> here???
// ...if it's used only in -help then it would be simpler to
// remove it from here and get everything in formDoc(..), same
// goes for <arg>...
options: function(...prefix){ options: function(...prefix){
var that = this var that = this
prefix = prefix.length == 0 ? prefix = prefix.length == 0 ?
@ -281,7 +284,9 @@ object.Constructor('Parser', {
key = this.optionInputPattern.test(key) ? key = this.optionInputPattern.test(key) ?
key.replace(this.optionInputPattern, this.optionPrefix+'$1') key.replace(this.optionInputPattern, this.optionPrefix+'$1')
: key.replace(this.commandInputPattern, this.commandPrefix+'$1') : key.replace(this.commandInputPattern, this.commandPrefix+'$1')
var seen = new Set([key]) // quote '*'...
key = key.replace(/^(.)\*$/, '$1\\*')
var seen = new Set()
while(key in this while(key in this
&& typeof(this[key]) == typeof('str')){ && typeof(this[key]) == typeof('str')){
key = this[key] key = this[key]
@ -305,22 +310,16 @@ object.Constructor('Parser', {
// doc config... // doc config...
helpColumnOffset: 3, helpColumnOffset: 3,
helpColumnPrefix: '- ', helpColumnPrefix: '- ',
//helpOptionSeparator: ' | ',
helpArgumentSeparator: ', ', helpArgumentSeparator: ', ',
//helpValueSeparator: '=', helpValueSeparator: '=',
helpValueSeparator: ' ',
// doc sections... // doc sections...
// XXX might be a good idea to read these from package.json by default...
// XXX
author: undefined, author: undefined,
license: undefined, license: undefined,
usage: '$SCRIPTNAME [OPTIONS]', usage: '$SCRIPTNAME [OPTIONS]',
doc: undefined, doc: undefined,
// XXX test this with string value...
examples: undefined, examples: undefined,
// XXX add license and version info... //footer: '$SCRIPTNAME ($VERSION / $LICENSE) by $AUTHOR',
//footer: '$SCRIPTNAME ($VERSION) by $AUTHOR',
footer: undefined, footer: undefined,
// XXX should wrap long lines... // XXX should wrap long lines...
@ -498,13 +497,14 @@ object.Constructor('Parser', {
// Dynamic options / Dynamic commands // Dynamic options / Dynamic commands
// .section_doc is a string or array // .section_doc is a string or array
// //
// XXX need a way to quote '*' to make it usable in flags/commands... // NOTE: to explicitly handle '-*' option or '*' command define handlers
// for them under '-\\*' and '@\\*' respectively.
'-*': { '-*': {
//key: '-*', //key: '-*',
doc: false, doc: false,
//section_doc: ..., //section_doc: ...,
handler: function(_, key){ handler: function(_, key){
this.printError('Unknown '+ (key.startsWith('-') ? 'option:' : 'command:'), key) this.printError('unknown '+ (key.startsWith('-') ? 'option:' : 'command:'), key)
return module.ERROR } }, return module.ERROR } },
'@*': '-*', '@*': '-*',
@ -512,7 +512,6 @@ object.Constructor('Parser', {
// Default handler action... // Default handler action...
// //
// This is called when .handler is not set... // This is called when .handler is not set...
//
handlerDefault: function(handler, rest, key, value){ handlerDefault: function(handler, rest, key, value){
key = (handler.arg key = (handler.arg
&& handler.arg && handler.arg
@ -521,11 +520,9 @@ object.Constructor('Parser', {
.trim()) .trim())
// get the final key... // get the final key...
|| this.handler(key)[0].slice(1) || this.handler(key)[0].slice(1)
this[key] = // if value not given set true...
arguments.length < 4 ? this[key] = arguments.length < 4 ?
true this.handleArgumentValue(handler, true)
: value === undefined ?
handler.default || true
: value : value
return this }, return this },
@ -534,30 +531,24 @@ object.Constructor('Parser', {
// //
// If this is false/undefined value is passed to the handler as-is... // If this is false/undefined value is passed to the handler as-is...
// //
// Example: // NOTE: to disable this functionality just set:
// typeHandler: { // handleArgumentValue: false
// int: parseInt, handleArgumentValue: function(handler, value){
// float: parseFloat, var convert =
// number: function(v){ return new Number(v) }, typeof(handler.type) == 'function' ?
// string: function(v){ return v.toString() }, handler.type
// ... : (this.typeHandlers
// }, || this.constructor.typeHandlers
// handleArgumentValue: function(handler, value){ || {})[handler.type]
// var convert = typeof(handler.type) == 'function' ? return convert ?
// handler.type convert.call(this, value)
// : this.typeHandler[handler.type] : value },
// return convert ?
// convert(value)
// : value },
//
// XXX should we define a handler.type handler???
handleArgumentValue: false,
// Handle error exit... // Handle error exit...
// //
// If this is set to false Parser will not call process.exit(..) on // If this is set to false Parser will not call process.exit(..) on
// error... // error...
handleErrorExit: function(arg){ handleErrorExit: function(arg, reason){
typeof(process) != 'unhandled' typeof(process) != 'unhandled'
&& process.exit(1) }, && process.exit(1) },
@ -598,7 +589,7 @@ object.Constructor('Parser', {
// handler... // handler...
__call__: function(context, argv, main, root_value){ __call__: function(context, argv, main, root_value){
var parsed = Object.create(this) var parsed = Object.create(this)
var nested = parsed.nested = false var opt_pattern = parsed.optionInputPattern
var rest = parsed.rest = var rest = parsed.rest =
argv == null ? argv == null ?
(typeof(process) != 'unhandled' ? (typeof(process) != 'unhandled' ?
@ -609,6 +600,7 @@ object.Constructor('Parser', {
main = main main = main
|| require.main.filename || require.main.filename
// nested command handler... // nested command handler...
var nested = parsed.nested = false
if(context instanceof Parser){ if(context instanceof Parser){
nested = parsed.nested = true nested = parsed.nested = true
main = context.scriptName +' '+ main main = context.scriptName +' '+ main
@ -620,55 +612,56 @@ object.Constructor('Parser', {
|| rest.unshift(main) } || rest.unshift(main) }
// script stuff... // script stuff...
var script = parsed.script = rest.shift() var script = parsed.script = rest.shift()
parsed.scriptName = script.split(/[\\\/]/).pop() var basename = script.split(/[\\\/]/).pop()
parsed.scriptName = parsed.scriptName || basename
parsed.scriptPath = script.slice(0, parsed.scriptPath = script.slice(0,
script.length - parsed.scriptName.length) script.length - parsed.scriptName.length)
var opt_pattern = parsed.optionInputPattern
// helpers... // helpers...
var handleError = function(reason, arg, rest){ var handleError = function(reason, arg, rest){
parsed.error(reason, arg, rest) parsed.error(reason, arg, rest)
parsed.handleErrorExit parsed.handleErrorExit
&& parsed.handleErrorExit(arg, reason) } && parsed.handleErrorExit(arg, reason) }
var defaultHandler = function(handler){ var defaultHandler = function(handler){
return function(rest, arg, value) { return function(...args){
return parsed.handlerDefault(handler, ...arguments) } } return parsed.handlerDefault(handler, ...args) } }
var runHandler = function(handler, arg, rest){ var runHandler = function(handler, arg, rest){
var [arg, value] = arg instanceof Array ? var [arg, value] = arg instanceof Array ?
arg arg
: arg.split(/=/) : arg.split(/=/)
// get option value... // get value...
value = value == null ? value = value == null ?
((handler.arg && !opt_pattern.test(rest[0])) ? (((handler.arg && !opt_pattern.test(rest[0])) ?
rest.shift() rest.shift()
: (typeof(process) != 'undefined' && handler.env) ? : (typeof(process) != 'undefined' && handler.env) ?
(process.env[handler.env] process.env[handler.env]
|| handler.default) : value)
: handler.default) || handler.default)
: value : value
// value conversion... // value conversion...
value = (value != null value = (value != null
&& parsed.handleArgumentValue) ? && parsed.handleArgumentValue) ?
parsed.handleArgumentValue(handler, value) parsed.handleArgumentValue(handler, value)
: value : value
// required value ...
if(handler.valueRequired && value == null){
handleError('value missing', arg, rest)
parsed.printError('value missing:', arg+'=?')
return module.ERROR }
// run handler... // run handler...
var res = (typeof(handler) == 'function' ? var res = (typeof(handler) == 'function' ?
handler handler
: (handler.handler : (handler.handler
|| defaultHandler(handler))) || defaultHandler(handler)))
.call(parsed, .call(parsed, rest, arg,
rest,
arg,
...(value != null ? ...(value != null ?
[value] [value]
: [])) : []))
// handle .STOP / .ERROR
res === module.STOP res === module.STOP
&& parsed.stop(arg, rest) && parsed.stop(arg, rest)
// XXX might be a good idea to use exceptions for this...
res === module.ERROR res === module.ERROR
// XXX is this the correct reason???
&& handleError('unknown', arg, rest) && handleError('unknown', arg, rest)
return res } return res }
// NOTE: if successful this needs to modify the arg, thus it // NOTE: if successful this needs to modify the arg, thus it
@ -698,6 +691,9 @@ object.Constructor('Parser', {
var unhandled = [] var unhandled = []
while(rest.length > 0){ while(rest.length > 0){
var arg = rest.shift() var arg = rest.shift()
// NOTE: opts and commands do not follow the same path here
// because options if unidentified need to be split into
// single letter options and commands to not...
var type = opt_pattern.test(arg) ? var type = opt_pattern.test(arg) ?
'opt' 'opt'
: parsed.isCommand(arg) ? : parsed.isCommand(arg) ?
@ -713,17 +709,19 @@ object.Constructor('Parser', {
&& parsed.splitOptions && parsed.splitOptions
&& splitArgs(arg, rest)) && splitArgs(arg, rest))
// dynamic or error... // dynamic or error...
|| parsed.handler( || parsed[type == 'opt' ? '-*' : '@*']
type == 'opt' ? // in case no handler found and '-*' / '@*' not defined...
'-*' if(handler == null){
: '@*')[1] handleError('unknown', arg, rest)
parsed.printError('unknown '+(type == 'opt' ? 'option:' : 'command:'), arg)
return module.ERROR }
// normalize output of splitArgs(..) // normalize output of splitArgs(..)
;[arg, handler] = handler instanceof Array ? ;[arg, handler] = handler instanceof Array ?
handler handler
: [arg, handler] : [arg, handler]
// value handler called...
;(handler.env // mark handler...
|| 'default' in handler) ;(handler.env || 'default' in handler)
&& values.add(handler) && values.add(handler)
seen.add(handler) seen.add(handler)
@ -755,7 +753,7 @@ object.Constructor('Parser', {
[k[0], handler.default], [k[0], handler.default],
rest)) }) rest)) })
// check required options... // check and report required options...
var missing = parsed var missing = parsed
.requiredOptions() .requiredOptions()
.filter(function([k, a, d, h]){ .filter(function([k, a, d, h]){
@ -764,13 +762,14 @@ object.Constructor('Parser', {
return k.pop() }) return k.pop() })
if(missing.length > 0){ if(missing.length > 0){
handleError('required', missing, rest) handleError('required', missing, rest)
parsed.printError('Required but missing:', missing.join(', ')) parsed.printError('required but missing:', missing.join(', '))
return parsed } return parsed }
// post handlers... // handle root value...
root_value = root_value && parsed.handleArgumentValue ? root_value = (root_value && parsed.handleArgumentValue) ?
parsed.handleArgumentValue(parsed, root_value) parsed.handleArgumentValue(parsed, root_value)
: root_value : root_value
parsed.then(unhandled, root_value, rest) parsed.then(unhandled, root_value, rest)
return parsed }, return parsed },

11
test.js
View File

@ -52,6 +52,7 @@ argv.Parser({
default: 333, default: 333,
}, },
// XXX need to handle value correctly...
'-test': argv.Parser({ '-test': argv.Parser({
env: 'TEST', env: 'TEST',
arg: 'TEST', arg: 'TEST',
@ -59,6 +60,12 @@ argv.Parser({
}).then(function(){ }).then(function(){
console.log('TEST', ...arguments) }), console.log('TEST', ...arguments) }),
'-int': {
arg: 'INT|int',
type: 'int',
valueRequired: true,
},
'@nested': argv.Parser({ '@nested': argv.Parser({
doc: 'nested parser.', doc: 'nested parser.',
@ -69,6 +76,10 @@ argv.Parser({
}).then(function(){ }).then(function(){
console.log('NESTED DONE', ...arguments) }), console.log('NESTED DONE', ...arguments) }),
'-\\*': {
handler: function(){
console.log('-\\*:', ...arguments) } },
// these aliases will not get shown... // these aliases will not get shown...
// dead-end alias... // dead-end alias...