cleanup, testing and experimenting....

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2020-06-16 18:31:58 +03:00
parent 679437a822
commit 0bb9c7b644
2 changed files with 150 additions and 109 deletions

204
argv.js
View File

@ -16,6 +16,14 @@ module.OPTION_PATTERN = /^--?/
module.COMMAND_PATTERN = /^[a-zA-Z]/ module.COMMAND_PATTERN = /^[a-zA-Z]/
module.STOP =
{doc: 'stop option processing'}
module.ERROR =
{doc: 'option processing error'}
//--------------------------------------------------------------------- //---------------------------------------------------------------------
Object.defineProperty(String.prototype, 'raw', { Object.defineProperty(String.prototype, 'raw', {
@ -23,6 +31,19 @@ Object.defineProperty(String.prototype, 'raw', {
return this.replace(/\x1b\[..?m/g, '') }, }) return this.replace(/\x1b\[..?m/g, '') }, })
var afterCallback = function(name){
var attr = '__after_'+ name
return function(func){
(this[attr] = this[attr] || []).push(func)
return this } }
var afterCallbackCall = function(name, context, ...args){
return (context['__after_'+ name] || [])
.forEach(function(func){
func.call(context, ...args) }) }
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// basic argv parser... // basic argv parser...
@ -227,16 +248,16 @@ function(spec){
rest: argv, rest: argv,
} }
var other = [] var unhandled = []
while(argv.length > 0){ while(argv.length > 0){
var arg = argv.shift() var arg = argv.shift()
var type = opt_pattern.test(arg) ? var type = opt_pattern.test(arg) ?
'opt' 'opt'
: spec.__iscommand__(arg) ? : spec.__iscommand__(arg) ?
'cmd' 'cmd'
: 'other' : 'unhandled'
// options / commands... // options / commands...
if(type != 'other'){ if(type != 'unhandled'){
// get handler... // get handler...
var handler = spec.__gethandler__(arg).pop() var handler = spec.__gethandler__(arg).pop()
|| spec.__unknown__ || spec.__unknown__
@ -254,9 +275,9 @@ function(spec){
arg, arg,
argv) argv)
continue } continue }
// other... // unhandled...
other.push(arg) } unhandled.push(arg) }
return other } } return unhandled } }
@ -287,12 +308,14 @@ object.Constructor('Parser', {
// Handler API... // Handler API...
// XXX should these be .getOptions(..) / .getCommands(..) ??? //
// Format: // Format:
// [ // [
// [<keys>, <arg>, <doc>, <handler>], // [<keys>, <arg>, <doc>, <handler>],
// ... // ...
// ] // ]
//
// XXX should these be .getOptions(..) / .getCommands(..) ???
options: function(...prefix){ options: function(...prefix){
var that = this var that = this
prefix = prefix.length == 0 ? prefix = prefix.length == 0 ?
@ -306,9 +329,10 @@ object.Constructor('Parser', {
if(!opt.startsWith(prefix)){ if(!opt.startsWith(prefix)){
return } return }
var [k, h] = that.getHandler(opt) var [k, h] = that.getHandler(opt)
handlers[k] ? h !== undefined
handlers[k][0].push(opt) && (handlers[k] ?
: (handlers[k] = [[opt], h.arg, h.doc || k.slice(1), h]) }) handlers[k][0].push(opt)
: (handlers[k] = [ [opt], h.arg, h.doc || k.slice(1), h ])) })
return Object.values(handlers) }) return Object.values(handlers) })
.flat(1) .flat(1)
.map(function(e, i){ return [e, i] }) .map(function(e, i){ return [e, i] })
@ -346,17 +370,14 @@ object.Constructor('Parser', {
// doc stuff... // doc stuff...
// XXX revise naming...
helpColumnOffset: 3, helpColumnOffset: 3,
helpColumnPrefix: '- ', helpColumnPrefix: '- ',
// XXX these can be functions...
doc: undefined,
usage: '$SCRIPTNAME [OPTIONS]', usage: '$SCRIPTNAME [OPTIONS]',
doc: undefined,
examples: undefined, examples: undefined,
footer: undefined, footer: undefined,
// XXX better name...
alignColumns: function(a, b, ...rest){ alignColumns: function(a, b, ...rest){
var opts_width = this.helpColumnOffset || 4 var opts_width = this.helpColumnOffset || 4
var prefix = this.helpColumnPrefix || '' var prefix = this.helpColumnPrefix || ''
@ -368,86 +389,80 @@ object.Constructor('Parser', {
: [a] }, : [a] },
// Builtin options/commands... // Builtin options/commands...
// XXX do we need to encapsulate this???
// ...on one hand encapsulation is cleaner but on the other it:
// 1) splits the option spec and parser config
// 2) forces the use of two mechanisms for option spec and
// parser config...
// XXX need these to be sortable/groupable -- keep help at top...
'-h': '-help', '-h': '-help',
'-help': { '-help': {
doc: 'print this message and exit.', doc: 'print this message and exit.',
priority: 99, priority: 99,
// XXX argv is first for uniformity with .__call__(..) -- need
// the two to be interchangeable...
// ...an alternative would keep it last, but this feels more fragile...
handler: function(argv, key, value){ handler: function(argv, key, value){
var that = this var that = this
var x
console.log([ var expandVars = function(str){
`Usage: ${ return str
typeof(this.usage) == 'function' ? .replace(/\$SCRIPTNAME/g, that.scriptName) }
this.usage(this) var getValue = function(name){
: this.usage }`, return that[name] ?
// doc... ['', typeof(that[name]) == 'function' ?
...(this.doc ? that[name]()
['', typeof(this.doc) == 'function' ? : that[name]]
this.__doc__() : [] }
: this.doc] var section = function(title, items){
: []), items = items instanceof Array ? items : [items]
// options... return items.length > 0 ?
'', ['', title +':', ...items]
'Options:', : [] }
...(this.options()
.map(function([opts, arg, doc]){ console.log(
return [opts.join(' | -') +' '+ (arg || ''), doc] })), expandVars([
// commands... `Usage: ${ getValue('usage').join('') }`,
...(((x = this.commands()) && x.length > 0) ? // doc (optional)...
['', 'Commands:', ...getValue('doc'),
...x.map(function([cmd, _, doc]){ // options...
return [ ...section('Options',
cmd this.options()
.map(function(cmd){ .map(function([opts, arg, doc]){
return cmd.slice(1)}) return [ opts.join(' | -') +' '+ (arg || ''), doc] })),
.join(' | '), // commands (optional)...
doc] })] ...section('Commands',
: []), this.commands()
// examples... .map(function([cmd, _, doc]){
...(this.examples ? return [
['', 'Examples:', ...( cmd
.map(function(cmd){ return cmd.slice(1)})
.join(' | '),
doc] })),
// examples (optional)...
...section('Examples',
this.examples instanceof Array ? this.examples instanceof Array ?
this.examples this.examples
.map(function(e){ .map(function(e){
return e instanceof Array ? e : [e] }) return e instanceof Array ? e : [e] })
: this.examples == 'function' ? : getValue('examples') ),
this.examples(this) // footer (optional)...
: this.examples )] ...getValue('footer') ]
: []), // expand/align columns...
// footer... .map(function(e){
...(this.footer? return e instanceof Array ?
['', typeof(this.footer) == 'function' ? // NOTE: we need to expandVars(..) here so as to
this.footer(this) // be able to calculate actual widths...
: this.footer] that.alignColumns(...e.map(expandVars))
: []) ] .map(function(s){ return '\t'+ s })
.map(function(e){ : e })
return e instanceof Array ? .flat()
that.alignColumns(...e .join('\n')))
.map(function(s){
return s.replace(/\$SCRIPTNAME/g, that.scriptName) }))
// indent lists...
.map(function(s){
return '\t'+ s })
: e })
.flat()
.join('\n')
.replace(/\$SCRIPTNAME/g, this.scriptName))
// XXX should we explicitly exit here or in the runner??? // XXX should we explicitly exit here or in the runner???
process.exit() }}, return module.STOP }},
unknownOption: function(key){
unknownOption: function(_, key){
console.error('Unknown option:', key) console.error('Unknown option:', key)
process.exit(1) }, return module.ERROR },
// post parsing callbacks...
then: afterCallback('parsing'),
stop: afterCallback('stop'),
error: afterCallback('error'),
// XXX need to unify this with handler as much as possible to make // XXX need to unify this with handler as much as possible to make
@ -458,6 +473,9 @@ object.Constructor('Parser', {
// - script // - script
// ...these should be either avoided or "inherited" // ...these should be either avoided or "inherited"
__call__: function(context, argv){ __call__: function(context, argv){
var that = this
var nested = false
// nested command handler... // nested command handler...
// XXX the condition is a bit too strong... // XXX the condition is a bit too strong...
if(context instanceof Parser){ if(context instanceof Parser){
@ -465,6 +483,7 @@ object.Constructor('Parser', {
this.script = this.scriptName = this.script = this.scriptName =
context.scriptName +' '+ arguments[2] context.scriptName +' '+ arguments[2]
this.argv = [context.scriptName, this.scriptName, ...argv] this.argv = [context.scriptName, this.scriptName, ...argv]
nested = true
// root parser... // root parser...
} else { } else {
@ -481,16 +500,16 @@ object.Constructor('Parser', {
var opt_pattern = this.optionPattern var opt_pattern = this.optionPattern
var other = [] var unhandled = []
while(argv.length > 0){ while(argv.length > 0){
var arg = argv.shift() var arg = argv.shift()
var type = opt_pattern.test(arg) ? var type = opt_pattern.test(arg) ?
'opt' 'opt'
: this.isCommand(arg) ? : this.isCommand(arg) ?
'cmd' 'cmd'
: 'other' : 'unhandled'
// options / commands... // options / commands...
if(type != 'other'){ if(type != 'unhandled'){
// get handler... // get handler...
var handler = this.getHandler(arg).pop() var handler = this.getHandler(arg).pop()
|| this.unknownOption || this.unknownOption
@ -499,17 +518,28 @@ object.Constructor('Parser', {
argv.shift() argv.shift()
: undefined : undefined
// run handler... // run handler...
;(typeof(handler) == 'function' ? var res = (typeof(handler) == 'function' ?
handler handler
: handler.handler) : handler.handler)
.call(this, .call(this,
argv, argv,
arg, arg,
...(handler.arg ? [value] : [])) ...(handler.arg ? [value] : []))
// handle .STOP / .ERROR
if(res === module.STOP || res === module.ERROR){
afterCallbackCall(
res === module.STOP ? 'stop' : 'error',
this, arg)
return nested ?
res
: this }
continue } continue }
// other... // unhandled...
other.push(arg) } unhandled.push(arg) }
return other },
// post handlers...
afterCallbackCall('parsing', this, unhandled)
return this },
__init__: function(spec){ __init__: function(spec){
Object.assign(this, spec) Object.assign(this, spec)

55
test.js
View File

@ -20,33 +20,44 @@ var argv = require('./argv')
var p = var p =
module.p = module.p =
argv.Parser({ argv.Parser({
'@help': '-help', '@help': '-help',
'-v': '-verbose', '-v': '-verbose',
'-verbose': function(){ '-verbose': function(){
console.log('>>> VERBOSE:', ...arguments) console.log('>>> VERBOSE:', ...arguments)
return 'verbose' return 'verbose'
},
'-c': '@command',
'@cmd': '@command',
'@command': {
priority: -50,
handler: function(){
console.log('>>> COMMAND:', ...arguments)
return 'command'
}, },
},
// XXX this for some reason breaks... '-c': '@command',
//'@test': argv.Parser({ '@cmd': '@command',
//}), '@command': {
priority: -50,
handler: function(){
console.log('>>> COMMAND:', ...arguments)
return 'command'
},
},
'@nested': argv.Parser({ // XXX dead-end alias...
doc: 'nested parser.', '-d': '-dead-end',
}), '@test': argv.Parser({
}) }),
'@nested': argv.Parser({
doc: 'nested parser.',
'@nested': argv.Parser({
doc: 'nested nested parser.',
}),
}),
})
.then(function(){
console.log('DONE') })
.stop(function(){
console.log('STOP') })
.error(function(){
console.log('ERROR') })