Alex A. Naanou feae367b57 bumped version...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2020-07-29 21:11:49 +03:00
2020-07-29 21:09:50 +03:00
2020-06-13 23:09:14 +03:00
2020-07-29 21:11:49 +03:00
2020-07-29 21:09:50 +03:00
2020-07-29 17:59:03 +03:00

argv.js

Simple argv parser

Motivation

I needed a new argv parser for a quick and dirty project I was working on and evaluating and selecting the proper existing parser and then learning its API, quirks and adapting the architecture to it seemed to be more complicated, require more effort and far less fun than putting together a trivial parser myself in a couple of hours.
This code is an evolution of that parser.

Features

  • Simple
  • Supports both the option (a-la ls) and command (a-la git) paradigms
  • Nestable
    parsers can be nested as option/command handlers defining independent nested contexts
  • Option expansion
    -abc expands to -a -b -c if -abc is not defined
  • Option/command value passing
    implicit -a 123 (requires definition or manual handling) or explicit -a=123
  • Environment variable option/command value defaults
  • Option/command value conversion
  • Option/command value collection
  • Multiple option prefix support
  • Reasonable defaults:
    • -help generate and print help
    • -version print version
    • -quiet suppress printing
    • - stop argument processing
  • Extensible:
    • Hooks for dynamic option/command handling
    • Customizable error and stop condition handling

Planned Features

  • Run <command>-<sub-command> scripts
  • Option doc grouping (???)

Contents

Installation

$ npm install ig-argv

Basic usage

Create a script and make it runnable

$ touch script.js
$ chmod +x script.js

Now for the code

#!/usr/bin/env node

// compatible with both node's and RequireJS' require(..)
var argv = require('ig-argv')

var parser = argv.Parser({
		// option definitions...
		// ...
	})
	.then(function(){
		// things to do after the options are handled...
		// ...
	})

// run the parser...
__filename == require.main
	&& parser(process.argv)

Now let us populate the option definitions:

var parser = argv.Parser({
		// doc sections...
		varsion: '0.0.1',
		doc: 'Example script options',
		author: 'John Smith <j.smith@some-mail.com>',
		footer: 'Written by $AUTHOR ($VERSION / $LICENSE).',
		license: 'BSD-3-Clause',


		// Basic options
		//
		// These if encountered simply asign a value to an attribute on the 
		// parsed object...
		//
		// If no value is given true is asigned to indicate that the 
		// option/command is present in the commandline.

		'-bool': {
			doc: 'if given set .bool to true' },


		// option with a value...
		'-value': {
			doc: 'set .x to X',

			// 'X' (VALUE) is used for -help while 'x' (key) is where the 
			// value will be written...
			arg: 'X | x',

			// the value is optional by default but we can make it required...
			valueRequired: true,
		},


		// setup an alias -r -> -required
		'-r': '-required',

		// a required option...
		'-required': {
			doc: 'set .required_option_given to true'

			// NOTE: we can omit the VALUE part to not require a value...
			// NOTE: of no attr is specified in arg option name is used.
			arg: '| required_option_given',

			required: true,

			// keep this near the top of the options list in -help...
			priority: 80,
		},


		'-int': {
			doc: 'pass an integer value',

			// NOTE: if not key is given the VALUE name is used as a key, so the 
			// 		value here is assigned to .INT...
			arg: 'INT',

			// convert the input value to int...
			type: 'int',
		},
		

		'-default': {
			doc: 'option with default value',
			arg: 'VALUE | default',

			default: 'some value',
		},


		'-home': {
			doc: 'set home path',
			arg: 'HOME | home',

			// get the default value from the environment variable $HOME...
			env: 'HOME',
		},

		
		// collecting values...
		'-p': '-push',
		'-push': {
			doc: 'push elements to a .list',
			arg: 'ELEM | list',

			// this will add each argument to a -push option to a list...
			collect: 'list',
		},


		// Command...
		//
		// The only difference between an option and a command is
		// the prefix ('-' vs. '@') that determines how it is parsed,
		// otherwise they are identical and everything above applies here 
		// too...
		'@command': {
			// ...
		},

		// Since options and commands are identical aliases from one to the 
		// other to commands are also supported...
		'-c': '@command',


		// Active options/commnads 
		//
		// These define .handler's which are executed when the option is 
		// encountered by the parser...

		'-active': {
			doc: 'basic active option',
			handler: function(args, key, value){
				// ...
			} },

		// a shorthand active option...
		// NOTE: this is recomended only for quick and dirty mashups and not
		//		for production code...
		'-s': '-shorthand-active',
		'-shorthand-active': function(args, key, value){
			// ...
		},


		// Nested parsers...
		//
		'@nested': argv.Parser({
				// ...
			}).then(function(){
				// ...
			}),
	})

