From dd74fb14448033b6fd40c3d6a95d3a653209bb1a Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Sat, 1 Aug 2020 19:25:31 +0300 Subject: [PATCH] reworked error handling... Signed-off-by: Alex A. Naanou --- argv.js | 225 ++++++++++++++++++++++++-------------------- examples/bare.js | 6 +- examples/options.js | 7 +- package.json | 2 +- test.js | 17 +++- 5 files changed, 148 insertions(+), 109 deletions(-) diff --git a/argv.js b/argv.js index d04615b..0ae0f53 100644 --- a/argv.js +++ b/argv.js @@ -49,7 +49,14 @@ module.ParserError = // NOTE: I do not get why JavaScript's Error implements this // statically... get name(){ - return this.constructor.name }, }) + return this.constructor.name }, + + // NOTE: msg is handled by Error(..) + __init__: function(msg, arg, rest){ + this.arg = arg + this.rest = rest + }, + }) module.ParserTypeError = object.Constructor('ParserTypeError', module.ParserError, {}) @@ -712,7 +719,7 @@ object.Constructor('Parser', { //section_doc: ..., handler: function(_, key){ throw module.ParserError( - `Unknown ${key.startsWith('-') ? 'option:' : 'command:'} ${ key }`) } }, + `Unknown ${key.startsWith('-') ? 'option:' : 'command:'} $ARG`) } }, '@*': '-*', @@ -731,11 +738,20 @@ object.Constructor('Parser', { // .printError(error, ...) // -> error // + // NOTE: this handles $ARG in error.message. printError: afterCallback('print_error', null, function(...args){ if(args[0] instanceof module.ParserError){ + var err = args[0] console.error( - this.scriptName+':', args[0].name+':', args[0].message, ...args.slice(1)) - return args[0] } + this.scriptName+':', + err.name+':', + err.message + // XXX this should be done in ParserError but there + // we have to fight Error's implementation of + // .message and its use... + .replace(/\$ARG/, err.arg), + ...args.slice(1)) + return err } console.error(this.scriptName+': Error:', ...args) return this }), @@ -820,7 +836,8 @@ object.Constructor('Parser', { // error... handleErrorExit: function(arg, reason){ typeof(process) != 'unhandled' - && process.exit(1) }, + && process.exit(1) + return this }, // Post parsing callbacks... @@ -859,11 +876,6 @@ object.Constructor('Parser', { // all the parse data... // NOTE: this (i.e. parser) can be used as a nested command/option // handler... - // NOTE: we can't throw ParserError(..) from outside the try/catch - // block in here as it will not be handled locally... - // XXX this may need a rethink -- should the try/catch block - // include the rest of the cases where reportError(..) is - // used or be on a level above runHandler(..) __call__: function(context, argv, main, root_value){ var parsed = Object.create(this) var opt_pattern = parsed.optionInputPattern @@ -896,17 +908,14 @@ object.Constructor('Parser', { // helpers... var handleError = function(reason, arg, rest){ + arg = arg || reason.arg + rest = rest || reason.rest reason = reason instanceof Error ? [reason.name, reason.message].join(': ') : reason parsed.error(reason, arg, rest) parsed.handleErrorExit && parsed.handleErrorExit(arg, reason) } - var reportError = function(message, arg, rest){ - message = message - .replace(/\$ARG/g, arg) - parsed.printError(module.ParserError(message)) - return handleError(message, arg, rest) } var runHandler = function(handler, arg, rest){ var [arg, value] = arg instanceof Array ? arg @@ -927,26 +936,24 @@ object.Constructor('Parser', { : value // required value check... if(handler.valueRequired && value == null){ - return reportError('Value missing: $ARG=?', arg, rest) } + throw module.ParserValueError('Value missing: ${ arg }=?') } + // run handler... try { - // run handler... var res = parsed.handle(handler, rest, arg, value) + // update error object with current context's arg and rest... } catch(err){ - // re-throw the error... - // NOTE: do not like that this can mask the location of - // the original error. - if(!(err instanceof module.ParserError)){ - throw err } - // XXX should we report an error here??? - parsed.printError(err) - res = err } + if(err instanceof module.ParserError){ + err.arg = err.arg || arg + err.rest = err.rest || rest } + throw err } // NOTE: we also need to handle the errors passed to us from // nested parsers... res === module.STOP && parsed.stop(arg, rest) + // XXX revise -- do we need to re-handle errors??? res instanceof module.ParserError && handleError(res, arg, rest) return res } @@ -969,88 +976,100 @@ object.Constructor('Parser', { rest.splice(0, 0, ...r) return [ a, parsed.handler(a)[1] ] } - // parse/interpret the arguments and call handlers... - var values = new Set() - var seen = new Set() - var unhandled = parsed.unhandled = [] - while(rest.length > 0){ - var arg = rest.shift() - // non-string stuff in arg list... - if(typeof(arg) != typeof('str')){ - unhandled.push(arg) - continue } - // 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, dfl] = opt_pattern.test(arg) ? - ['opt', OPTION_PREFIX +'*'] - : parsed.isCommand(arg) ? - ['cmd', COMMAND_PREFIX +'*'] - : ['unhandled'] - // options / commands... - if(type != 'unhandled'){ - // quote '-*' / '@*'... - arg = arg.replace(/^(.)\*$/, '$1\\*') - // get handler... - var handler = parsed.handler(arg)[1] - // handle merged options... - || (type == 'opt' - && parsed.splitOptions - // NOTE: we set arg here... - && ([arg, handler] = splitArgs(arg, rest))[1] ) - // dynamic or error... - || parsed.handler(dfl)[1] - // no handler found and '-*' or '@*' not defined... - if(handler == null){ - return reportError(`Unknown ${ type == 'opt' ? 'option' : 'command:' } $ARG`, arg, rest) } + try{ + // parse/interpret the arguments and call handlers... + var values = new Set() + var seen = new Set() + var unhandled = parsed.unhandled = [] + while(rest.length > 0){ + var arg = rest.shift() + // non-string stuff in arg list... + if(typeof(arg) != typeof('str')){ + unhandled.push(arg) + continue } + // 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, dfl] = opt_pattern.test(arg) ? + ['opt', OPTION_PREFIX +'*'] + : parsed.isCommand(arg) ? + ['cmd', COMMAND_PREFIX +'*'] + : ['unhandled'] + // options / commands... + if(type != 'unhandled'){ + // quote '-*' / '@*'... + arg = arg.replace(/^(.)\*$/, '$1\\*') + // get handler... + var handler = parsed.handler(arg)[1] + // handle merged options... + || (type == 'opt' + && parsed.splitOptions + // NOTE: we set arg here... + && ([arg, handler] = splitArgs(arg, rest))[1] ) + // dynamic or error... + || parsed.handler(dfl)[1] + // no handler found and '-*' or '@*' not defined... + if(handler == null){ + throw ParserError(`Unknown ${ type == 'opt' ? 'option' : 'command:' } $ARG`, arg) } - // mark handler... - ;(handler.env || 'default' in handler) - && values.add(handler) - seen.add(handler) + // mark handler... + ;(handler.env || 'default' in handler) + && values.add(handler) + seen.add(handler) - var res = runHandler(handler, arg, rest) + var res = runHandler(handler, arg, rest) - // handle stop conditions... - if(res === module.STOP - || res instanceof module.ParserError){ - return nested ? - res - : parsed } - // finish arg processing now... - if(res === module.THEN){ - break } - continue } - // unhandled... - unhandled.push(arg) } - // call value handlers with .env or .default values that were - // not explicitly called yet... - // XXX an error, THEN or STOP returned from runHandler(..) in here will - // not stop execution -- should it??? - // XXX a ParserError thrown here will not be handled correctly - // in the root parser... - parsed.optionsWithValue() - .forEach(function([k, a, d, handler]){ - values.has(handler) - || (((typeof(process) != 'undefined' - && handler.env in process.env) - || handler.default) - && seen.add(handler) - // XXX should we handle STOP / ParserError here??? - && runHandler(handler, - [k[0], handler.default], - rest)) }) + // handle stop conditions... + if(res === module.STOP + || res instanceof module.ParserError){ + return nested ? + res + : parsed } + // finish arg processing now... + if(res === module.THEN){ + break } + continue } + // unhandled... + unhandled.push(arg) } + // call value handlers with .env or .default values that were + // not explicitly called yet... + // XXX THEN or STOP returned from runHandler(..) in here will + // not stop execution -- should it??? + parsed.optionsWithValue() + .forEach(function([k, a, d, handler]){ + values.has(handler) + || (((typeof(process) != 'undefined' + && handler.env in process.env) + || handler.default) + && seen.add(handler) + && runHandler(handler, + [k[0], handler.default], + rest)) }) - // check and report required options... - var missing = parsed - .requiredOptions() - .filter(function([k, a, d, h]){ - return !seen.has(h) }) - .map(function([k, a, d, h]){ - return k.pop() }) - if(missing.length > 0){ - reportError('required but missing: $ARG', missing.join(', '), rest) - return parsed } + // check and report required options... + var missing = parsed + .requiredOptions() + .filter(function([k, a, d, h]){ + return !seen.has(h) }) + .map(function([k, a, d, h]){ + return k.pop() }) + if(missing.length > 0){ + throw module.ParserError(`required but missing: $ARG`, missing.join(', ')) } + + // handle ParserError... + } catch(err){ + // re-throw the error... + if(!(err instanceof module.ParserError)){ + throw err } + + // report local errors... + // NOTE: non-local errors are threaded as return values... + parsed.printError(err) + handleError(err, err.arg, rest) + + return nested ? + err + : parsed } // handle root value... root_value = diff --git a/examples/bare.js b/examples/bare.js index 17f2b2b..9ff5e67 100644 --- a/examples/bare.js +++ b/examples/bare.js @@ -3,7 +3,9 @@ // compatible with both node's and RequireJS' require(..) var argv = require('../argv') -var parser = argv.Parser({ +var parser = +exports.parser = +argv.Parser({ // option definitions... // ... }) @@ -13,7 +15,7 @@ var parser = argv.Parser({ }) // run the parser... -__filename == require.main.filename +__filename == (require.main || {}).filename && parser(process.argv) // vim:set ts=4 sw=4 spell : diff --git a/examples/options.js b/examples/options.js index 90f643b..4d41d34 100644 --- a/examples/options.js +++ b/examples/options.js @@ -2,7 +2,9 @@ var argv = require('../argv') -var parser = argv.Parser({ +var parser = +exports.parser = +argv.Parser({ doc: 'Example script options', // to make things consistent we'll take the version from package.json @@ -71,6 +73,7 @@ var parser = argv.Parser({ }, + // XXX this is misbehaving -- setting true instead of $HOME '-home': { doc: 'set home path', arg: 'HOME | home', @@ -145,7 +148,7 @@ var parser = argv.Parser({ // run the parser... -__filename == require.main.filename +__filename == (require.main || {}).filename && parser() // vim:set ts=4 sw=4 spell : diff --git a/package.json b/package.json index 133678d..38c4b40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ig-argv", - "version": "2.8.1", + "version": "2.9.0", "description": "simple argv parser", "main": "argv.js", "scripts": { diff --git a/test.js b/test.js index ea6aa4a..2fc7a31 100644 --- a/test.js +++ b/test.js @@ -14,6 +14,11 @@ var object = require('ig-object') var argv = require('./argv') +var bare = module.bare = require('./examples/bare').parser +var options = module.options = require('./examples/options').parser + + + //--------------------------------------------------------------------- @@ -90,7 +95,7 @@ argv.Parser({ '-error': { doc: 'throw an error', handler: function(){ - throw argv.ParserError('error') }}, + throw argv.ParserError('error: $ARG') }}, '-passive-error': { doc: 'throw an error', handler: function(){ @@ -148,6 +153,16 @@ argv.Parser({ '-k': '-l', '-l': '-m', '-m': '-k', + + + '@bare': bare, + '@opts': options, + + + // collision test... + // NOTE: values of these will shadow the API... + '@options': {}, + '-handler': {}, }) //.print(function(...args){ // console.log('----\n', ...args)