commit 596e52f1bc5f2aecf091ffb4911f3ee9e9c6e43e Author: Corey Hart Date: Sun Jul 8 14:34:37 2012 -0700 -Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.lintignore.json b/.lintignore.json new file mode 100644 index 0000000..e71e2ec --- /dev/null +++ b/.lintignore.json @@ -0,0 +1,5 @@ +{ + "ignore": [ + "node_modules/" + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c20e900 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: all test + + +all: lint + +lint: + @node build/lint.js diff --git a/build/lint.js b/build/lint.js new file mode 100644 index 0000000..cc0c567 --- /dev/null +++ b/build/lint.js @@ -0,0 +1 @@ +require( 'nlint' ).render( __dirname + '/../' ); diff --git a/index.js b/index.js new file mode 100644 index 0000000..6e63e59 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require( './lib/argv.js' ); diff --git a/lib/argv.js b/lib/argv.js new file mode 100644 index 0000000..22863eb --- /dev/null +++ b/lib/argv.js @@ -0,0 +1,434 @@ +var PATH = require( 'path' ), + toString = Object.prototype.toString, + rhome = /^\~\//, + rroot = /^\//, + rlistcsv = /^(list|csv)\,[a-z]+$/, + rdash = /^\-/, + rddash = /^\-\-/, + ristarget = /^[^\-]/, + SCRIPT_NAME = ( process.argv[ 1 ] || '' ).split( '/' ).pop(), + helpOption = { + name: 'help', + short: 'h', + type: function(){ return true; }, + description: 'Displays help information about this script', + example: "'" + SCRIPT_NAME + " -h' or '" + SCRIPT_NAME + " --help'", + onset: function( args ) { + self.help( args.mod ); + process.exit( 0 ); + } + }, + self; + +// argv module +module.exports = self = { + + // Default script name + name: SCRIPT_NAME, + + // Default description to the triggered script 'node script' + description: 'Usage: ' + SCRIPT_NAME + ' [options]', + + // Modules + mods: {}, + + // Shorthand options + short: { h: helpOption }, + + // Options + options: { help: helpOption }, + + // List of common types + types: { + + string: function( value ) { + return value.toString(); + }, + + path: function( value ) { + if ( rhome.exec( value ) ) { + value = PATH.normalize( process.env.HOME + '/' + value.replace( rhome, '' ) ); + } + else if ( ! rroot.exec( value ) ) { + value = PATH.normalize( process.cwd() + '/' + value ); + } + + return value; + }, + + 'int': function( value ) { + return parseInt( value, 10 ); + }, + + 'float': function( value ) { + return parseFloat( value, 10 ); + }, + + 'boolean': function( value ) { + return ( value == 'true' || value === '1' ); + }, + + list: function( value, name, options ) { + if ( ! options[ name ] ) { + options[ name ] = []; + } + + options[ name ].push( value ); + return options[ name ]; + }, + + csv: function( value ) { + return value.split( ',' ); + }, + + 'listcsv-combo': function( type ) { + var parts = type.split( ',' ), + primary = parts.shift(), + secondary = parts.shift(); + + return function( value, name, options, args ) { + // Entry is going to be an array + if ( ! options[ name ] ) { + options[ name ] = []; + } + + // Channel to csv or list + if ( primary == 'csv' ) { + value.split( ',' ).forEach(function( val ) { + options[ name ].push( self.types[ secondary ]( val, name, options, args ) ); + }); + } + else { + options[ name ].push( self.types[ secondary ]( value, name, options, args ) ); + } + + return options[ name ]; + }; + } + + }, + + // Creates custom type function + type: function( name, callback ) { + if ( callback === undefined ) { + return self.types[ name ]; + } + else if ( self.isFunction( callback ) ) { + self.types[ name ] = callback; + } + else if ( callback === null && self.types.hasOwnProperty( name ) ) { + delete self.types[ name ]; + } + + return self; + }, + + // Setting version number, and auto setting version option + version: function( v ) { + self.option({ + name: 'version', + short: 'v', + type: function(){ return true; }, + description: 'Displays version info', + example: "'" + self.name + " -v' or '" + self.name + " --version'", + onset: function( args ) { + console.log( v + "\n" ); + process.exit( 0 ); + } + }); + + return self; + }, + + // Adding options to definitions list + option: function( mod, object ) { + if ( object === undefined ) { + object = mod; + mod = undefined; + } + + // Iterate over array for multi entry + if ( self.isArray( object ) ) { + object.forEach(function( entry ) { + self.option( mod, entry ); + }); + } + // Handle edge case + else if ( ! self.isObject( object ) ) { + throw new Error( 'No option definition provided' + ( mod ? ' for module ' + mod : '' ) ); + } + // Handle module definition + else if ( object.mod ) { + self.mod( object ); + } + // Correct the object + else { + if ( ! object.name ) { + throw new Error( 'No name provided for option' ); + } + else if ( ! object.type ) { + throw new Error( 'No type proveded for option' ); + } + + object.description = object.description || ''; + object.type = self.isFunction( object.type ) ? object.type : + self.isString( object.type ) && rlistcsv.exec( object.type ) ? self.types[ 'listcsv-combo' ]( object.type ) : + self.isString( object.type ) && self.types[ object.type ] ? self.types[ object.type ] : + self.types.string; + + // Apply to module + if ( mod ) { + if ( ! self.mods[ mod ] ) { + self.mods[ mod ] = { mod: mod, options: {}, short: {} }; + } + + // Attach option to submodule + mod = self.mods[ mod ]; + mod.options[ object.name ] = object; + + // Attach shorthand + if ( object.short ) { + mod.short[ object.short ] = object; + } + } + // Apply to root options + else { + self.options[ object.name ] = object; + + // Attach shorthand option + if ( object.short ) { + self.short[ object.short ] = object; + } + } + } + + return self; + }, + + // Creating module + mod: function( object ) { + var mod; + + // Allow multi mod setup + if ( self.isArray( object ) ) { + object.forEach(function( value ) { + self.mod( value ); + }); + } + // Handle edge case + else if ( ! self.isObject( object ) ) { + throw new Error( 'No mod definition provided' ); + } + // Force mod name + else if ( ! object.mod ) { + throw new Error( "Expecting 'mod' entry for module" ); + } + // Create object if not already done so + else if ( ! self.mods[ object.mod ] ) { + self.mods[ object.mod ] = { mod: object.mod, options: {}, short: {} }; + } + + // Setup + mod = self.mods[ object.mod ]; + mod.description = object.description || mod.description; + + // Attach each option + self.option( mod.mod, object.options ); + + return self; + }, + + // Cleans out current options + clear: function(){ + self.options = {}; + self.mods = {}; + + return self; + }, + + // Description setup + info: function( mod, description ) { + if ( description === undefined ) { + self.description = mod; + } + else if ( self.mods[ mod ] ) { + self.mods[ mod ] = description; + } + + return self; + }, + + // Prints out the help doc + help: function( mod ) { + var output = [], name, option; + + // Printing out just a module's definitions + if ( mod && ( mod = self.mods[ mod ] ) ) { + output = [ '', mod.description, '' ]; + + for ( name in mod.options ) { + option = mod.options[ name ]; + + output.push( "\t--" +option.name + ( option.short ? ', -' + option.short : '' ) ); + output.push( "\t\t" + option.description ); + if ( option.example ) { + output.push( "\t\t" + option.example ); + } + + // Spacing + output.push( "" ); + } + } + // Printing out just the root options + else { + output = [ '', self.description, '' ]; + + for ( name in self.options ) { + option = self.options[ name ]; + + output.push( "\t--" +option.name + ( option.short ? ', -' + option.short : '' ) ); + output.push( "\t\t" + option.description ); + if ( option.example ) { + output.push( "\t\t" + option.example ); + } + + // Spacing + output.push( "" ); + } + } + + // Print out the output + console.log( output.join( "\n" ) + "\n\n" ); + return self; + }, + + // Runs the arguments parser + _run: function( argv ) { + var args = { targets: [], options: {} }, + opts = self.options, + shortOpts = self.short, + skip, option, index, value, arg; + + // Allow for passing of arguments list + argv = self.isArray( argv ) ? argv : process.argv.slice( 2 ); + + // Switch to module's options when used + if ( argv.length && ristarget.exec( argv[ 0 ] ) && self.mods[ argv[ 0 ] ] ) { + args.mod = argv.shift(); + opts = self.mods[ args.mod ].options; + shortOpts = self.mods[ args.mod ].short; + } + + // Iterate over arguments + argv.forEach(function( arg, i ) { + // Allow skipping of arguments + if ( skip ) { + return ( skip = false ); + } + // Full option '--option' + else if ( rddash.exec( arg ) ) { + arg = arg.replace( rddash, '' ); + + // Default no value to true + if ( ( index = arg.indexOf( '=' ) ) !== -1 ) { + value = arg.substr( index + 1 ); + arg = arg.substr( 0, index ); + } + else { + value = 'true'; + } + + // Be strict, if option doesn't exist, throw and error + if ( ! ( option = opts[ arg ] ) ) { + throw "Option '--" + arg + "' not supported"; + } + + // Send through type converter + args.options[ arg ] = option.type( value, arg, args.options, args ); + + // Trigger onset callback when option is set + if ( self.isFunction( option.onset ) ) { + option.onset( args ); + } + } + // Shorthand option '-o' + else if ( rdash.exec( arg ) ) { + arg = arg.replace( rdash, '' ); + + if ( arg.length > 1 ) { + arg.split( '' ).forEach(function( character ) { + if ( ! ( option = shortOpts[ character ] ) ) { + throw "Option '-" + character + "' not supported"; + } + + args.options[ option.name ] = option.type( value, option.name, args.options, args ); + }); + } + else { + // Check next option for target association + if ( argv[ i + 1 ] && ristarget.exec( argv[ i + 1 ] ) ) { + value = argv[ i + 1 ]; + skip = true; + } + else { + value = 'true'; + } + + // Ensure that an option exists + if ( ! ( option = shortOpts[ arg ] ) ) { + throw "Option '-" + arg + "' not supported"; + } + + // Convert it + args.options[ option.name ] = option.type( value, option.name, args.options, args ); + + // Trigger onset callback when option is set + if ( self.isFunction( option.onset ) ) { + option.onset( args ); + } + } + } + // Targets + else { + args.targets.push( arg ); + } + }); + + return args; + }, + + run: function( argv ) { + try { + return self._run( argv ); + } + catch ( e ) { + if ( ! self.isString( e ) ) { + throw e; + } + + console.log( "\n" + e + ". Trigger '" + self.name + " -h' for more details.\n\n" ); + process.exit( 1 ); + } + } + +}; + + +// Type tests +"Boolean Number String Function Array Date RegExp Object Error".split(' ').forEach(function( method ) { + if ( method == 'Array' ) { + return ( self.isArray = Array.isArray ); + } + else if ( method == 'Error' ) { + self.isError = function( object ) { + return object && ( object instanceof Error ); + }; + + return; + } + + var match = '[object ' + method + ']'; + self[ 'is' + method ] = function( object ) { + return object !== undefined && object !== null && toString.call( object ) == match; + }; +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..66b31bc --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "argv", + "description": "CLI Argument Parser", + "url": "https://github.com/codenothing/argv", + "keywords": [ "cli", "argv", "options" ], + "author": "Corey Hart ", + "version": "0.0.1", + "main": "./index.js", + "engines": { + "node": ">=0.6.10" + }, + "dependencies": { + }, + "devDependencies": { + "nlint": "0.0.1" + } +}