This will create a parser that supports the following:

$ ./script.js --help 

$ ./script.js --value 321

$ ./script.js --value=321

$ ./script.js command

$ ./script.js nested -h

$ ./script.js -fb

Error reporting

XXX

XXX add subsections by task

XXX

XXX might be a good idea to split out the rest to a INDETAIL.md or similar...

Configuration

Parser(<spec>)
	-> <parser>

The <spec> object is "merged" into the <parser> instance overriding or extending it's API/data.

The <parser> expects/handles the following data in the <spec> object:

  • the configuration attributes and methods
    Attributes and methods used to configure, modify, extend or overload parser functionality.

    Note that these attributes are the same attributes inherited by <parser> and are simply merged into the new instance created by Parser(..), thus there are no restrictions on what attributes/methods can be overloaded or extended in this way, but care must be taken when overloading elements that were not designed to be overloaded.

  • option/command definitions
    The keys for these are prefixed either by "-" for options or by "@" for commands and are either objects, functions or parser instances.

    The only difference between an option and a command is that the former are passed to the script with a "-" or "--" prefix (by default) and the later are passed by name without prefixes.

    In all other regards options and commands are the same.

  • option/command aliases
    An alias is an option/command key with a string value.
    That value references a different option or command, i.e. is an option/command name.

    Looping (referencing the original alias) or dead-end (referencing non-existent options) aliases are ignored.

Option/command configuration

<option>.handler(..)

Option handler.

'-option': {
	handler: function(opts, key, value){
		// handle the option...
		// ...
	},
},

or a shorthand:

'-option': function(opts, key, value){
	// handle the option...
	// ...
},

The handler gets called if the option is given or if it was not explicitly given but has a default value set.

opts contains the mutable list of arguments passed to the script starting just after the currently handled option/command. If the handler needs to handle it's own arguments it can modify this list in place and the parser will continue from the resulting state.

One use-case for this would be and option handler that needs to handle it's arguments in a custom manner, for example for handling multiple arguments.

key is the actual normalized ([<prefix-char>]<name-str>) option/command triggering the .handler(..).

