time-to-botec

Benchmark sampling in different programming languages
Log | Files | Refs | README

main.js (10267B)


      1 /**
      2 * @license Apache-2.0
      3 *
      4 * Copyright (c) 2018 The Stdlib Authors.
      5 *
      6 * Licensed under the Apache License, Version 2.0 (the "License");
      7 * you may not use this file except in compliance with the License.
      8 * You may obtain a copy of the License at
      9 *
     10 *    http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing, software
     13 * distributed under the License is distributed on an "AS IS" BASIS,
     14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15 * See the License for the specific language governing permissions and
     16 * limitations under the License.
     17 */
     18 
     19 /* eslint-disable stdlib/jsdoc-doctest, no-restricted-syntax */
     20 
     21 'use strict';
     22 
     23 // MODULES //
     24 
     25 var parseArgs = require( 'minimist' ); // TODO: replace with stdlib equivalent
     26 var defaults = require( './defaults.json' );
     27 var isInteger = require( './is_integer.js' );
     28 var validate = require( './validate.js' );
     29 var proc = require( './process.js' );
     30 var log = require( './console.js' );
     31 var exitCode = require( './exit_code.js' );
     32 var notifier = require( './notifier.js' );
     33 
     34 
     35 // VARIABLES //
     36 
     37 // NOTE: for the following, we explicitly avoid using stdlib packages in this particular package in order to avoid circular dependencies. This should not be problematic as (1) this package is unlikely to be used outside of Node.js and, thus, in environments lacking support for the built-in APIs, and (2) most of the historical bugs for the respective APIs were in environments such as IE and not the versions of V8 included in Node.js >= v0.10.x.
     38 var defineProperty = Object.defineProperty;
     39 var objectKeys = Object.keys;
     40 
     41 
     42 // FUNCTIONS //
     43 
     44 /**
     45 * Defines a read-only non-enumerable property.
     46 *
     47 * @private
     48 * @param {Object} obj - object on which to define the property
     49 * @param {(string|symbol)} prop - property name
     50 * @param {*} value - value to set
     51 *
     52 * @example
     53 * var obj = {};
     54 *
     55 * setReadOnly( obj, 'foo', 'bar' );
     56 *
     57 * try {
     58 *     obj.foo = 'boop';
     59 * } catch ( err ) {
     60 *     console.error( err.message );
     61 * }
     62 */
     63 function setReadOnly( obj, prop, value ) {
     64 	defineProperty( obj, prop, {
     65 		'configurable': false,
     66 		'enumerable': false,
     67 		'writable': false,
     68 		'value': value
     69 	});
     70 }
     71 
     72 
     73 // MAIN //
     74 
     75 /**
     76 * Command-line interface constructor.
     77 *
     78 * @constructor
     79 * @param {Options} [options] - options
     80 * @param {Object} [options.pkg={}] - package meta information (package.json)
     81 * @param {string} [options.version] - command-line interface version
     82 * @param {string} [options.help=""] - help text
     83 * @param {(string|boolean)} [options.title=true] - process title or a boolean indicating whether to set the process title
     84 * @param {boolean} [options.updates=true] - boolean indicating whether to check if a command-line interface is an outdated version
     85 * @param {Array} [options.argv] - command-line arguments
     86 * @param {Options} [options.options={}] - command-line interface options
     87 * @throws {TypeError} must provide an object
     88 * @throws {TypeError} must provide valid options
     89 * @returns {CLI} command-line interface
     90 *
     91 * @example
     92 * var opts = {
     93 *     'pkg': require( './path/to/package.json' ),
     94 *     'help': 'Usage: beep [options] <boop>',
     95 *     'title': 'foo',
     96 *     'updates': true,
     97 *     'options': {
     98 *         'boolean': [
     99 *             'help',
    100 *             'version'
    101 *         ]
    102 *     }
    103 * };
    104 * var cli = new CLI( opts );
    105 * // returns <CLI>
    106 *
    107 * cli.close();
    108 */
    109 function CLI( options ) {
    110 	var nopts;
    111 	var flags;
    112 	var keys;
    113 	var opts;
    114 	var argv;
    115 	var args;
    116 	var self;
    117 	var err;
    118 	if ( !( this instanceof CLI ) ) {
    119 		if ( arguments.length ) {
    120 			return new CLI( options );
    121 		}
    122 		return new CLI();
    123 	}
    124 	opts = {
    125 		'pkg': {},
    126 		'help': defaults.help,
    127 		'title': defaults.title,
    128 		'version': defaults.version,
    129 		'updates': defaults.updates,
    130 		'argv': defaults.argv,
    131 		'options': {}
    132 	};
    133 	if ( arguments.length ) {
    134 		err = validate( opts, options );
    135 		if ( err ) {
    136 			throw err;
    137 		}
    138 	}
    139 	self = this;
    140 
    141 	// Force the process to exit if an error is encountered when writing to `stdout` or `stderr`:
    142 	proc.stdout.on( 'error', proc.exit );
    143 	proc.stderr.on( 'error', proc.exit );
    144 
    145 	/**
    146 	* Returns parsed command-line arguments.
    147 	*
    148 	* @name args
    149 	* @memberof CLI#
    150 	* @type {Function}
    151 	* @returns {StringArray} parsed command-line arguments
    152 	*
    153 	* @example
    154 	* var cli = new CLI();
    155 	*
    156 	* var args = cli.args();
    157 	* // returns <Array>
    158 	*/
    159 	setReadOnly( this, 'args', getArgs );
    160 
    161 	/**
    162 	* Returns parsed command-line flags.
    163 	*
    164 	* @name flags
    165 	* @memberof CLI#
    166 	* @type {Function}
    167 	* @returns {Object} parsed command-line flags
    168 	*
    169 	* @example
    170 	* var cli = new CLI();
    171 	*
    172 	* var flags = cli.flags();
    173 	* // returns <Object>
    174 	*/
    175 	setReadOnly( this, 'flags', getFlags );
    176 
    177 	/**
    178 	* Prints usage information and exits the process.
    179 	*
    180 	* @name help
    181 	* @memberof CLI#
    182 	* @type {Function}
    183 	*
    184 	* @example
    185 	* var opts = {
    186 	*     'help': 'Usage: beep [options] <boop>'
    187 	* };
    188 	* var cli = new CLI( opts );
    189 	*
    190 	* cli.help();
    191 	* // => 'Usage: beep [options] <boop>'
    192 	*/
    193 	setReadOnly( this, 'help', help );
    194 
    195 	/**
    196 	* Prints the command-line interface version and exits the process.
    197 	*
    198 	* @name version
    199 	* @memberof CLI#
    200 	* @type {Function}
    201 	*
    202 	* @example
    203 	* var opts = {
    204 	*     'pkg': require( './path/to/package.json' )
    205 	* };
    206 	* var cli = new CLI( opts );
    207 	*
    208 	* cli.version();
    209 	* // => '#.#.#'
    210 	*/
    211 	setReadOnly( this, 'version', version );
    212 
    213 	// Check whether to set the process title...
    214 	if ( opts.title === true && opts.pkg ) {
    215 		if ( typeof opts.pkg.bin === 'object' && opts.pkg.bin !== null ) {
    216 			keys = objectKeys( opts.pkg.bin );
    217 
    218 			// Note: we don't have a way of knowing which command name in the `bin` hash was invoked; thus, we assume the first entry.
    219 			proc.title = keys[ 0 ];
    220 		} else if ( opts.pkg.name ) {
    221 			proc.title = opts.pkg.name;
    222 		}
    223 	} else if ( opts.title ) {
    224 		proc.title = opts.title;
    225 	}
    226 	// Check whether to notify the user of a new CLI version...
    227 	if ( opts.updates && opts.pkg && opts.pkg.name && opts.pkg.version ) {
    228 		nopts = {
    229 			'pkg': opts.pkg
    230 		};
    231 		notifier( nopts ).notify();
    232 	}
    233 	// Determine the command-line interface version...
    234 	if ( !opts.version && opts.pkg && opts.pkg.version ) {
    235 		opts.version = opts.pkg.version;
    236 	}
    237 	// Parse command-line arguments:
    238 	if ( opts.argv ) {
    239 		opts.argv = opts.argv.slice( 2 );
    240 	} else {
    241 		opts.argv = proc.argv.slice( 2 );
    242 	}
    243 	argv = parseArgs( opts.argv, opts.options );
    244 
    245 	// Cache parsed arguments:
    246 	args = argv._;
    247 	delete argv._;
    248 	flags = argv;
    249 
    250 	// Determine whether to print help text...
    251 	if ( flags.help ) {
    252 		return this.help( 0 );
    253 	}
    254 	// Determine whether to print the version...
    255 	if ( flags.version ) {
    256 		return this.version();
    257 	}
    258 	return this;
    259 
    260 	/**
    261 	* Returns parsed command-line arguments.
    262 	*
    263 	* @private
    264 	* @returns {StringArray} parsed command-line arguments
    265 	*/
    266 	function getArgs() {
    267 		return args.slice();
    268 	}
    269 
    270 	/**
    271 	* Returns parsed command-line flags.
    272 	*
    273 	* @private
    274 	* @returns {Object} parsed command-line flags
    275 	*/
    276 	function getFlags() {
    277 		var keys;
    278 		var o;
    279 		var k;
    280 		var i;
    281 
    282 		keys = objectKeys( flags );
    283 		o = {};
    284 		for ( i = 0; i < keys.length; i++ ) {
    285 			k = keys[ i ];
    286 			o[ k ] = flags[ k ];
    287 		}
    288 		return o;
    289 	}
    290 
    291 	/**
    292 	* Prints usage information.
    293 	*
    294 	* ## Notes
    295 	*
    296 	* -   Upon printing usage information, the function forces the process to exit.
    297 	*
    298 	* @private
    299 	* @param {NonNegativeInteger} [code=0] - exit code
    300 	*/
    301 	function help( code ) {
    302 		log.error( opts.help );
    303 		self.close( code || 0 );
    304 	}
    305 
    306 	/**
    307 	* Prints the command-line interface version.
    308 	*
    309 	* ## Notes
    310 	*
    311 	* -   Upon printing the version, the function forces the process to exit.
    312 	*
    313 	* @private
    314 	*/
    315 	function version() {
    316 		log.error( opts.version );
    317 		self.close();
    318 	}
    319 }
    320 
    321 /**
    322 * Gracefully exits the command-line interface and the calling process.
    323 *
    324 * @name close
    325 * @memberof CLI.prototype
    326 * @type {Function}
    327 * @param {NonNegativeInteger} [code=0] - exit code
    328 * @throws {TypeError} must provide a nonnegative integer
    329 * @returns {void}
    330 *
    331 * @example
    332 * var cli = new CLI();
    333 *
    334 * // Gracefully exit:
    335 * cli.close();
    336 */
    337 setReadOnly( CLI.prototype, 'close', function close( code ) {
    338 	if ( arguments.length === 0 ) {
    339 		exitCode( proc, 0 );
    340 		return;
    341 	}
    342 	if ( typeof code !== 'number' || !isInteger( code ) || code < 0 ) {
    343 		throw new TypeError( 'invalid argument. Must provide a nonnegative integer. Value: `' + code + '`.' );
    344 	}
    345 	exitCode( proc, code );
    346 });
    347 
    348 /**
    349 * Exits the command-line interface and the calling process due to an error.
    350 *
    351 * ## Notes
    352 *
    353 * -   The value assigned to the `message` property of the provided `Error` object is printed to `stderr` prior to exiting the command-line interface and the calling process.
    354 *
    355 * @name error
    356 * @memberof CLI.prototype
    357 * @type {Function}
    358 * @param {Error} error - error object
    359 * @param {NonNegativeInteger} [code=1] - exit code
    360 * @throws {TypeError} first argument must be an error object
    361 * @throws {TypeError} second argument must be a nonnegative integer
    362 * @returns {void}
    363 *
    364 * @example
    365 * var cli = new CLI();
    366 *
    367 * // ...
    368 *
    369 * // Create an error object:
    370 * var err = new Error( 'invalid operation' );
    371 *
    372 * // Exit the process:
    373 * cli.error( err, 0 );
    374 */
    375 setReadOnly( CLI.prototype, 'error', function onError( error, code ) {
    376 	var c;
    377 	if ( !( error instanceof Error ) ) {
    378 		throw new TypeError( 'invalid argument. First argument must be an error object. Value: `' + error + '`.' );
    379 	}
    380 	if ( arguments.length > 1 ) {
    381 		if ( typeof code !== 'number' || !isInteger( code ) || code < 0 ) {
    382 			throw new TypeError( 'invalid argument. Second argument must be a nonnegative integer. Value: `' + code + '`.' );
    383 		}
    384 		c = code;
    385 	} else {
    386 		c = 1;
    387 	}
    388 	log.error( 'Error: %s', error.message );
    389 	exitCode( proc, c );
    390 });
    391 
    392 /**
    393 * Forces the command-line interface (and the calling process) to exit.
    394 *
    395 * @name exit
    396 * @memberof CLI.prototype
    397 * @type {Function}
    398 * @param {NonNegativeInteger} [code=0] - exit code
    399 * @throws {TypeError} must provide a nonnegative integer
    400 * @returns {void}
    401 *
    402 * @example
    403 * var cli = new CLI();
    404 *
    405 * // Forcefully exit:
    406 * cli.exit();
    407 */
    408 setReadOnly( CLI.prototype, 'exit', function exit( code ) {
    409 	if ( arguments.length === 0 ) {
    410 		return proc.exit( 0 );
    411 	}
    412 	if ( typeof code !== 'number' || !isInteger( code ) || code < 0 ) {
    413 		throw new TypeError( 'invalid argument. Must provide a nonnegative integer. Value: `' + code + '`.' );
    414 	}
    415 	proc.exit( code );
    416 });
    417 
    418 
    419 // EXPORTS //
    420 
    421 module.exports = CLI;