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;