This can be useful to identify the actual option triggering the handler when using aliases, if a single handler is used for multiple options, or when it is needed to handle a specific prefix differently (a-la find's syntax with +option and -option having different semantics).

value gets the value passed to the option.

A value can be passed either explicitly passed (via = syntax), implicitly parsed from the argv via the <option>.arg definition or is undefined otherwise.

A handler can return one of the THEN, STOP or ParserError instance to control further parsing and/or execution. (See: THEN / STOP for more info.)

<option>.doc

Option/command documentation string used in -help.

If this is set to false the option will be hidden from -help.

<option>.priority

Option/command priority in the -help.

Can be a positive or negative number or undefined.

Ordering is as follows:

  • options in descending positive .priority,
  • options with undefined .priority in order of definition,
  • options in descending negative .priority.

Note that options and commands are grouped separately.

The built-in options -help, -version and -quiet have a priority of 99 so that they appear the the top of the -help list.

Any option defining .required and not defining an explicit .priority will be sorted via <parser>.requiredOptionPriority (80 by default).

<option>.arg

Option/command argument definition.

arg: '<arg-name>'
arg: '<arg-name> | <key>'

If defined and no explicit value is passed to the option command (via =) then the parser will consume the directly next non-option if present in argv as a value, passing it to the <option>.type handler, if defined, then the <option>.handler(..), if defined, or setting it to <key> otherwise.

Sets the option/command argument name given in -help for the option and the key where the value will be written.

The <key> is not used if <option>.handler(..) is defined.

<option>.type

Option/command argument type definition.

The given type handler will be used to convert the option value before it is passed to the handler or set to the given <key>.

Supported types:

  • "string" (default behavior)
  • "bool"
  • "int"
  • "float"
  • "number"
  • "date" expects a new Date(..) compatible date string
  • "list" expects a ","-separated value, split and written as an Array object

Type handlers are defined in Parser.typeHandlers or can be overwritten by <spec>.typeHandlers.

If not set values are written as strings.

Defining a new global type handler:

// check if a value is email-compatible...
argv.Parser.typeHandlers.email = function(value, ...options){
	if(!/[a-zA-Z][a-zA-Z.-]*@[a-zA-Z.-]+/.test(value)){
		throw new TypeRrror('email: format error:', value) }
	return value }

Defining a local to parser instance type handler:

var parser = new Parser({
	// Note that inheriting from the global type handlers is required 
	// only if one needs to use the global types, otherwise just setting
	// a bare object is enough...
	typeHandlers: Object.assign(Object.create(Parser.typeHandlers), {
		email: function(value, ...options){
			// ...
		},
		// ...
	}),

	// ...
})

<option>.collect

Option value collection mode.

The given handler will be used to collect values passed to multiple occurrences of the option and write the result to <key>.

Supported collection modes:

  • "list" group values into an Array object
  • "set" group values into a Set object
  • "string" concatenate values into a string.
    This also supports an optional separator, for example "string|\t" will collect values into a string joining them with a tab (i.e. "\t").
    Default separator is: " "
  • "toggle" toggle option value (bool).
    Note that the actual value assigned to an option is ignored here and can be omitted.

Type handlers are defined in Parser.valueCollectors or can be overwritten by <spec>.valueCollectors.

<option>.collect can be used in conjunction with <option>.type to both convert and collect values.

If not set, each subsequent option will overwrite the previously set value.

Defining a global value collector:

// '+' prefixed flags will add values to set while '-' prefixed flag will 
// remove value from set...
argv.Parser.valueCollectors.Set = function(value, current, key){ 
	current = current || new Set()
	return key[0] != '-' ?
		current.add(value) 
		: (cur.delete(value), current) }

Defining handlers local to a parser instance handler is the same as for type handlers above.

<option>.env

Determines the environment variable to be used as the default value for option/command, if set.

If this is set, the corresponding environment variable is non-zero and <option>.handler(..) is defined, the handler will be called regardless of weather the option was given by the user or not.

<option>.default

Sets the default value for option/command's value.

If this is set to a value other than undefined and <option>.handler(..) is defined, the handler will be called regardless of weather the option was given by the user or not.

<option>.required

Sets weather the parser should complain/err if option/command is not given.

Note that this also implicitly prioritizes the option, for more info see: <option>.priority.

<option>.valueRequired

Sets weather the parser should complain/err if option/value value is not given.

Built-in options

- / --

Stop processing further options.

This can be used to terminate nested parsers or to stop option processing in the root parser to handle the rest of the options in <parser>.then(..), for example.

-* / @*

Handle options/commands for which no definition is found.

By default -* will print an "unhandled option/command" error and terminate.

By default @* is an alias to -*.

-v / --version

This will output the value of .version and exit.

-q / --quiet

This will turn quiet mode on.

In quiet mode <parser>.print(..) will not print anything.

Passing --help or --version will disable quiet mode and print normally.

Note that this will only set <parser>.quiet to true and disable output of <parser>.print(..), any user code needs to either also use <parser>.print(..) for output (not always practical) or respect <parser>.quiet.

-h / --help

By default -help will output in the following format:

<usage>

<doc>

Options:
	<option-spec> <option-val>		
				- <option-doc>
				  (<opt-required>, <opt-default>, <opt-env>)
	...

Dynamic options:
	...

Commands:
	...

Examples:
	...

<footer>

All sections are optional and will not be rendered if they contain no data.

Value placeholders

All documentation strings can contain special placeholders that will get replaced with appropriate values when rendering help.

  • $SCRIPTNAME replaced with the value of .scriptName,
  • $VERSION replaced with .version,
  • $LICENSE replaced with .license.
Automatically defined values

These values are set by the parser just before parsing starts:

  • .script - full script path, usually this is the value of argv[0],
  • .scriptName - base name of the script,
  • .scriptPath - path of the script.

These will be overwritten when the parser is called.

<parser>.doc

Script documentation.

<spec>.doc = <string> | <function>

Default value: undefined

<parser>.usage

Basic usage hint.

<spec>.usage = <string> | <function> | undefined

Default value: "$SCRIPTNAME [OPTIONS]"

<parser>.version

Version number.

<spec>.usage = <string> | <function> | undefined

If this is not defined -version will print "0.0.0".

Default value: undefined

<parser>.license

Short license information.

<spec>.usage = <string> | <function> | undefined

Default value: undefined

<parser>.examples
<spec>.usage = <string> | <list> | <function> | undefined

Example list format:

[
	[<example-code>, <example-doc>, ...],
	...
]

Default value: undefined

<parser>.footer

Additional information.

<spec>.footer = <string> | <function> | undefined

Default value: undefined

More control over help...

For more info on help formatting see <parser>.help* attributes in the source.

Nested parsers

An option/command handler can be a parser instance.

From the point of view of the nested parser nothing is different it gets passed the remaining list of arguments and handles it on it's own.

The containing parser treats the nested parser just like any normal handler with it's attributes and API.

Note that if the nested parser consumes the rest of the arguments, the containing parser is left with an empty list and it will stop parsing and return normally.

A way to explicitly stop the nested parser processing at a specific point in the argument list is to pass it a - argument at that point.

For example:

$ script -a nested -b -c - -x -y -z

Here script will handle -a then delegate to nested which in turn will consume -b, -c and on - return, rest of the arguments are again handled by script.

This is similar to the way programming languages handle passing arguments to functions, for example in Lisp this is similar to:

(script a (nested b c) x y z)

And in C-like-call-syntax languages like C/Python/JavaScript/... this would (a bit less cleanly) be:

script(a, nested(b, c), x, y, z)

The difference here is that nested has control over what it handles, and depending on its definition, can either override the default - option as well as stop handling arguments at any point it chooses (similar to words in stack languages like Fort or Factor).

Components and API

THEN / STOP

Values that if returned by option/command handlers can control the parse flow.

  • THEN Stop parsing and call <parser>.then(..) callbacks.
  • STOP Stop parsing and call <parser>.stop(..) callbacks, skipping <parser>.then(..).

THEN is useful when we want to stop option processing and trigger the post-parse stage (i.e. calling <parser>.then(..)) for example to pass the rest of the options to some other command.

STOP is used for options like -help when no post-parsing is needed.

ParserError(..)

A base error constructor.

If an instance of ParseError is thrown or returned by the handler parsing is stopped, <parsing>.error(..) is called and then the parser will exit with an error (see: <parser>.handleErrorExit(..)).

The following error constructors are also defined:

  • ParserTypeError(..)
  • ParserValueError(..)

Note that ParserError instances can be both returned or thrown.

Parser(..)

Construct a parser instance

Parser(<spec>)
	-> <parser>

See <parser>(..) for more info.

<parser>.then(..)

Add callback to then "event".

<parser>.then(<callback>)
	-> <parser>
callback(<unhandled>, <root-value>, <rest>)
	-> <obj>

then is triggered when parsing is done or stopped from an option handler by returning THEN.

<parser>.stop(..)

Add callback to stop "event".

<parser>.stop(<callback>)
	-> <parser>
callback(<arg>, <rest>)
	-> <obj>

stop is triggered when a handler returns STOP.

<parser>.error(..)

Add callback to error "event".

<parser>.error(<callback>)
	-> <parser>
callback(<reason>, <arg>, <rest>)
	-> <obj>

error is triggered when a handler returns ERROR.

<parser>.off(..)

Remove callback from "event".

	<parser>.off(<event>, <callback>)
		-> <parser>

<parser>(..)

Execute the parser instance.

Run the parser on process.argv

<parser>()
	-> <result>

Explicitly pass a list of arguments where <argv>[0] is treated as the script path.

<parser>(<argv>)
	-> <result>

Explicitly pass both a list of arguments and script path.

<parser>(<argv>, <main>)
	-> <result>

If <main> is present in <argv> all the arguments before it will be ignored, otherwise the whole list is processed as if <main> was its head.

Advanced parser API

<parser>.print(..) / <parser>.printError(..)

Handle how <parser> prints things.

<parser>.print(..) and <parser>.printError(..) are very similar but handle different cases, similar to console.log(..) and console.error(..)

<parser>.print(...)
	-> <parser>

<parser>.printError(...)
	-> <parser>
<parser>.printError(<error>, ...)
	-> <error>

Both support callback binding:

<parser>.print(<func>)
	-> <parser>

<parser>.printError(<func>)
	-> <parser>

Both <parser>.print(..) and <parser>.printError(..) can safely be overloaded if the callback feature is not going to be used by the user the print callbacks are not used internally.

For full callback API see: extra.afterCallback(..) in argv.js.

<parser>.handlerDefault(..)

Called when <option>.handler(..) is not defined.

By default this sets option values on the parsed object.

<parser>.handleArgumentValue(..)

Handle argument value conversion.

By default this handles the <option>.type mechanics.

If this is set to false values will be set as-is.

<parser>.handleErrorExit(..)

Handle exit on error.

By default this will call process.exit(1) for the root parser and does nothing for nested parsers.

If set to false the parser will simply return like any normal function.

<parser>.handle(..)

Manually trigger <arg> handling.

<parser>.handle(<arg>, <rest>, <key>, <value>)
	-> <res>

This is intended to be used for delegating handling from one handler to another. Note that this does not handle errors or other protocols handled by <parser>(..), this only calls the <arg> handler (or if it was not defined the default handler) so it is not recommended for this to be called from outside an option handler method/function.

This is not intended for overloading.

<parser>.setHandlerValue(..)

Set handler value manually, this uses <handler>.arg and if not set <key> to write <value> on the parsed object.

<parser>.setHandlerValue(<handler>, <key>, <value>)
	-> <parser>

This is useful when extending argv.js, for client code values can be set directly.

This is not intended for overloading.

More...

For more info see the source.

License

BSD 3-Clause License

Copyright (c) 2016-2020, Alex A. Naanou,
All rights reserved.

Languages
JavaScript 100%