diff --git a/lib/apply-extends.ts b/lib/apply-extends.ts index d235ac9a7..38dc9df73 100644 --- a/lib/apply-extends.ts +++ b/lib/apply-extends.ts @@ -32,7 +32,7 @@ function mergeDeep (config1: Dictionary, config2: Dictionary) { return target } -export function applyExtends (config: Dictionary, cwd: string, mergeExtends: boolean) { +export function applyExtends (config: Dictionary, cwd: string, mergeExtends = false): Dictionary { let defaultConfig = {} if (Object.prototype.hasOwnProperty.call(config, 'extends')) { diff --git a/lib/command.ts b/lib/command.ts index e508eecc9..7055b38ac 100644 --- a/lib/command.ts +++ b/lib/command.ts @@ -1,4 +1,4 @@ -import { Dictionary, assertNotUndefined, assertNotTrue } from './common-types' +import { Dictionary, assertNotStrictEqual } from './common-types' import { isPromise } from './is-promise' import { applyMiddleware, commandMiddlewareFactory, Middleware } from './middleware' import { parseCommand, Positional } from './parse-command' @@ -7,9 +7,7 @@ import { RequireDirectoryOptions } from 'require-directory' import { UsageInstance } from './usage' import { inspect } from 'util' import { ValidationInstance } from './validation' -import { YargsInstance, isYargsInstance, Options, OptionDefinition } from './yargs-types' -import { DetailedArguments, Arguments } from 'yargs-parser' -import { Context } from 'vm' +import { YargsInstance, isYargsInstance, Options, OptionDefinition, Context, Configuration, Arguments, DetailedArguments } from './yargs' import requireDirectory = require('require-directory') import whichModule = require('which-module') import Parser = require('yargs-parser') @@ -148,7 +146,7 @@ export function command ( function extractDesc ({ describe, description, desc }: CommandHandlerDefinition) { for (const test of [describe, description, desc]) { if (typeof test === 'string' || test === false) return test - assertNotTrue(test) + assertNotStrictEqual(test, true as true) } return false } @@ -161,7 +159,7 @@ export function command ( self.runCommand = function runCommand (command, yargs, parsed, commandIndex) { let aliases = parsed.aliases - const commandHandler = handlers[command] || handlers[aliasMap[command]] || defaultCommand + const commandHandler = handlers[command!] || handlers[aliasMap[command!]] || defaultCommand const currentContext = yargs.getContext() let numFiles = currentContext.files.length const parentCommands = currentContext.commands.slice() @@ -186,7 +184,7 @@ export function command ( ) } innerArgv = innerYargs._parseArgs(null, null, true, commandIndex) - aliases = innerYargs.parsed.aliases + aliases = (innerYargs.parsed as DetailedArguments).aliases } else if (isCommandBuilderOptionDefinitions(builder)) { // as a short hand, an object can instead be provided, specifying // the options that a command takes. @@ -201,11 +199,11 @@ export function command ( innerYargs.option(key, builder[key]) }) innerArgv = innerYargs._parseArgs(null, null, true, commandIndex) - aliases = innerYargs.parsed.aliases + aliases = (innerYargs.parsed as DetailedArguments).aliases } if (!yargs._hasOutput()) { - positionalMap = populatePositionals(commandHandler, innerArgv, currentContext) + positionalMap = populatePositionals(commandHandler, innerArgv as Arguments, currentContext) } const middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares) @@ -213,7 +211,14 @@ export function command ( // we apply validation post-hoc, so that custom // checks get passed populated positional arguments. - if (!yargs._hasOutput()) yargs._runValidation(innerArgv, aliases, positionalMap, yargs.parsed.error, !command) + if (!yargs._hasOutput()) { + yargs._runValidation( + innerArgv as Arguments, + aliases, + positionalMap, + (yargs.parsed as DetailedArguments).error, + !command) + } if (commandHandler.handler && !yargs._hasOutput()) { yargs._setHasOutput() @@ -279,7 +284,7 @@ export function command ( } self.runDefaultBuilderOn = function (yargs) { - assertNotUndefined(defaultCommand) + assertNotStrictEqual(defaultCommand, undefined) if (shouldUpdateUsage(yargs)) { // build the root-level command string from the default string. const commandString = DEFAULT_MARKER.test(defaultCommand.original) @@ -360,7 +365,7 @@ export function command ( // short-circuit parse. if (!unparsed.length) return - const config = Object.assign({}, options.configuration, { + const config: Configuration = Object.assign({}, options.configuration, { 'populate--': true }) const parsed = Parser.detailed(unparsed, Object.assign({}, options, { @@ -440,7 +445,7 @@ export function command ( } self.unfreeze = () => { const frozen = frozens.pop() - assertNotUndefined(frozen) + assertNotStrictEqual(frozen, undefined) ;({ handlers, aliasMap, @@ -475,7 +480,7 @@ export interface CommandInstance { getCommands (): string[] hasDefaultCommand (): boolean reset (): CommandInstance - runCommand (command: string, yargs: YargsInstance, parsed: DetailedArguments, commandIndex: number): void + runCommand (command: string | null, yargs: YargsInstance, parsed: DetailedArguments, commandIndex?: number): Arguments | Promise runDefaultBuilderOn (yargs: YargsInstance): void unfreeze(): void } @@ -546,3 +551,7 @@ type FrozenCommandInstance = { aliasMap: Dictionary defaultCommand: CommandHandler | undefined } + +export interface FinishCommandHandler { + (handlerResult: any): any +} diff --git a/lib/common-types.ts b/lib/common-types.ts index ee1c9fcb1..5404bc7e1 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -1,13 +1,48 @@ -import { notStrictEqual } from 'assert' +import { notStrictEqual, strictEqual } from 'assert' +/** + * An object whose all properties have the same type. + */ export type Dictionary = { [key: string]: T } +/** + * Returns the keys of T that match Dictionary and are not arrays. + */ +export type DictionaryKeyof = Exclude>, KeyOf> + +/** + * Returns the keys of T that match U. + */ +export type KeyOf = Exclude<{ [K in keyof T]: T[K] extends U ? K : never }[keyof T], undefined> + +/** + * An array whose first element is not undefined. + */ export type NotEmptyArray = [T, ...T[]] -export function assertNotTrue (actual: T | true): asserts actual is T { - notStrictEqual(actual, true) +/** + * Returns the type of a Dictionary or array values. + */ +export type ValueOf = T extends (infer U)[] ? U : T[keyof T]; + +/** + * Typing wrapper around assert.notStrictEqual() + */ +export function assertNotStrictEqual (actual: T|N, expected: N, message ?: string | Error) +: asserts actual is Exclude { + notStrictEqual(actual, expected, message) +} + +/** + * Asserts actual is a single key, not a key array or a key map. + */ +export function assertSingleKey (actual: string | string[] | Dictionary): asserts actual is string { + strictEqual(typeof actual, 'string') } -export function assertNotUndefined (actual: T | undefined): asserts actual is T { - notStrictEqual(actual, undefined) +/** + * Typing wrapper around Object.keys() + */ +export function objectKeys (object: T) { + return Object.keys(object) as (keyof T)[] } diff --git a/lib/completion.ts b/lib/completion.ts index ff465bb36..5f428a3d5 100644 --- a/lib/completion.ts +++ b/lib/completion.ts @@ -4,8 +4,9 @@ import { isPromise } from './is-promise' import { parseCommand } from './parse-command' import * as path from 'path' import { UsageInstance } from './usage' -import { YargsInstance } from './yargs-types' +import { YargsInstance } from './yargs' import { Arguments, DetailedArguments } from 'yargs-parser' +import { assertNotStrictEqual } from './common-types' // add bash completions to your // yargs-powered applications. @@ -31,7 +32,9 @@ export function completion (yargs: YargsInstance, usage: UsageInstance, command: // a custom completion function can be provided // to completion(). - if (completionFunction) { + function runCompletionFunction (argv: Arguments) { + assertNotStrictEqual(completionFunction, null) + if (isSyncCompletionFunction(completionFunction)) { const result = completionFunction(current, argv) @@ -54,6 +57,10 @@ export function completion (yargs: YargsInstance, usage: UsageInstance, command: } } + if (completionFunction) { + return isPromise(argv) ? argv.then(runCompletionFunction) : runCompletionFunction(argv) + } + const handlers = command.getCommandHandlers() for (let i = 0, ii = args.length; i < ii; ++i) { if (handlers[args[i]] && handlers[args[i]].builder) { @@ -137,7 +144,7 @@ export function completion (yargs: YargsInstance, usage: UsageInstance, command: } /** Instance of the completion module. */ -interface CompletionInstance { +export interface CompletionInstance { completionKey: string generateCompletionScript($0: string, cmd: string): string getCompletion(args: string[], done: (completions: string[]) => any): any @@ -145,7 +152,7 @@ interface CompletionInstance { setParsed(parsed: DetailedArguments): void } -type CompletionFunction = SyncCompletionFunction | AsyncCompletionFunction +export type CompletionFunction = SyncCompletionFunction | AsyncCompletionFunction interface SyncCompletionFunction { (current: string, argv: Arguments): string[] | Promise diff --git a/lib/middleware.ts b/lib/middleware.ts index 8ff51e950..1186dcdda 100644 --- a/lib/middleware.ts +++ b/lib/middleware.ts @@ -1,7 +1,6 @@ import { argsert } from './argsert' import { isPromise } from './is-promise' -import { YargsInstance } from './yargs-types' -import { Arguments } from 'yargs-parser' +import { YargsInstance, Arguments } from './yargs' export function globalMiddlewareFactory (globalMiddleware: Middleware[], context: T) { return function (callback: MiddlewareCallback | MiddlewareCallback[], applyBeforeValidation = false) { @@ -31,20 +30,20 @@ export function commandMiddlewareFactory (commandMiddleware?: MiddlewareCallback } export function applyMiddleware ( - argv: Arguments, + argv: Arguments | Promise, yargs: YargsInstance, middlewares: Middleware[], beforeValidation: boolean ) { const beforeValidationError = new Error('middleware cannot return a promise when applyBeforeValidation is true') return middlewares - .reduce>((accumulation, middleware) => { + .reduce>((acc, middleware) => { if (middleware.applyBeforeValidation !== beforeValidation) { - return accumulation + return acc } - if (isPromise(accumulation)) { - return accumulation + if (isPromise(acc)) { + return acc .then(initialObj => Promise.all>([initialObj, middleware(initialObj, yargs)]) ) @@ -52,17 +51,17 @@ export function applyMiddleware ( Object.assign(initialObj, middlewareObj) ) } else { - const result = middleware(argv, yargs) + const result = middleware(acc, yargs) if (beforeValidation && isPromise(result)) throw beforeValidationError return isPromise(result) - ? result.then(middlewareObj => Object.assign(accumulation, middlewareObj)) - : Object.assign(accumulation, result) + ? result.then(middlewareObj => Object.assign(acc, middlewareObj)) + : Object.assign(acc, result) } }, argv) } -interface MiddlewareCallback { +export interface MiddlewareCallback { (argv: Arguments, yargs: YargsInstance): Partial | Promise> } diff --git a/lib/obj-filter.ts b/lib/obj-filter.ts index e8cfa2112..c4767189c 100644 --- a/lib/obj-filter.ts +++ b/lib/obj-filter.ts @@ -1,8 +1,11 @@ -import { Dictionary } from './common-types' +import { objectKeys } from './common-types' -export function objFilter (original: Dictionary, filter: (k: string, v: T) => boolean = () => true) { - const obj: Dictionary = {} - Object.keys(original || {}).forEach((key) => { +export function objFilter ( + original = {} as T, + filter: (k: keyof T, v: T[keyof T]) => boolean = () => true +) { + const obj = {} as T + objectKeys(original).forEach((key) => { if (filter(key, original[key])) { obj[key] = original[key] } diff --git a/lib/typings/require-main-filename.d.ts b/lib/typings/require-main-filename.d.ts new file mode 100644 index 000000000..30ca73474 --- /dev/null +++ b/lib/typings/require-main-filename.d.ts @@ -0,0 +1,21 @@ +// TODO: either create @types/require-main-filename or or convert require-main-filename to typescript + +/** + * Returns the entry point of the current application. + * + * `require.main.filename` is great for figuring out the entry point for the current application. + * This can be combined with a module like pkg-conf to, as if by magic, load top-level configuration. + * + * Unfortunately, `require.main.filename` sometimes fails when an application is executed + * with an alternative process manager, e.g., iisnode. + * + * `require-main-filename` is a shim that addresses this problem. + * + * @param _require require function + * @returns hash of modules in specified directory + */ +declare function requireMainFilename(_require: NodeRequire): string; + +declare module 'require-main-filename' { + export = requireMainFilename; +} diff --git a/lib/typings/y18n.d.ts b/lib/typings/y18n.d.ts index 3a5d451d2..961623ddb 100644 --- a/lib/typings/y18n.d.ts +++ b/lib/typings/y18n.d.ts @@ -1,59 +1,64 @@ // TODO: either update @types/y18n with this or convert y18n to typescript // Forked from: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/a9cb5fa/types/y18n/index.d.ts -interface Config { - /** - * The locale directory, default ./locales. - */ - directory: string; - /** - * Should newly observed strings be updated in file, default true. - */ - updateFiles: boolean; - /** - * What locale should be used. - */ - locale: string; - /** - * Should fallback to a language-only file (e.g. en.json) be allowed - * if a file matching the locale does not exist (e.g. en_US.json), default true. - */ - fallbackToLanguage: boolean; -} +/* eslint no-redeclare: "off" */ +declare namespace y18n { + interface Config { + /** + * The locale directory, default ./locales. + */ + directory?: string; + /** + * Should newly observed strings be updated in file, default true. + */ + updateFiles?: boolean; + /** + * What locale should be used. + */ + locale?: string; + /** + * Should fallback to a language-only file (e.g. en.json) be allowed + * if a file matching the locale does not exist (e.g. en_US.json), default true. + */ + fallbackToLanguage?: boolean; + } + + export class Y18N { + /** + * Create an instance of y18n with the config provided + */ + constructor(config?: Config); + + /** + * Print a localized string, %s will be replaced with args. + */ + __(str: string, arg1?: string, arg2?: string, arg3?: string): string; + + /** + * Print a localized string with appropriate pluralization. + * If %d is provided in the string, the quantity will replace this placeholder. + */ + __n(singular: string, plural: string, quantity: number, ...param: any[]): string; -declare class Y18N { - /** - * Create an instance of y18n with the config provided - */ - constructor(config?: Config); - - /** - * Print a localized string, %s will be replaced with args. - */ - __(str: string, arg1?: string, arg2?: string, arg3?: string): string; - - /** - * Print a localized string with appropriate pluralization. - * If %d is provided in the string, the quantity will replace this placeholder. - */ - __n(singular: string, plural: string, quantity: number, ...param: any[]): string; - - /** - * Set the current locale being used. - */ - setLocale(str: string): void; - - /** - * What locale is currently being used? - */ - getLocale(): string; - - /** - * Update the current locale with the key value pairs in obj. - */ - updateLocale(obj: object): void; + /** + * Set the current locale being used. + */ + setLocale(str: string): void; + + /** + * What locale is currently being used? + */ + getLocale(): string; + + /** + * Update the current locale with the key value pairs in obj. + */ + updateLocale(obj: { [key: string]: string }): void; + } } +declare function y18n (opts?: y18n.Config): y18n.Y18N; + declare module 'y18n' { - export = Y18N; + export = y18n; } diff --git a/lib/typings/yargs-parser.d.ts b/lib/typings/yargs-parser.d.ts new file mode 100644 index 000000000..cc4f4c08f --- /dev/null +++ b/lib/typings/yargs-parser.d.ts @@ -0,0 +1,124 @@ +// TODO: either update @types/yargs-parser with this or convert yargs-parser to typescript +// Forked from: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/699b8159a6f571a14ef6d1d07b956cf78c8e729c/types/yargs-parser/index.d.ts + +/* eslint no-redeclare: "off" */ +declare namespace yargsParser { + interface Arguments { + /** Non-option arguments */ + _: string[]; + /** All remaining options */ + [argName: string]: any; + } + + interface DetailedArguments { + /** An object representing the parsed value of `args` */ + argv: Arguments; + /** Populated with an error object if an exception occurred during parsing. */ + error: Error | null; + /** The inferred list of aliases built by combining lists in opts.alias. */ + aliases: { [alias: string]: string[] }; + /** Any new aliases added via camel-case expansion. */ + newAliases: { [alias: string]: boolean }; + /** The configuration loaded from the yargs stanza in package.json. */ + configuration: Configuration; + } + + interface Configuration { + /** Should variables prefixed with --no be treated as negations? Default is `true` */ + 'boolean-negation': boolean; + /** Should hyphenated arguments be expanded into camel-case aliases? Default is `true` */ + 'camel-case-expansion': boolean; + /** Should arrays be combined when provided by both command line arguments and a configuration file. Default is `false` */ + 'combine-arrays': boolean; + /** Should keys that contain . be treated as objects? Default is `true` */ + 'dot-notation': boolean; + /** Should arguments be coerced into an array when duplicated. Default is `true` */ + 'duplicate-arguments-array': boolean; + /** Should array arguments be coerced into a single array when duplicated. Default is `true` */ + 'flatten-duplicate-arrays': boolean; + /** Should arrays consume more than one positional argument following their flag? Default is `true` */ + 'greedy-arrays': boolean; + /** Should parsing stop at the first text argument? This is similar to how e.g. ssh parses its command line. Default is `false` */ + 'halt-at-non-option': boolean; + /** Should nargs consume dash options as well as positional arguments? Default is `false` */ + 'nargs-eats-options': boolean; + /** The prefix to use for negated boolean variables. Default is `'no-'` */ + 'negation-prefix': string; + /** Should keys that look like numbers be treated as such? Default is `true` */ + 'parse-numbers': boolean; + /** Should unparsed flags be stored in -- or _. Default is `false` */ + 'populate--': boolean; + /** Should a placeholder be added for keys not set via the corresponding CLI argument? Default is `false` */ + 'set-placeholder-key': boolean; + /** Should a group of short-options be treated as boolean flags? Default is `true` */ + 'short-option-groups': boolean; + /** Should aliases be removed before returning results? Default is `false` */ + 'strip-aliased': boolean; + /** Should dashed keys be removed before returning results? This option has no effect if camel-case-expansion is disabled. Default is `false` */ + 'strip-dashed': boolean; + /** Should unknown options be treated like regular arguments? An unknown option is one that is not configured in opts. Default is `false` */ + 'unknown-options-as-args': boolean; + } + + type ArrayOption = string | { key: string; string?: boolean, boolean?: boolean, number?: boolean, integer?: boolean }; + + interface Options { + /** An object representing the set of aliases for a key: `{ alias: { foo: ['f']} }`. */ + alias: { [key: string]: string | string[] }; + /** + * Indicate that keys should be parsed as an array: `{ array: ['foo', 'bar'] }`. + * Indicate that keys should be parsed as an array and coerced to booleans / numbers: + * { array: [ { key: 'foo', boolean: true }, {key: 'bar', number: true} ] }`. + */ + array: ArrayOption | ArrayOption[]; + /** Arguments should be parsed as booleans: `{ boolean: ['x', 'y'] }`. */ + boolean: string | string[]; + /** Indicate a key that represents a path to a configuration file (this file will be loaded and parsed). */ + config: string | string[] | { [key: string]: boolean | ConfigCallback }; + /** configuration objects to parse, their properties will be set as arguments */ + configObjects: Array<{ [key: string]: any }>; + /** Provide configuration options to the yargs-parser. */ + configuration: Partial; + /** + * Provide a custom synchronous function that returns a coerced value from the argument provided (or throws an error), e.g. + * `{ coerce: { foo: function (arg) { return modifiedArg } } }`. + */ + coerce: { [key: string]: CoerceCallback }; + /** Indicate a key that should be used as a counter, e.g., `-vvv = {v: 3}`. */ + count: string | string[]; + /** Provide default values for keys: `{ default: { x: 33, y: 'hello world!' } }`. */ + default: { [key: string]: any }; + /** Environment variables (`process.env`) with the prefix provided should be parsed. */ + envPrefix: string; + /** Specify that a key requires n arguments: `{ narg: {x: 2} }`. */ + narg: { [key: string]: number }; + /** `path.normalize()` will be applied to values set to this key. */ + normalize: string | string[]; + /** Keys should be treated as strings (even if they resemble a number `-x 33`). */ + string: string | string[]; + /** Keys should be treated as numbers. */ + number: string | string[]; + /** i18n handler, defaults to util.format */ + __: (format: any, ...param: any[]) => string; + /** alias lookup table defaults */ + key: { [key: string]: any }; + } + + interface CoerceCallback { + (arg: any): any + } + + interface ConfigCallback { + (configPath: string): { [key: string]: any } + } + + interface Parser { + (args: string | any[], opts?: Partial): Arguments; + detailed(args: string | any[], opts?: Partial): DetailedArguments; + } +} + +declare var yargsParser: yargsParser.Parser +declare module 'yargs-parser' { + export = yargsParser; +} diff --git a/lib/usage.ts b/lib/usage.ts index 3e9878e0f..a4204e49a 100644 --- a/lib/usage.ts +++ b/lib/usage.ts @@ -1,14 +1,15 @@ // this file handles outputting usage instructions, // failures, etc. keeps logging in one place. -import { Dictionary, assertNotUndefined } from './common-types' +import { Dictionary, assertNotStrictEqual } from './common-types' import { objFilter } from './obj-filter' import * as path from 'path' -import { YargsInstance } from './yargs-types' +import { YargsInstance } from './yargs' import { YError } from './yerror' +import { Y18N } from 'y18n' +import { DetailedArguments } from 'yargs-parser' import decamelize = require('decamelize') import setBlocking = require('set-blocking') import stringWidth = require('string-width') -import Y18N = require('y18n') export function usage (yargs: YargsInstance, y18n: Y18N) { const __ = y18n.__ @@ -111,8 +112,12 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { self.getCommands = () => commands let descriptions: Dictionary = {} - self.describe = function describe (keyOrKeys: string | Dictionary, desc?: string) { - if (typeof keyOrKeys === 'object') { + self.describe = function describe (keyOrKeys: string | string[] | Dictionary, desc?: string) { + if (Array.isArray(keyOrKeys)) { + keyOrKeys.forEach((k) => { + self.describe(k, desc) + }) + } else if (typeof keyOrKeys === 'object') { Object.keys(keyOrKeys).forEach((k) => { self.describe(k, keyOrKeys[k]) }) @@ -128,7 +133,7 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { } let wrapSet = false - let wrap: number | undefined + let wrap: number | null | undefined self.wrap = (cols) => { wrapSet = true wrap = cols @@ -245,9 +250,9 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { // perform some cleanup on the keys array, making it // only include top-level keys not their aliases. const aliasKeys = (Object.keys(options.alias) || []) - .concat(Object.keys(yargs.parsed.newAliases) || []) + .concat(Object.keys((yargs.parsed as DetailedArguments).newAliases) || []) - keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1)) + keys = keys.filter(key => !(yargs.parsed as DetailedArguments).newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1)) // populate 'Options:' group with any keys that have not // explicitly had a group set. @@ -309,7 +314,7 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { if (~options.array.indexOf(key)) type = `[${__('array')}]` if (~options.number.indexOf(key)) type = `[${__('number')}]` - const deprecatedExtra = (deprecated: string | boolean) => typeof deprecated === 'string' + const deprecatedExtra = (deprecated?: string | boolean) => typeof deprecated === 'string' ? `[${__('deprecated: %s', deprecated)}]` : `[${__('deprecated')}]` @@ -378,7 +383,7 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { // return the maximum width of a string // in the left-hand column of a table. - function maxWidth (table: [string, ...any[]][] | Dictionary, theWrap?: number, modifier?: string) { + function maxWidth (table: [string, ...any[]][] | Dictionary, theWrap?: number | null, modifier?: string) { let width = 0 // table might be of the form [leftColumn], @@ -457,7 +462,7 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { } function filterHiddenOptions (key: string) { - return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || yargs.parsed.argv[yargs.getOptions().showHiddenOpt] + return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || (yargs.parsed as DetailedArguments).argv[yargs.getOptions().showHiddenOpt] } self.showHelp = (level: 'error' | 'log' | ((message: string) => void)) => { @@ -564,7 +569,7 @@ export function usage (yargs: YargsInstance, y18n: Y18N) { } self.unfreeze = function unfreeze () { const frozen = frozens.pop() - assertNotUndefined(frozen) + assertNotStrictEqual(frozen, undefined) ;({ failMessage, failureOutput, @@ -586,8 +591,7 @@ export interface UsageInstance { clearCachedHelpMessage(): void command(cmd: string, description: string | undefined, isDefault: boolean, aliases: string[], deprecated?: boolean): void deferY18nLookup(str: string): string - describe(key: string, desc?: string): void - describe(keys: Dictionary): void + describe(keys: string | string[] | Dictionary, desc?: string): void epilog(msg: string): void example(cmd: string, description?: string): void fail(msg?: string | null, err?: YError | string): void @@ -601,19 +605,17 @@ export interface UsageInstance { getUsageDisabled(): boolean help(): string reset(localLookup: Dictionary): UsageInstance - showHelp(level: 'error' | 'log'): void - showHelp(level: (message: string) => void): void - showHelpOnFail(message?: string): UsageInstance - showHelpOnFail(enabled: boolean, message: string): UsageInstance + showHelp(level: 'error' | 'log' | ((message: string) => void)): void + showHelpOnFail (enabled?: boolean | string, message?: string): UsageInstance showVersion(): void stringifiedValues(values?: any[], separator?: string): string unfreeze(): void usage(msg: string | null, description?: string | false): UsageInstance version(ver: any): void - wrap(cols: number): void + wrap(cols: number | null | undefined): void } -interface FailureFunction { +export interface FailureFunction { (msg: string | undefined | null, err: YError | string | undefined, usage: UsageInstance): void } diff --git a/lib/validation.ts b/lib/validation.ts index cb5055eb6..48f5ce841 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -1,11 +1,11 @@ import { argsert } from './argsert' -import { Dictionary, assertNotUndefined } from './common-types' +import { Dictionary, assertNotStrictEqual } from './common-types' import { levenshtein as distance } from './levenshtein' import { objFilter } from './obj-filter' import { UsageInstance } from './usage' -import { YargsInstance } from './yargs-types' -import { Arguments, DetailedArguments } from 'yargs-parser' -import Y18N = require('y18n') +import { YargsInstance, Arguments } from './yargs' +import { DetailedArguments } from 'yargs-parser' +import { Y18N } from 'y18n' const specialKeys = ['$0', '--', '_'] // validation-type-stuff, missing params, @@ -84,7 +84,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 // make sure all the required arguments are present. self.requiredArguments = function requiredArguments (argv) { const demandedOptions = yargs.getDemandedOptions() - let missing: Dictionary | null = null + let missing: Dictionary | null = null for (const key of Object.keys(demandedOptions)) { if (!Object.prototype.hasOwnProperty.call(argv, key) || typeof argv[key] === 'undefined') { @@ -179,7 +179,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 if (!Object.prototype.hasOwnProperty.call(aliases, key)) { return false } - const newAliases = yargs.parsed.newAliases + const newAliases = (yargs.parsed as DetailedArguments).newAliases for (const a of [key, ...aliases[key]]) { if (!Object.prototype.hasOwnProperty.call(newAliases, a) || !newAliases[key]) { return true @@ -254,7 +254,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 // check implications, argument foo implies => argument bar. let implied: Dictionary = {} - self.implies = function implies (key: string | Dictionary, value?: KeyOrPos | KeyOrPos[]) { + self.implies = function implies (key, value) { argsert(' [array|number|string]', [key, value], arguments.length) if (typeof key === 'object') { @@ -269,6 +269,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 if (Array.isArray(value)) { value.forEach((i) => self.implies(key, i)) } else { + assertNotStrictEqual(value, undefined) implied[key].push(value) } } @@ -325,7 +326,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 } let conflicting: Dictionary<(string | undefined)[]> = {} - self.conflicts = function conflicts (key: string | Dictionary, value?: string | string[]) { + self.conflicts = function conflicts (key, value) { argsert(' [array|string]', [key, value], arguments.length) if (typeof key === 'object') { @@ -393,7 +394,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 } self.unfreeze = function unfreeze () { const frozen = frozens.pop() - assertNotUndefined(frozen) + assertNotStrictEqual(frozen, undefined) ;({ implied, checks, @@ -408,15 +409,13 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 export interface ValidationInstance { check(f: CustomCheck['func'], global: boolean): void conflicting(argv: Arguments): void - conflicts(key: string, value: string | string[]): void - conflicts(key: Dictionary): void + conflicts(key: string | Dictionary, value?: string | string[]): void customChecks(argv: Arguments, aliases: DetailedArguments['aliases']): void freeze(): void getConflicting(): Dictionary<(string | undefined)[]> getImplied(): Dictionary implications(argv: Arguments): void - implies(key: string, value: KeyOrPos | KeyOrPos[]): void - implies(key: Dictionary): void + implies(key: string | Dictionary, value?: KeyOrPos | KeyOrPos[]): void isValidAndSomeAliasIsNotNew(key: string, aliases: DetailedArguments['aliases']): boolean limitedChoices(argv: Arguments): void nonOptionCount(argv: Arguments): void @@ -445,4 +444,4 @@ interface FrozenValidationInstance { conflicting: Dictionary<(string | undefined)[]> } -type KeyOrPos = string | number | undefined +export type KeyOrPos = string | number diff --git a/lib/yargs-types.ts b/lib/yargs-types.ts deleted file mode 100644 index 73f29b15b..000000000 --- a/lib/yargs-types.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { CommandInstance } from './command' -import { Dictionary } from './common-types' -import { Arguments, DetailedArguments, Configuration } from 'yargs-parser' -import { YError } from './yerror' -import { UsageInstance } from './usage' - -/** Instance of the yargs module. */ -export interface YargsInstance { - $0: string - argv: Arguments - customScriptName: boolean - parsed: DetailedArguments - _copyDoubleDash (argv: Arguments): void - _getLoggerInstance (): LoggerInstance - _getParseContext(): Object - _hasOutput (): boolean - _hasParseCallback (): boolean - // TODO: to be precised once yargs is tsified - _parseArgs (args: null, shortCircuit: null, _calledFromCommand: boolean, commandIndex: number): Arguments - _runValidation ( - argv: Arguments, - aliases: Dictionary, - positionalMap: Dictionary, - parseErrors: Error | null, - isDefaultCommand: boolean - ): void - _setHasOutput (): void - array (key: string): YargsInstance - boolean (key: string): YargsInstance - count (key: string): YargsInstance - demandOption (key: string, msg: string): YargsInstance - exit (code: number, err?: YError | string): void - getCommandInstance (): CommandInstance - getContext (): Context - getDemandedOptions (): Dictionary - getDemandedCommands (): Dictionary<{ - min: number - max: number, - minMsg?: string | null, - maxMsg?: string | null - }> - getDeprecatedOptions (): Dictionary - getExitProcess (): boolean - getGroups (): Dictionary - getHandlerFinishCommand (): (handlerResult: any) => any - getOptions (): Options - getParserConfiguration (): ParserConfiguration - getUsageInstance (): UsageInstance - global (globals: string | string[], global?: boolean): YargsInstance - normalize (key: string): YargsInstance - number (key: string): YargsInstance - option (key: string, opt: OptionDefinition): YargsInstance - parse (args: string[], shortCircuit: boolean): Arguments - reset (aliases?: DetailedArguments['aliases']): YargsInstance - showHelp (level: string): YargsInstance - string (key: string): YargsInstance -} - -export function isYargsInstance (y: YargsInstance | void): y is YargsInstance { - return !!y && (typeof y._parseArgs === 'function') -} - -/** Yargs' context. */ -interface Context { - commands: string[] - // TODO: to be precised once yargs is tsified - files: {}[] - fullCommands: string[] -} - -type LoggerInstance = Pick - -export interface Options { - array: string[] - alias: Dictionary - boolean: string[] - choices: Dictionary - // TODO: to be precised once yargs is tsified - config: {} - configuration: ParserConfiguration - count: string[] - default: Dictionary - defaultDescription: Dictionary - hiddenOptions: string[] - /** Manually set keys */ - key: Dictionary - normalize: string[] - number: string[] - showHiddenOpt: string - string: string[] -} - -interface ParserConfiguration extends Configuration { - /** Should command be sorted in help */ - 'sort-commands': boolean -} - -export interface OptionDefinition { - // TODO: to be precised once yargs is tsified -} diff --git a/lib/yargs.ts b/lib/yargs.ts new file mode 100644 index 000000000..9080fa91e --- /dev/null +++ b/lib/yargs.ts @@ -0,0 +1,1715 @@ +import { CommandInstance, CommandHandler, CommandBuilderDefinition, CommandBuilder, CommandHandlerCallback, FinishCommandHandler, command as Command } from './command' +import { Dictionary, assertNotStrictEqual, KeyOf, DictionaryKeyof, ValueOf, objectKeys, assertSingleKey } from './common-types' +import { + Arguments as ParserArguments, + DetailedArguments as ParserDetailedArguments, + Configuration as ParserConfiguration, + Options as ParserOptions, + ConfigCallback, + CoerceCallback +} from 'yargs-parser' +import { YError } from './yerror' +import { UsageInstance, FailureFunction, usage as Usage } from './usage' +import { argsert } from './argsert' +import * as fs from 'fs' + +import { completion as Completion, CompletionInstance, CompletionFunction } from './completion' +import * as path from 'path' + +import { validation as Validation, ValidationInstance, KeyOrPos } from './validation' +import { Y18N } from 'y18n' +import { objFilter } from './obj-filter' +import { applyExtends } from './apply-extends' +import { globalMiddlewareFactory, MiddlewareCallback, Middleware } from './middleware' +import * as processArgv from './process-argv' +import { RequireDirectoryOptions } from 'require-directory' +import { isPromise } from './is-promise' +import Parser = require('yargs-parser') +import y18nFactory = require('y18n') +import setBlocking = require('set-blocking') +import findUp = require('find-up') +import requireMainFilename = require('require-main-filename') + +export function Yargs (processArgs: string | string[] = [], cwd = process.cwd(), parentRequire = require) { + const self = {} as YargsInstance + let command: CommandInstance + let completion: CompletionInstance | null = null + let groups: Dictionary = {} + const globalMiddleware: Middleware[] = [] + let output = '' + const preservedGroups: Dictionary = {} + let usage: UsageInstance + let validation: ValidationInstance + let handlerFinishCommand: FinishCommandHandler | null = null + + const y18n = y18nFactory({ + directory: path.resolve(__dirname, '../../locales'), + updateFiles: false + }) + + self.middleware = globalMiddlewareFactory(globalMiddleware, self) + + self.scriptName = function (scriptName) { + self.customScriptName = true + self.$0 = scriptName + return self + } + + // ignore the node bin, specify this in your + // bin file with #!/usr/bin/env node + let default$0: string[] + if (/\b(node|iojs|electron)(\.exe)?$/.test(process.argv[0])) { + default$0 = process.argv.slice(1, 2) + } else { + default$0 = process.argv.slice(0, 1) + } + + self.$0 = default$0 + .map(x => { + const b = rebase(cwd, x) + return x.match(/^(\/|([a-zA-Z]:)?\\)/) && b.length < x.length ? b : x + }) + .join(' ').trim() + + if (process.env._ !== undefined && processArgv.getProcessArgvBin() === process.env._) { + self.$0 = process.env._.replace( + `${path.dirname(process.execPath)}/`, '' + ) + } + + // use context object to keep track of resets, subcommand execution, etc + // submodules should modify and check the state of context as necessary + const context = { resets: -1, commands: [], fullCommands: [], files: [] } + self.getContext = () => context + + // puts yargs back into an initial state. any keys + // that have been set to "global" will not be reset + // by this action. + let options: Options + self.resetOptions = self.reset = function resetOptions (aliases = {}) { + context.resets++ + options = options || {} + // put yargs back into an initial state, this + // logic is used to build a nested command + // hierarchy. + const tmpOptions = {} as Options + tmpOptions.local = options.local ? options.local : [] + tmpOptions.configObjects = options.configObjects ? options.configObjects : [] + + // if a key has been explicitly set as local, + // we should reset it before passing options to command. + const localLookup: Dictionary = {} + tmpOptions.local.forEach((l) => { + localLookup[l] = true + ;(aliases[l] || []).forEach((a) => { + localLookup[a] = true + }) + }) + + // add all groups not set to local to preserved groups + Object.assign( + preservedGroups, + Object.keys(groups).reduce((acc, groupName) => { + const keys = groups[groupName].filter(key => !(key in localLookup)) + if (keys.length > 0) { + acc[groupName] = keys + } + return acc + }, {} as Dictionary) + ) + // groups can now be reset + groups = {} + + const arrayOptions: KeyOf[] = [ + 'array', 'boolean', 'string', 'skipValidation', + 'count', 'normalize', 'number', + 'hiddenOptions' + ] + + const objectOptions: DictionaryKeyof[] = [ + 'narg', 'key', 'alias', 'default', 'defaultDescription', + 'config', 'choices', 'demandedOptions', 'demandedCommands', 'coerce', + 'deprecatedOptions' + ] + + arrayOptions.forEach(k => { + tmpOptions[k] = (options[k] || []).filter(k => !localLookup[k]) + }) + + objectOptions.forEach(>(k: K) => { + tmpOptions[k] = objFilter(options[k], k => !localLookup[k as string]) + }) + + tmpOptions.envPrefix = options.envPrefix + options = tmpOptions + + // if this is the first time being executed, create + // instances of all our helpers -- otherwise just reset. + usage = usage ? usage.reset(localLookup) : Usage(self, y18n) + validation = validation ? validation.reset(localLookup) : Validation(self, usage, y18n) + command = command ? command.reset() : Command(self, usage, validation, globalMiddleware) + if (!completion) completion = Completion(self, usage, command) + + completionCommand = null + output = '' + exitError = null + hasOutput = false + self.parsed = false + + return self + } + self.resetOptions() + + // temporary hack: allow "freezing" of reset-able state for parse(msg, cb) + const frozens: FrozenYargsInstance[] = [] + function freeze () { + frozens.push({ + options, + configObjects: options.configObjects.slice(0), + exitProcess, + groups, + strict, + strictCommands, + completionCommand, + output, + exitError, + hasOutput, + parsed: self.parsed, + parseFn, + parseContext, + handlerFinishCommand + }) + usage.freeze() + validation.freeze() + command.freeze() + } + function unfreeze () { + const frozen = frozens.pop() + assertNotStrictEqual(frozen, undefined) + let configObjects: Dictionary[] + ;({ + options, + configObjects, + exitProcess, + groups, + output, + exitError, + hasOutput, + parsed: self.parsed, + strict, + strictCommands, + completionCommand, + parseFn, + parseContext, + handlerFinishCommand + } = frozen) + options.configObjects = configObjects + usage.unfreeze() + validation.unfreeze() + command.unfreeze() + } + + self.boolean = function (keys) { + argsert('', [keys], arguments.length) + populateParserHintArray('boolean', keys) + return self + } + + self.array = function (keys) { + argsert('', [keys], arguments.length) + populateParserHintArray('array', keys) + return self + } + + self.number = function (keys) { + argsert('', [keys], arguments.length) + populateParserHintArray('number', keys) + return self + } + + self.normalize = function (keys) { + argsert('', [keys], arguments.length) + populateParserHintArray('normalize', keys) + return self + } + + self.count = function (keys) { + argsert('', [keys], arguments.length) + populateParserHintArray('count', keys) + return self + } + + self.string = function (keys) { + argsert('', [keys], arguments.length) + populateParserHintArray('string', keys) + return self + } + + self.requiresArg = function (keys) { + // the 2nd paramter [number] in the argsert the assertion is mandatory + // as populateParserHintSingleValueDictionary recursively calls requiresArg + // with Nan as a 2nd parameter, although we ignore it + argsert(' [number]', [keys], arguments.length) + // If someone configures nargs at the same time as requiresArg, + // nargs should take precedent, + // see: https://github.com/yargs/yargs/pull/1572 + // TODO: make this work with aliases, using a check similar to + // checkAllAliases() in yargs-parser. + if (typeof keys === 'string' && options.narg[keys]) { + return self + } else { + populateParserHintSingleValueDictionary(self.requiresArg, 'narg', keys, NaN) + } + return self + } + + self.skipValidation = function (keys) { + argsert('', [keys], arguments.length) + populateParserHintArray('skipValidation', keys) + return self + } + + function populateParserHintArray> ( + type: T, + keys: string | string[] + ) { + keys = ([] as string[]).concat(keys) + keys.forEach((key) => { + key = sanitizeKey(key) + options[type].push(key) + }) + } + + self.nargs = function (key: string | string[] | Dictionary, value?: number) { + argsert(' [number]', [key, value], arguments.length) + populateParserHintSingleValueDictionary(self.nargs, 'narg', key, value) + return self + } + + self.choices = function (key: string | string[] | Dictionary, value?: string | string[]) { + argsert(' [string|array]', [key, value], arguments.length) + populateParserHintArrayDictionary(self.choices, 'choices', key, value) + return self + } + + self.alias = function (key: string | string[] | Dictionary, value?: string | string[]) { + argsert(' [string|array]', [key, value], arguments.length) + populateParserHintArrayDictionary(self.alias, 'alias', key, value) + return self + } + + // TODO: actually deprecate self.defaults. + self.default = self.defaults = function ( + key: string | string[] | Dictionary, + value?: any, + defaultDescription?: string + ) { + argsert(' [*] [string]', [key, value, defaultDescription], arguments.length) + if (defaultDescription) { + assertSingleKey(key) + options.defaultDescription[key] = defaultDescription + } + if (typeof value === 'function') { + assertSingleKey(key) + if (!options.defaultDescription[key]) options.defaultDescription[key] = usage.functionDescription(value) + value = value.call() + } + populateParserHintSingleValueDictionary<'default'>(self.default, 'default', key, value) + return self + } + + self.describe = function (key: string | string[] | Dictionary, desc?: string) { + argsert(' [string]', [key, desc], arguments.length) + setKey(key, true) + usage.describe(key, desc) + return self + } + + function setKey (key: string | string[] | Dictionary, set?: boolean | string) { + populateParserHintSingleValueDictionary(setKey, 'key', key, set) + return self + } + + function demandOption (keys: string | string[] | Dictionary, msg?: string) { + argsert(' [string]', [keys, msg], arguments.length) + populateParserHintSingleValueDictionary(self.demandOption, 'demandedOptions', keys, msg) + return self + } + self.demandOption = demandOption + + self.coerce = function ( + keys: string | string[] | Dictionary, + value?: CoerceCallback + ) { + argsert(' [function]', [keys, value], arguments.length) + populateParserHintSingleValueDictionary(self.coerce, 'coerce', keys, value) + return self + } + + function populateParserHintSingleValueDictionary< + T extends Exclude, DictionaryKeyof> | 'default', + K extends keyof Options[T] & string = keyof Options[T] & string, + V extends ValueOf = ValueOf + > ( + builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, + type: T, + key: K | K[] | { [key in K]: V }, + value?: V + ) { + populateParserHintDictionary(builder, type, key, value, (type, key, value) => { + options[type][key] = value as ValueOf + }) + } + + function populateParserHintArrayDictionary< + T extends DictionaryKeyof, + K extends keyof Options[T] & string = keyof Options[T] & string, + V extends ValueOf> | ValueOf>[] = ValueOf> | ValueOf>[] + > ( + builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, + type: T, + key: K | K[] | { [key in K]: V }, + value?: V + ) { + populateParserHintDictionary(builder, type, key, value, (type, key, value) => { + options[type][key] = (options[type][key] || [] as Options[T][keyof Options[T]]).concat(value) + }) + } + + function populateParserHintDictionary ( + builder: (key: K, value: V, ...otherArgs: any[]) => YargsInstance, + type: T, + key: K | K[] | { [key in K]: V }, + value: V | undefined, + singleKeyHandler: (type: T, key: K, value?: V) => void + ) { + if (Array.isArray(key)) { + // an array of keys with one value ['x', 'y', 'z'], function parse () {} + key.forEach((k) => { + builder(k, value!) + }) + } else if (((key): key is { [key in K]: V } => typeof key === 'object')(key)) { + // an object of key value pairs: {'x': parse () {}, 'y': parse() {}} + for (const k of objectKeys(key)) { + builder(k, key[k]) + } + } else { + singleKeyHandler(type, sanitizeKey(key), value) + } + } + + // TODO(bcoe): in future major versions move more objects towards + // Object.create(null): + function sanitizeKey (key: K): K; + function sanitizeKey (key: '__proto__'): '___proto___'; + function sanitizeKey (key: any) { + if (key === '__proto__') return '___proto___' + return key + } + + function deleteFromParserHintObject (optionKey: string) { + // delete from all parsing hints: + // boolean, array, key, alias, etc. + objectKeys(options).forEach((hintKey: keyof Options) => { + // configObjects is not a parsing hint array + if (((key): key is 'configObjects' => key === 'configObjects')(hintKey)) return + const hint = options[hintKey] + if (Array.isArray(hint)) { + if (~hint.indexOf(optionKey)) hint.splice(hint.indexOf(optionKey), 1) + } else if (typeof hint === 'object') { + delete (hint as Dictionary)[optionKey] + } + }) + // now delete the description from usage.js. + delete usage.getDescriptions()[optionKey] + } + + self.config = function config ( + key: string | string[] | Dictionary = 'config', + msg?: string | ConfigCallback, + parseFn?: ConfigCallback + ) { + argsert('[object|string] [string|function] [function]', [key, msg, parseFn], arguments.length) + // allow a config object to be provided directly. + if ((typeof key === 'object') && !Array.isArray(key)) { + key = applyExtends(key, cwd, self.getParserConfiguration()['deep-merge-config']) + options.configObjects = (options.configObjects || []).concat(key) + return self + } + + // allow for a custom parsing function. + if (typeof msg === 'function') { + parseFn = msg + msg = undefined + } + + self.describe(key, msg || usage.deferY18nLookup('Path to JSON config file')) + ;(Array.isArray(key) ? key : [key]).forEach((k) => { + options.config[k] = parseFn || true + }) + + return self + } + + self.example = function (cmd, description) { + argsert(' [string]', [cmd, description], arguments.length) + usage.example(cmd, description) + return self + } + + self.command = function (cmd, description, builder, handler, middlewares, deprecated) { + argsert(' [string|boolean] [function|object] [function] [array] [boolean|string]', [cmd, description, builder, handler, middlewares, deprecated], arguments.length) + command.addHandler(cmd, description, builder, handler, middlewares, deprecated) + return self + } + + self.commandDir = function (dir, opts) { + argsert(' [object]', [dir, opts], arguments.length) + const req = parentRequire || require + command.addDirectory(dir, self.getContext(), req, require('get-caller-file')(), opts) + return self + } + + // TODO: deprecate self.demand in favor of + // .demandCommand() .demandOption(). + self.demand = self.required = self.require = function demand ( + keys: string | string[] | Dictionary | number, + max?: number | string[] | string | true, + msg?: string | true + ) { + // you can optionally provide a 'max' key, + // which will raise an exception if too many '_' + // options are provided. + if (Array.isArray(max)) { + max.forEach((key) => { + assertNotStrictEqual(msg, true as true) + demandOption(key, msg) + }) + max = Infinity + } else if (typeof max !== 'number') { + msg = max + max = Infinity + } + + if (typeof keys === 'number') { + assertNotStrictEqual(msg, true as true) + self.demandCommand(keys, max, msg, msg) + } else if (Array.isArray(keys)) { + keys.forEach((key) => { + assertNotStrictEqual(msg, true as true) + demandOption(key, msg) + }) + } else { + if (typeof msg === 'string') { + demandOption(keys, msg) + } else if (msg === true || typeof msg === 'undefined') { + demandOption(keys) + } + } + + return self + } + + self.demandCommand = function demandCommand ( + min: number = 1, + max?: number | string, + minMsg?: string | null, + maxMsg?: string | null + ) { + argsert('[number] [number|string] [string|null|undefined] [string|null|undefined]', [min, max, minMsg, maxMsg], arguments.length) + + if (typeof max !== 'number') { + minMsg = max + max = Infinity + } + + self.global('_', false) + + options.demandedCommands._ = { + min, + max, + minMsg, + maxMsg + } + + return self + } + + self.getDemandedOptions = () => { + argsert([], 0) + return options.demandedOptions + } + + self.getDemandedCommands = () => { + argsert([], 0) + return options.demandedCommands + } + + self.deprecateOption = function deprecateOption (option, message) { + argsert(' [string|boolean]', [option, message], arguments.length) + options.deprecatedOptions[option] = message + return self + } + + self.getDeprecatedOptions = () => { + argsert([], 0) + return options.deprecatedOptions + } + + self.implies = function ( + key: string | Dictionary, + value?: KeyOrPos | KeyOrPos[] + ) { + argsert(' [number|string|array]', [key, value], arguments.length) + validation.implies(key, value) + return self + } + + self.conflicts = function (key1: string | Dictionary, key2?: string | string[]) { + argsert(' [string|array]', [key1, key2], arguments.length) + validation.conflicts(key1, key2) + return self + } + + self.usage = function ( + msg: string | null, + description?: CommandHandler['description'], + builder?: CommandBuilderDefinition | CommandBuilder, + handler?: CommandHandlerCallback + ) { + argsert(' [string|boolean] [function|object] [function]', [msg, description, builder, handler], arguments.length) + + if (description !== undefined) { + assertNotStrictEqual(msg, null) + // .usage() can be used as an alias for defining + // a default command. + if ((msg || '').match(/^\$0( |$)/)) { + return self.command(msg, description, builder, handler) + } else { + throw new YError('.usage() description must start with $0 if being used as alias for .command()') + } + } else { + usage.usage(msg) + return self + } + } + + self.epilogue = self.epilog = function (msg) { + argsert('', [msg], arguments.length) + usage.epilog(msg) + return self + } + + self.fail = function (f) { + argsert('', [f], arguments.length) + usage.failFn(f) + return self + } + + self.onFinishCommand = function (f) { + argsert('', [f], arguments.length) + handlerFinishCommand = f + return self + } + + self.getHandlerFinishCommand = () => handlerFinishCommand + + self.check = function (f, _global) { + argsert(' [boolean]', [f, _global], arguments.length) + validation.check(f, _global !== false) + return self + } + + self.global = function global (globals, global) { + argsert(' [boolean]', [globals, global], arguments.length) + globals = ([] as string[]).concat(globals) + if (global !== false) { + options.local = options.local.filter(l => globals.indexOf(l) === -1) + } else { + globals.forEach((g) => { + if (options.local.indexOf(g) === -1) options.local.push(g) + }) + } + return self + } + + self.pkgConf = function pkgConf (key, rootPath) { + argsert(' [string]', [key, rootPath], arguments.length) + let conf = null + // prefer cwd to require-main-filename in this method + // since we're looking for e.g. "nyc" config in nyc consumer + // rather than "yargs" config in nyc (where nyc is the main filename) + const obj = pkgUp(rootPath || cwd) + + // If an object exists in the key, add it to options.configObjects + if (obj[key] && typeof obj[key] === 'object') { + conf = applyExtends(obj[key], rootPath || cwd, self.getParserConfiguration()['deep-merge-config']) + options.configObjects = (options.configObjects || []).concat(conf) + } + + return self + } + + const pkgs: Dictionary = {} + function pkgUp (rootPath?: string) { + const npath = rootPath || '*' + if (pkgs[npath]) return pkgs[npath] + + let obj = {} + try { + let startDir = rootPath || requireMainFilename(parentRequire) + + // When called in an environment that lacks require.main.filename, such as a jest test runner, + // startDir is already process.cwd(), and should not be shortened. + // Whether or not it is _actually_ a directory (e.g., extensionless bin) is irrelevant, find-up handles it. + if (!rootPath && path.extname(startDir)) { + startDir = path.dirname(startDir) + } + + const pkgJsonPath = findUp.sync('package.json', { + cwd: startDir + }) + assertNotStrictEqual(pkgJsonPath, undefined) + obj = JSON.parse(fs.readFileSync(pkgJsonPath).toString()) + } catch (noop) {} + + pkgs[npath] = obj || {} + return pkgs[npath] + } + + let parseFn: ParseCallback | null = null + let parseContext: object | null = null + self.parse = function parse ( + args?: string | string[], + shortCircuit?: object | ParseCallback | boolean, + _parseFn?: ParseCallback + ) { + argsert('[string|array] [function|boolean|object] [function]', [args, shortCircuit, _parseFn], arguments.length) + freeze() + if (typeof args === 'undefined') { + const argv = self._parseArgs(processArgs) + const tmpParsed = self.parsed + unfreeze() + // TODO: remove this compatibility hack when we release yargs@15.x: + self.parsed = tmpParsed + return argv + } + + // a context object can optionally be provided, this allows + // additional information to be passed to a command handler. + if (typeof shortCircuit === 'object') { + parseContext = shortCircuit + shortCircuit = _parseFn + } + + // by providing a function as a second argument to + // parse you can capture output that would otherwise + // default to printing to stdout/stderr. + if (typeof shortCircuit === 'function') { + parseFn = shortCircuit as ParseCallback + shortCircuit = false + } + // completion short-circuits the parsing process, + // skipping validation, etc. + if (!shortCircuit) processArgs = args + + if (parseFn) exitProcess = false + + const parsed = self._parseArgs(args, !!shortCircuit) + completion!.setParsed(self.parsed as DetailedArguments) + if (parseFn) parseFn(exitError, parsed, output) + unfreeze() + + return parsed + } + + self._getParseContext = () => parseContext || {} + + self._hasParseCallback = () => !!parseFn + + self.option = self.options = function option ( + key: string | Dictionary, + opt?: OptionDefinition + ) { + argsert(' [object]', [key, opt], arguments.length) + if (typeof key === 'object') { + Object.keys(key).forEach((k) => { + self.options(k, key[k]) + }) + } else { + if (typeof opt !== 'object') { + opt = {} + } + + options.key[key] = true // track manually set keys. + + if (opt.alias) self.alias(key, opt.alias) + + const deprecate = opt.deprecate || opt.deprecated + + if (deprecate) { + self.deprecateOption(key, deprecate) + } + + const demand = opt.demand || opt.required || opt.require + + // A required option can be specified via "demand: true". + if (demand) { + self.demand(key, demand) + } + + if (opt.demandOption) { + self.demandOption(key, typeof opt.demandOption === 'string' ? opt.demandOption : undefined) + } + + if (opt.conflicts) { + self.conflicts(key, opt.conflicts) + } + + if ('default' in opt) { + self.default(key, opt.default) + } + + if (opt.implies !== undefined) { + self.implies(key, opt.implies) + } + + if (opt.nargs !== undefined) { + self.nargs(key, opt.nargs) + } + + if (opt.config) { + self.config(key, opt.configParser) + } + + if (opt.normalize) { + self.normalize(key) + } + + if (opt.choices) { + self.choices(key, opt.choices) + } + + if (opt.coerce) { + self.coerce(key, opt.coerce) + } + + if (opt.group) { + self.group(key, opt.group) + } + + if (opt.boolean || opt.type === 'boolean') { + self.boolean(key) + if (opt.alias) self.boolean(opt.alias) + } + + if (opt.array || opt.type === 'array') { + self.array(key) + if (opt.alias) self.array(opt.alias) + } + + if (opt.number || opt.type === 'number') { + self.number(key) + if (opt.alias) self.number(opt.alias) + } + + if (opt.string || opt.type === 'string') { + self.string(key) + if (opt.alias) self.string(opt.alias) + } + + if (opt.count || opt.type === 'count') { + self.count(key) + } + + if (typeof opt.global === 'boolean') { + self.global(key, opt.global) + } + + if (opt.defaultDescription) { + options.defaultDescription[key] = opt.defaultDescription + } + + if (opt.skipValidation) { + self.skipValidation(key) + } + + const desc = opt.describe || opt.description || opt.desc + self.describe(key, desc) + if (opt.hidden) { + self.hide(key) + } + + if (opt.requiresArg) { + self.requiresArg(key) + } + } + + return self + } + self.getOptions = () => options + + self.positional = function (key, opts) { + argsert(' ', [key, opts], arguments.length) + if (context.resets === 0) { + throw new YError(".positional() can only be called in a command's builder function") + } + + // .positional() only supports a subset of the configuration + // options available to .option(). + const supportedOpts: (keyof PositionalDefinition)[] = ['default', 'defaultDescription', 'implies', 'normalize', + 'choices', 'conflicts', 'coerce', 'type', 'describe', + 'desc', 'description', 'alias'] + opts = objFilter(opts, (k, v) => { + let accept = supportedOpts.indexOf(k) !== -1 + // type can be one of string|number|boolean. + if (k === 'type' && ['string', 'number', 'boolean'].indexOf(v) === -1) accept = false + return accept + }) + + // copy over any settings that can be inferred from the command string. + const fullCommand = context.fullCommands[context.fullCommands.length - 1] + const parseOptions = fullCommand ? command.cmdToParseOptions(fullCommand) : { + array: [], + alias: {}, + default: {}, + demand: {} + } + objectKeys(parseOptions).forEach((pk) => { + const parseOption = parseOptions[pk] + if (Array.isArray(parseOption)) { + if (parseOption.indexOf(key) !== -1) opts[pk] = true + } else { + if (parseOption[key] && !(pk in opts)) opts[pk] = parseOption[key] + } + }) + self.group(key, usage.getPositionalGroupName()) + return self.option(key, opts) + } + + self.group = function group (opts, groupName) { + argsert(' ', [opts, groupName], arguments.length) + const existing = preservedGroups[groupName] || groups[groupName] + if (preservedGroups[groupName]) { + // we now only need to track this group name in groups. + delete preservedGroups[groupName] + } + + const seen: Dictionary = {} + groups[groupName] = (existing || []).concat(opts).filter((key) => { + if (seen[key]) return false + return (seen[key] = true) + }) + return self + } + // combine explicit and preserved groups. explicit groups should be first + self.getGroups = () => Object.assign({}, groups, preservedGroups) + + // as long as options.envPrefix is not undefined, + // parser will apply env vars matching prefix to argv + self.env = function (prefix) { + argsert('[string|boolean]', [prefix], arguments.length) + if (prefix === false) delete options.envPrefix + else options.envPrefix = prefix || '' + return self + } + + self.wrap = function (cols) { + argsert('', [cols], arguments.length) + usage.wrap(cols) + return self + } + + let strict = false + self.strict = function (enabled) { + argsert('[boolean]', [enabled], arguments.length) + strict = enabled !== false + return self + } + self.getStrict = () => strict + + let strictCommands = false + self.strictCommands = function (enabled) { + argsert('[boolean]', [enabled], arguments.length) + strictCommands = enabled !== false + return self + } + self.getStrictCommands = () => strictCommands + + let parserConfig: Configuration = {} + self.parserConfiguration = function parserConfiguration (config) { + argsert('', [config], arguments.length) + parserConfig = config + return self + } + self.getParserConfiguration = () => parserConfig + + self.showHelp = function (level) { + argsert('[string|function]', [level], arguments.length) + if (!self.parsed) self._parseArgs(processArgs) // run parser, if it has not already been executed. + if (command.hasDefaultCommand()) { + context.resets++ // override the restriction on top-level positoinals. + command.runDefaultBuilderOn(self) + } + usage.showHelp(level) + return self + } + + let versionOpt: string | null = null + self.version = function version (opt?: string | false, msg?: string, ver?: string) { + const defaultVersionOpt = 'version' + argsert('[boolean|string] [string] [string]', [opt, msg, ver], arguments.length) + + // nuke the key previously configured + // to return version #. + if (versionOpt) { + deleteFromParserHintObject(versionOpt) + usage.version(undefined) + versionOpt = null + } + + if (arguments.length === 0) { + ver = guessVersion() + opt = defaultVersionOpt + } else if (arguments.length === 1) { + if (opt === false) { // disable default 'version' key. + return self + } + ver = opt + opt = defaultVersionOpt + } else if (arguments.length === 2) { + ver = msg + msg = undefined + } + + versionOpt = typeof opt === 'string' ? opt : defaultVersionOpt + msg = msg || usage.deferY18nLookup('Show version number') + + usage.version(ver || undefined) + self.boolean(versionOpt) + self.describe(versionOpt, msg) + return self + } + + function guessVersion () { + const obj = pkgUp() + + return obj.version || 'unknown' + } + + let helpOpt: string | null = null + self.addHelpOpt = self.help = function addHelpOpt (opt?: string | false, msg?: string) { + const defaultHelpOpt = 'help' + argsert('[string|boolean] [string]', [opt, msg], arguments.length) + + // nuke the key previously configured + // to return help. + if (helpOpt) { + deleteFromParserHintObject(helpOpt) + helpOpt = null + } + + if (arguments.length === 1) { + if (opt === false) return self + } + + // use arguments, fallback to defaults for opt and msg + helpOpt = typeof opt === 'string' ? opt : defaultHelpOpt + self.boolean(helpOpt) + self.describe(helpOpt, msg || usage.deferY18nLookup('Show help')) + return self + } + + const defaultShowHiddenOpt = 'show-hidden' + options!.showHiddenOpt = defaultShowHiddenOpt + self.addShowHiddenOpt = self.showHidden = function addShowHiddenOpt (opt?: string | false, msg?: string) { + argsert('[string|boolean] [string]', [opt, msg], arguments.length) + + if (arguments.length === 1) { + if (opt === false) return self + } + + const showHiddenOpt = typeof opt === 'string' ? opt : defaultShowHiddenOpt + self.boolean(showHiddenOpt) + self.describe(showHiddenOpt, msg || usage.deferY18nLookup('Show hidden options')) + options.showHiddenOpt = showHiddenOpt + return self + } + + self.hide = function hide (key) { + argsert('', [key], arguments.length) + options.hiddenOptions.push(key) + return self + } + + self.showHelpOnFail = function showHelpOnFail (enabled?: string | boolean, message?: string) { + argsert('[boolean|string] [string]', [enabled, message], arguments.length) + usage.showHelpOnFail(enabled, message) + return self + } + + var exitProcess = true + self.exitProcess = function (enabled = true) { + argsert('[boolean]', [enabled], arguments.length) + exitProcess = enabled + return self + } + self.getExitProcess = () => exitProcess + + var completionCommand: string | null = null + self.completion = function (cmd?: string, desc?: string | false | CompletionFunction, fn?: CompletionFunction) { + argsert('[string] [string|boolean|function] [function]', [cmd, desc, fn], arguments.length) + + // a function to execute when generating + // completions can be provided as the second + // or third argument to completion. + if (typeof desc === 'function') { + fn = desc + desc = undefined + } + + // register the completion command. + completionCommand = cmd || completionCommand || 'completion' + if (!desc && desc !== false) { + desc = 'generate completion script' + } + self.command(completionCommand, desc) + + // a function can be provided + if (fn) completion!.registerFunction(fn) + + return self + } + + self.showCompletionScript = function ($0, cmd) { + argsert('[string] [string]', [$0, cmd], arguments.length) + $0 = $0 || self.$0 + _logger.log(completion!.generateCompletionScript($0, cmd || completionCommand || 'completion')) + return self + } + + self.getCompletion = function (args, done) { + argsert(' ', [args, done], arguments.length) + completion!.getCompletion(args, done) + } + + self.locale = function (locale?: string): any { + argsert('[string]', [locale], arguments.length) + if (!locale) { + guessLocale() + return y18n.getLocale() + } + detectLocale = false + y18n.setLocale(locale) + return self + } + + self.updateStrings = self.updateLocale = function (obj) { + argsert('', [obj], arguments.length) + detectLocale = false + y18n.updateLocale(obj) + return self + } + + let detectLocale = true + self.detectLocale = function (detect) { + argsert('', [detect], arguments.length) + detectLocale = detect + return self + } + self.getDetectLocale = () => detectLocale + + var hasOutput = false + var exitError: YError | string | undefined | null = null + // maybe exit, always capture + // context about why we wanted to exit. + self.exit = (code, err) => { + hasOutput = true + exitError = err + if (exitProcess) process.exit(code) + } + + // we use a custom logger that buffers output, + // so that we can print to non-CLIs, e.g., chat-bots. + const _logger = { + log (...args: any[]) { + if (!self._hasParseCallback()) console.log(...args) + hasOutput = true + if (output.length) output += '\n' + output += args.join(' ') + }, + error (...args: any[]) { + if (!self._hasParseCallback()) console.error(...args) + hasOutput = true + if (output.length) output += '\n' + output += args.join(' ') + } + } + self._getLoggerInstance = () => _logger + // has yargs output an error our help + // message in the current execution context. + self._hasOutput = () => hasOutput + + self._setHasOutput = () => { + hasOutput = true + } + + let recommendCommands: boolean + self.recommendCommands = function (recommend = true) { + argsert('[boolean]', [recommend], arguments.length) + recommendCommands = recommend + return self + } + + self.getUsageInstance = () => usage + + self.getValidationInstance = () => validation + + self.getCommandInstance = () => command + + self.terminalWidth = () => { + argsert([], 0) + return typeof process.stdout.columns !== 'undefined' ? process.stdout.columns : null + } + + Object.defineProperty(self, 'argv', { + get: () => self._parseArgs(processArgs), + enumerable: true + }) + + self._parseArgs = function parseArgs ( + args: string | string [] | null, + shortCircuit?: boolean | null, + _calledFromCommand?: boolean, + commandIndex?: number + ) { + let skipValidation = !!_calledFromCommand + args = args || processArgs + + options.__ = y18n.__ + options.configuration = self.getParserConfiguration() + + const populateDoubleDash = !!options.configuration['populate--'] + const config = Object.assign({}, options.configuration, { + 'populate--': true + }) + const parsed = Parser.detailed(args, Object.assign({}, options, { + configuration: config + })) as DetailedArguments + + let argv = parsed.argv as Arguments + if (parseContext) argv = Object.assign({}, argv, parseContext) + const aliases = parsed.aliases + + argv.$0 = self.$0 + self.parsed = parsed + + try { + guessLocale() // guess locale lazily, so that it can be turned off in chain. + + // while building up the argv object, there + // are two passes through the parser. If completion + // is being performed short-circuit on the first pass. + if (shortCircuit) { + return (populateDoubleDash || _calledFromCommand) ? argv : self._copyDoubleDash(argv) + } + + // if there's a handler associated with a + // command defer processing to it. + if (helpOpt) { + // consider any multi-char helpOpt alias as a valid help command + // unless all helpOpt aliases are single-char + // note that parsed.aliases is a normalized bidirectional map :) + const helpCmds = [helpOpt] + .concat(aliases[helpOpt] || []) + .filter(k => k.length > 1) + // check if help should trigger and strip it from _. + if (~helpCmds.indexOf(argv._[argv._.length - 1])) { + argv._.pop() + argv[helpOpt] = true + } + } + + const handlerKeys = command.getCommands() + const requestCompletions = completion!.completionKey in argv + const skipRecommendation = argv[helpOpt!] || requestCompletions + const skipDefaultCommand = skipRecommendation && (handlerKeys.length > 1 || handlerKeys[0] !== '$0') + + if (argv._.length) { + if (handlerKeys.length) { + let firstUnknownCommand + for (let i = (commandIndex || 0), cmd; argv._[i] !== undefined; i++) { + cmd = String(argv._[i]) + if (~handlerKeys.indexOf(cmd) && cmd !== completionCommand) { + // commands are executed using a recursive algorithm that executes + // the deepest command first; we keep track of the position in the + // argv._ array that is currently being executed. + const innerArgv = command.runCommand(cmd, self, parsed, i + 1) + return populateDoubleDash ? innerArgv : self._copyDoubleDash(innerArgv) + } else if (!firstUnknownCommand && cmd !== completionCommand) { + firstUnknownCommand = cmd + break + } + } + + // run the default command, if defined + if (command.hasDefaultCommand() && !skipDefaultCommand) { + const innerArgv = command.runCommand(null, self, parsed) + return populateDoubleDash ? innerArgv : self._copyDoubleDash(innerArgv) + } + + // recommend a command if recommendCommands() has + // been enabled, and no commands were found to execute + if (recommendCommands && firstUnknownCommand && !skipRecommendation) { + validation.recommendCommands(firstUnknownCommand, handlerKeys) + } + } + + // generate a completion script for adding to ~/.bashrc. + if (completionCommand && ~argv._.indexOf(completionCommand) && !requestCompletions) { + if (exitProcess) setBlocking(true) + self.showCompletionScript() + self.exit(0) + } + } else if (command.hasDefaultCommand() && !skipDefaultCommand) { + const innerArgv = command.runCommand(null, self, parsed) + return populateDoubleDash ? innerArgv : self._copyDoubleDash(innerArgv) + } + + // we must run completions first, a user might + // want to complete the --help or --version option. + if (requestCompletions) { + if (exitProcess) setBlocking(true) + + // we allow for asynchronous completions, + // e.g., loading in a list of commands from an API. + args = ([] as string[]).concat(args) + const completionArgs = args.slice(args.indexOf(`--${completion!.completionKey}`) + 1) + completion!.getCompletion(completionArgs, (completions) => { + ;(completions || []).forEach((completion) => { + _logger.log(completion) + }) + + self.exit(0) + }) + return (populateDoubleDash || _calledFromCommand) ? argv : self._copyDoubleDash(argv) + } + + // Handle 'help' and 'version' options + // if we haven't already output help! + if (!hasOutput) { + Object.keys(argv).forEach((key) => { + if (key === helpOpt && argv[key]) { + if (exitProcess) setBlocking(true) + + skipValidation = true + self.showHelp('log') + self.exit(0) + } else if (key === versionOpt && argv[key]) { + if (exitProcess) setBlocking(true) + + skipValidation = true + usage.showVersion() + self.exit(0) + } + }) + } + + // Check if any of the options to skip validation were provided + if (!skipValidation && options.skipValidation.length > 0) { + skipValidation = Object.keys(argv).some(key => options.skipValidation.indexOf(key) >= 0 && argv[key] === true) + } + + // If the help or version options where used and exitProcess is false, + // or if explicitly skipped, we won't run validations. + if (!skipValidation) { + if (parsed.error) throw new YError(parsed.error.message) + + // if we're executed via bash completion, don't + // bother with validation. + if (!requestCompletions) { + self._runValidation(argv, aliases, {}, parsed.error) + } + } + } catch (err) { + if (err instanceof YError) usage.fail(err.message, err) + else throw err + } + + return (populateDoubleDash || _calledFromCommand) ? argv : self._copyDoubleDash(argv) + } + + // to simplify the parsing of positionals in commands, + // we temporarily populate '--' rather than _, with arguments + // after the '--' directive. After the parse, we copy these back. + self._copyDoubleDash = function (argv: Arguments | Promise): any { + if (isPromise(argv) || !argv._ || !argv['--']) return argv + argv._.push.apply(argv._, argv['--']) + + // TODO(bcoe): refactor command parsing such that this delete is not + // necessary: https://github.com/yargs/yargs/issues/1482 + try { + delete argv['--'] + } catch (_err) {} + + return argv + } + + self._runValidation = function runValidation (argv, aliases, positionalMap, parseErrors, isDefaultCommand = false) { + if (parseErrors) throw new YError(parseErrors.message) + validation.nonOptionCount(argv) + validation.requiredArguments(argv) + let failedStrictCommands = false + if (strictCommands) { + failedStrictCommands = validation.unknownCommands(argv) + } + if (strict && !failedStrictCommands) { + validation.unknownArguments(argv, aliases, positionalMap, isDefaultCommand) + } + validation.customChecks(argv, aliases) + validation.limitedChoices(argv) + validation.implications(argv) + validation.conflicting(argv) + } + + function guessLocale () { + if (!detectLocale) return + const locale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE || 'en_US' + self.locale(locale.replace(/[.:].*/, '')) + } + + // an app should almost always have --version and --help, + // if you *really* want to disable this use .help(false)/.version(false). + self.help() + self.version() + + return self +} + +// rebase an absolute path to a relative one with respect to a base directory +// exported for tests +export function rebase (base: string, dir: string) { + return path.relative(base, dir) +} + +/** Instance of the yargs module. */ +export interface YargsInstance { + $0: string + argv: Arguments + customScriptName: boolean + parsed: DetailedArguments | false + _copyDoubleDash> (argv: T): T + _getLoggerInstance (): LoggerInstance + _getParseContext(): Object + _hasOutput (): boolean + _hasParseCallback (): boolean + _parseArgs: { + (args: null, shortCircuit: null, _calledFromCommand: boolean, commandIndex?: number): Arguments | Promise + (args: string | string[], shortCircuit?: boolean): Arguments | Promise + } + _runValidation ( + argv: Arguments, + aliases: Dictionary, + positionalMap: Dictionary, + parseErrors: Error | null, + isDefaultCommand?: boolean + ): void + _setHasOutput (): void + addHelpOpt: { + (opt?: string | false): YargsInstance + (opt?: string, msg?: string): YargsInstance + } + addShowHiddenOpt: { + (opt?: string | false): YargsInstance + (opt?: string, msg?: string): YargsInstance + } + alias: { + (keys: string | string[], aliases: string | string[]): YargsInstance + (keyAliases: Dictionary): YargsInstance + } + array (keys: string | string[]): YargsInstance + boolean (keys: string | string[]): YargsInstance + check(f: (argv: Arguments, aliases: Dictionary) => any, _global?: boolean): YargsInstance + choices: { + (keys: string | string[], choices: string | string[]): YargsInstance + (keyChoices: Dictionary): YargsInstance + } + coerce: { + (keys: string | string[], coerceCallback: CoerceCallback): YargsInstance + (keyCoerceCallbacks: Dictionary): YargsInstance + } + command ( + cmd: string | string[], + description: CommandHandler['description'], + builder ?: CommandBuilderDefinition | CommandBuilder, + handler ?: CommandHandlerCallback, + commandMiddleware ?: Middleware[], + deprecated ?: boolean + ): YargsInstance + commandDir (dir: string, opts?: RequireDirectoryOptions): YargsInstance + completion: { + (cmd?: string, fn?: CompletionFunction): YargsInstance + (cmd?: string, desc?: string | false, fn?: CompletionFunction): YargsInstance + } + config: { + (config: Dictionary): YargsInstance + (keys?: string | string[], configCallback?: ConfigCallback): YargsInstance + (keys?: string | string[], msg?: string, configCallback?: ConfigCallback): YargsInstance + } + conflicts: { + (key: string, conflictsWith: string | string[]): YargsInstance + (keyConflicts: Dictionary): YargsInstance + } + count (keys: string | string[]): YargsInstance + default: { + (key: string, value: any, defaultDescription?: string): YargsInstance + (keys: string[], value: Exclude): YargsInstance + (keys: Dictionary): YargsInstance + } + defaults: YargsInstance['default'] + demand: { + (min: number, max?: number | string, msg?: string): YargsInstance + (keys: string | string[], msg?: string | true): YargsInstance + (keys: string | string[], max: string[], msg?: string | true): YargsInstance + (keyMsgs: Dictionary): YargsInstance + (keyMsgs: Dictionary, max: string[], msg?: string): YargsInstance + } + demandCommand (): YargsInstance + demandCommand (min: number, minMsg?: string): YargsInstance + demandCommand (min: number, max: number, minMsg?: string | null, maxMsg?: string | null): YargsInstance + demandOption: { + (keys: string | string[], msg?: string): YargsInstance + (keyMsgs: Dictionary): YargsInstance + } + deprecateOption (option: string, message?: string | boolean): YargsInstance + describe: { + (keys: string | string[], description?: string): YargsInstance + (keyDescriptions: Dictionary): YargsInstance + } + detectLocale (detect: boolean): YargsInstance + env (prefix?: string | false): YargsInstance + epilog: YargsInstance['epilogue'] + epilogue (msg: string): YargsInstance + example (cmd: string, description?: string): YargsInstance + exit (code: number, err?: YError | string): void + exitProcess (enabled: boolean): YargsInstance + fail (f: FailureFunction): YargsInstance + getCommandInstance (): CommandInstance + getCompletion(args: string[], done: (completions: string[]) => any): void + getContext (): Context + getDemandedCommands (): Options['demandedCommands'] + getDemandedOptions (): Options['demandedOptions'] + getDeprecatedOptions (): Options['deprecatedOptions'] + getDetectLocale(): boolean + getExitProcess (): boolean + getGroups (): Dictionary + getHandlerFinishCommand (): FinishCommandHandler | null + getOptions (): Options + getParserConfiguration (): Configuration + getStrict (): boolean + getStrictCommands (): boolean + getUsageInstance (): UsageInstance + getValidationInstance (): ValidationInstance + global (keys: string | string[], global?: boolean): YargsInstance + group (keys: string| string[], groupName: string): YargsInstance + help: YargsInstance['addHelpOpt'] + hide (key: string): YargsInstance + implies: { + (key: string, implication: KeyOrPos | KeyOrPos[]): YargsInstance + (keyImplications: Dictionary): YargsInstance + } + locale: { + (): string + (locale: string): YargsInstance + } + middleware (callback: MiddlewareCallback | MiddlewareCallback[], applyBeforeValidation?: boolean): YargsInstance + nargs: { + (keys: string | string[], nargs: number): YargsInstance + (keyNargs: Dictionary): YargsInstance + } + normalize (keys: string | string[]): YargsInstance + number (keys: string | string[]): YargsInstance + onFinishCommand (f: FinishCommandHandler): YargsInstance + option: { + (key: string, optionDefinition: OptionDefinition): YargsInstance + (keyOptionDefinitions: Dictionary): YargsInstance + } + options: YargsInstance['option'] + parse: { + (): Arguments | Promise + (args: string | string[], context: object, parseCallback?: ParseCallback): Arguments | Promise + (args: string | string[], parseCallback: ParseCallback): Arguments | Promise + (args: string | string[], shortCircuit: boolean): Arguments | Promise + } + parserConfiguration (config: Configuration): YargsInstance + pkgConf (key: string, rootPath?: string): YargsInstance + positional (key: string, positionalDefinition: PositionalDefinition): YargsInstance + recommendCommands (recommend: boolean): YargsInstance + require: YargsInstance['demand'] + required: YargsInstance['demand'] + requiresArg (keys: string | string[] | Dictionary): YargsInstance + reset (aliases?: DetailedArguments['aliases']): YargsInstance + resetOptions (aliases?: DetailedArguments['aliases']): YargsInstance + scriptName (scriptName: string): YargsInstance + showCompletionScript($0?: string, cmd?: string): YargsInstance + showHelp (level: 'error' | 'log' | ((message: string) => void)): YargsInstance + showHelpOnFail: { + (message?: string): YargsInstance + (enabled: boolean, message: string): YargsInstance + } + showHidden: YargsInstance['addShowHiddenOpt'] + skipValidation (keys: string | string[]): YargsInstance + strict (enable?: boolean): YargsInstance + strictCommands (enable?: boolean): YargsInstance + string (key: string | string []): YargsInstance + terminalWidth (): number | null + updateStrings (obj: Dictionary): YargsInstance + updateLocale: YargsInstance['updateStrings'] + usage: { + (msg: string | null): YargsInstance + ( + msg: string, + description: CommandHandler['description'], + builder?: CommandBuilderDefinition | CommandBuilder, + handler?: CommandHandlerCallback + ): YargsInstance + } + version: { + (ver?: string | false): YargsInstance + (key?: string, ver?: string): YargsInstance + (key?: string, msg?: string, ver?: string): YargsInstance + } + wrap (cols: number | null | undefined): YargsInstance +} + +export function isYargsInstance (y: YargsInstance | void): y is YargsInstance { + return !!y && (typeof y._parseArgs === 'function') +} + +/** Yargs' context. */ +export interface Context { + commands: string[] + files: string[] + fullCommands: string[] +} + +type LoggerInstance = Pick + +export interface Options extends ParserOptions { + __: Y18N['__'] + alias: Dictionary + array: string[] + boolean: string[] + choices: Dictionary + config: Dictionary + configObjects: Dictionary[] + configuration: Configuration + count: string[] + defaultDescription: Dictionary + demandedCommands: Dictionary<{ + min: number, + max: number, + minMsg?: string | null, + maxMsg?: string | null + }> + demandedOptions: Dictionary + deprecatedOptions: Dictionary + hiddenOptions: string[] + /** Manually set keys */ + key: Dictionary + local: string[] + normalize: string[] + number: string[] + showHiddenOpt: string + skipValidation: string[] + string: string[] +} + +export interface Configuration extends Partial { + /** Should a config object be deep-merged with the object config it extends? */ + 'deep-merge-config'?: boolean + /** Should commands be sorted in help? */ + 'sort-commands'?: boolean +} + +export interface OptionDefinition { + alias?: string | string[] + array?: boolean + boolean?: boolean + choices?: string | string[] + coerce?: CoerceCallback + config?: boolean + configParser?: ConfigCallback + conflicts?: string | string[] + count?: boolean + default?: any + defaultDescription?: string + deprecate?: string | boolean + deprecated?: OptionDefinition['deprecate'] + desc?: string + describe?: OptionDefinition['desc'] + description?: OptionDefinition['desc'] + demand?: string | true + demandOption?: OptionDefinition['demand'] + global?: boolean + group?: string + hidden?: boolean + implies?: string | number | KeyOrPos[] + nargs?: number + normalize?: boolean + number?: boolean + require?: OptionDefinition['demand'] + required?: OptionDefinition['demand'] + requiresArg?: boolean + skipValidation?: boolean + string?: boolean + type?: 'array' | 'boolean' | 'count' | 'number' | 'string' +} + +interface PositionalDefinition extends Pick { + type?: 'boolean' | 'number' | 'string' +} + +interface FrozenYargsInstance { + options: Options + configObjects: Dictionary[] + exitProcess: boolean + groups: Dictionary + strict: boolean + strictCommands: boolean + completionCommand: string | null + output: string + exitError: YError | string | undefined | null + hasOutput: boolean + parsed: DetailedArguments | false + parseFn: ParseCallback | null + parseContext: object | null + handlerFinishCommand: FinishCommandHandler | null +} + +interface ParseCallback { + (err: YError | string | undefined | null, argv: Arguments | Promise, output: string): void +} + +export interface Arguments extends ParserArguments { + /** The script name or node command */ + $0: string +} + +export interface DetailedArguments extends ParserDetailedArguments { + argv: Arguments +} diff --git a/package.json b/package.json index 0d41d6316..8117536bd 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "index.js", "yargs.js", "build", - "lib/**/*.js", "locales", "LICENSE" ], @@ -34,7 +33,6 @@ "@types/chai": "^4.2.11", "@types/mocha": "^7.0.2", "@types/node": "^10.0.3", - "@types/yargs-parser": "^15.0.0", "@typescript-eslint/eslint-plugin": "^3.0.0", "@typescript-eslint/parser": "^3.0.0", "c8": "^7.0.0", diff --git a/test/usage.js b/test/usage.js index ba9c1d4ef..ee2aa25d1 100644 --- a/test/usage.js +++ b/test/usage.js @@ -3631,4 +3631,19 @@ describe('usage tests', () => { ) }) }) + + it('should allow setting the same description for several keys', () => { + const r = checkUsage(() => yargs('--help') + .describe(['big', 'small'], 'Packet size') + .parse() + ) + + r.logs[0].split('\n').should.deep.equal([ + 'Options:', + ' --help Show help [boolean]', + ' --version Show version number [boolean]', + ' --big Packet size', + ' --small Packet size' + ]) + }) }) diff --git a/yargs.js b/yargs.js index 91bbf948c..93e8059ef 100644 --- a/yargs.js +++ b/yargs.js @@ -4,1300 +4,11 @@ async function requiresNode8OrGreater () {} requiresNode8OrGreater() -const { argsert } = require('./build/lib/argsert') -const fs = require('fs') -const { command: Command } = require('./build/lib/command') -const { completion: Completion } = require('./build/lib/completion') +const { Yargs, rebase } = require('./build/lib/yargs') const Parser = require('yargs-parser') -const path = require('path') -const { usage: Usage } = require('./build/lib/usage') -const { validation: Validation } = require('./build/lib/validation') -const Y18n = require('y18n') -const { objFilter } = require('./build/lib/obj-filter') -const setBlocking = require('set-blocking') -const { applyExtends } = require('./build/lib/apply-extends') -const { globalMiddlewareFactory } = require('./build/lib/middleware') -const { YError } = require('./build/lib/yerror') -const processArgv = require('./build/lib/process-argv') exports = module.exports = Yargs -function Yargs (processArgs, cwd, parentRequire) { - processArgs = processArgs || [] // handle calling yargs(). - - const self = {} - let command = null - let completion = null - let groups = {} - const globalMiddleware = [] - let output = '' - const preservedGroups = {} - let usage = null - let validation = null - let handlerFinishCommand = null - - const y18n = Y18n({ - directory: path.resolve(__dirname, './locales'), - updateFiles: false - }) - - self.middleware = globalMiddlewareFactory(globalMiddleware, self) - - if (!cwd) cwd = process.cwd() - - self.scriptName = function (scriptName) { - self.customScriptName = true - self.$0 = scriptName - return self - } - - // ignore the node bin, specify this in your - // bin file with #!/usr/bin/env node - if (/\b(node|iojs|electron)(\.exe)?$/.test(process.argv[0])) { - self.$0 = process.argv.slice(1, 2) - } else { - self.$0 = process.argv.slice(0, 1) - } - - self.$0 = self.$0 - .map((x, i) => { - const b = rebase(cwd, x) - return x.match(/^(\/|([a-zA-Z]:)?\\)/) && b.length < x.length ? b : x - }) - .join(' ').trim() - - if (process.env._ !== undefined && processArgv.getProcessArgvBin() === process.env._) { - self.$0 = process.env._.replace( - `${path.dirname(process.execPath)}/`, '' - ) - } - - // use context object to keep track of resets, subcommand execution, etc - // submodules should modify and check the state of context as necessary - const context = { resets: -1, commands: [], fullCommands: [], files: [] } - self.getContext = () => context - - // puts yargs back into an initial state. any keys - // that have been set to "global" will not be reset - // by this action. - let options - self.resetOptions = self.reset = function resetOptions (aliases) { - context.resets++ - aliases = aliases || {} - options = options || {} - // put yargs back into an initial state, this - // logic is used to build a nested command - // hierarchy. - const tmpOptions = {} - tmpOptions.local = options.local ? options.local : [] - tmpOptions.configObjects = options.configObjects ? options.configObjects : [] - - // if a key has been explicitly set as local, - // we should reset it before passing options to command. - const localLookup = {} - tmpOptions.local.forEach((l) => { - localLookup[l] = true - ;(aliases[l] || []).forEach((a) => { - localLookup[a] = true - }) - }) - - // add all groups not set to local to preserved groups - Object.assign( - preservedGroups, - Object.keys(groups).reduce((acc, groupName) => { - const keys = groups[groupName].filter(key => !(key in localLookup)) - if (keys.length > 0) { - acc[groupName] = keys - } - return acc - }, {}) - ) - // groups can now be reset - groups = {} - - const arrayOptions = [ - 'array', 'boolean', 'string', 'skipValidation', - 'count', 'normalize', 'number', - 'hiddenOptions' - ] - - const objectOptions = [ - 'narg', 'key', 'alias', 'default', 'defaultDescription', - 'config', 'choices', 'demandedOptions', 'demandedCommands', 'coerce', - 'deprecatedOptions' - ] - - arrayOptions.forEach((k) => { - tmpOptions[k] = (options[k] || []).filter(k => !localLookup[k]) - }) - - objectOptions.forEach((k) => { - tmpOptions[k] = objFilter(options[k], (k, v) => !localLookup[k]) - }) - - tmpOptions.envPrefix = options.envPrefix - options = tmpOptions - - // if this is the first time being executed, create - // instances of all our helpers -- otherwise just reset. - usage = usage ? usage.reset(localLookup) : Usage(self, y18n) - validation = validation ? validation.reset(localLookup) : Validation(self, usage, y18n) - command = command ? command.reset() : Command(self, usage, validation, globalMiddleware) - if (!completion) completion = Completion(self, usage, command) - - completionCommand = null - output = '' - exitError = null - hasOutput = false - self.parsed = false - - return self - } - self.resetOptions() - - // temporary hack: allow "freezing" of reset-able state for parse(msg, cb) - const frozens = [] - function freeze () { - const frozen = {} - frozens.push(frozen) - frozen.options = options - frozen.configObjects = options.configObjects.slice(0) - frozen.exitProcess = exitProcess - frozen.groups = groups - usage.freeze() - validation.freeze() - command.freeze() - frozen.strict = strict - frozen.strictCommands = strictCommands - frozen.completionCommand = completionCommand - frozen.output = output - frozen.exitError = exitError - frozen.hasOutput = hasOutput - frozen.parsed = self.parsed - frozen.parseFn = parseFn - frozen.parseContext = parseContext - frozen.handlerFinishCommand = handlerFinishCommand - } - function unfreeze () { - const frozen = frozens.pop() - options = frozen.options - options.configObjects = frozen.configObjects - exitProcess = frozen.exitProcess - groups = frozen.groups - output = frozen.output - exitError = frozen.exitError - hasOutput = frozen.hasOutput - self.parsed = frozen.parsed - usage.unfreeze() - validation.unfreeze() - command.unfreeze() - strict = frozen.strict - strictCommands = frozen.strictCommands - completionCommand = frozen.completionCommand - parseFn = frozen.parseFn - parseContext = frozen.parseContext - handlerFinishCommand = frozen.handlerFinishCommand - } - - self.boolean = function (keys) { - argsert('', [keys], arguments.length) - populateParserHintArray('boolean', keys) - return self - } - - self.array = function (keys) { - argsert('', [keys], arguments.length) - populateParserHintArray('array', keys) - return self - } - - self.number = function (keys) { - argsert('', [keys], arguments.length) - populateParserHintArray('number', keys) - return self - } - - self.normalize = function (keys) { - argsert('', [keys], arguments.length) - populateParserHintArray('normalize', keys) - return self - } - - self.count = function (keys) { - argsert('', [keys], arguments.length) - populateParserHintArray('count', keys) - return self - } - - self.string = function (keys) { - argsert('', [keys], arguments.length) - populateParserHintArray('string', keys) - return self - } - - self.requiresArg = function (keys, value) { - argsert(' [number]', [keys], arguments.length) - // If someone configures nargs at the same time as requiresArg, - // nargs should take precedent, - // see: https://github.com/yargs/yargs/pull/1572 - // TODO: make this work with aliases, using a check similar to - // checkAllAliases() in yargs-parser. - if (typeof keys === 'string' && options.narg[keys]) { - return self - } else { - populateParserHintObject(self.requiresArg, false, 'narg', keys, NaN) - } - return self - } - - self.skipValidation = function (keys) { - argsert('', [keys], arguments.length) - populateParserHintArray('skipValidation', keys) - return self - } - - function populateParserHintArray (type, keys, value) { - keys = [].concat(keys) - keys.forEach((key) => { - key = sanitizeKey(key) - options[type].push(key) - }) - } - - self.nargs = function (key, value) { - argsert(' [number]', [key, value], arguments.length) - populateParserHintObject(self.nargs, false, 'narg', key, value) - return self - } - - self.choices = function (key, value) { - argsert(' [string|array]', [key, value], arguments.length) - populateParserHintObject(self.choices, true, 'choices', key, value) - return self - } - - self.alias = function (key, value) { - argsert(' [string|array]', [key, value], arguments.length) - populateParserHintObject(self.alias, true, 'alias', key, value) - return self - } - - // TODO: actually deprecate self.defaults. - self.default = self.defaults = function (key, value, defaultDescription) { - argsert(' [*] [string]', [key, value, defaultDescription], arguments.length) - if (defaultDescription) options.defaultDescription[key] = defaultDescription - if (typeof value === 'function') { - if (!options.defaultDescription[key]) options.defaultDescription[key] = usage.functionDescription(value) - value = value.call() - } - populateParserHintObject(self.default, false, 'default', key, value) - return self - } - - self.describe = function (key, desc) { - argsert(' [string]', [key, desc], arguments.length) - populateParserHintObject(self.describe, false, 'key', key, true) - usage.describe(key, desc) - return self - } - - self.demandOption = function (keys, msg) { - argsert(' [string]', [keys, msg], arguments.length) - populateParserHintObject(self.demandOption, false, 'demandedOptions', keys, msg) - return self - } - - self.coerce = function (keys, value) { - argsert(' [function]', [keys, value], arguments.length) - populateParserHintObject(self.coerce, false, 'coerce', keys, value) - return self - } - - function populateParserHintObject (builder, isArray, type, key, value) { - if (Array.isArray(key)) { - const temp = Object.create(null) - // an array of keys with one value ['x', 'y', 'z'], function parse () {} - key.forEach((k) => { - temp[k] = value - }) - builder(temp) - } else if (typeof key === 'object') { - // an object of key value pairs: {'x': parse () {}, 'y': parse() {}} - Object.keys(key).forEach((k) => { - builder(k, key[k]) - }) - } else { - key = sanitizeKey(key) - // a single key value pair 'x', parse() {} - if (isArray) { - options[type][key] = (options[type][key] || []).concat(value) - } else { - options[type][key] = value - } - } - } - - // TODO(bcoe): in future major versions move more objects towards - // Object.create(null): - function sanitizeKey (key) { - if (key === '__proto__') return '___proto___' - return key - } - - function deleteFromParserHintObject (optionKey) { - // delete from all parsing hints: - // boolean, array, key, alias, etc. - Object.keys(options).forEach((hintKey) => { - const hint = options[hintKey] - if (Array.isArray(hint)) { - if (~hint.indexOf(optionKey)) hint.splice(hint.indexOf(optionKey), 1) - } else if (typeof hint === 'object') { - delete hint[optionKey] - } - }) - // now delete the description from usage.js. - delete usage.getDescriptions()[optionKey] - } - - self.config = function config (key, msg, parseFn) { - argsert('[object|string] [string|function] [function]', [key, msg, parseFn], arguments.length) - // allow a config object to be provided directly. - if (typeof key === 'object') { - key = applyExtends(key, cwd, self.getParserConfiguration()['deep-merge-config']) - options.configObjects = (options.configObjects || []).concat(key) - return self - } - - // allow for a custom parsing function. - if (typeof msg === 'function') { - parseFn = msg - msg = null - } - - key = key || 'config' - self.describe(key, msg || usage.deferY18nLookup('Path to JSON config file')) - ;(Array.isArray(key) ? key : [key]).forEach((k) => { - options.config[k] = parseFn || true - }) - - return self - } - - self.example = function (cmd, description) { - argsert(' [string]', [cmd, description], arguments.length) - usage.example(cmd, description) - return self - } - - self.command = function (cmd, description, builder, handler, middlewares, deprecated) { - argsert(' [string|boolean] [function|object] [function] [array] [boolean|string]', [cmd, description, builder, handler, middlewares, deprecated], arguments.length) - command.addHandler(cmd, description, builder, handler, middlewares, deprecated) - return self - } - - self.commandDir = function (dir, opts) { - argsert(' [object]', [dir, opts], arguments.length) - const req = parentRequire || require - command.addDirectory(dir, self.getContext(), req, require('get-caller-file')(), opts) - return self - } - - // TODO: deprecate self.demand in favor of - // .demandCommand() .demandOption(). - self.demand = self.required = self.require = function demand (keys, max, msg) { - // you can optionally provide a 'max' key, - // which will raise an exception if too many '_' - // options are provided. - if (Array.isArray(max)) { - max.forEach((key) => { - self.demandOption(key, msg) - }) - max = Infinity - } else if (typeof max !== 'number') { - msg = max - max = Infinity - } - - if (typeof keys === 'number') { - self.demandCommand(keys, max, msg, msg) - } else if (Array.isArray(keys)) { - keys.forEach((key) => { - self.demandOption(key, msg) - }) - } else { - if (typeof msg === 'string') { - self.demandOption(keys, msg) - } else if (msg === true || typeof msg === 'undefined') { - self.demandOption(keys) - } - } - - return self - } - - self.demandCommand = function demandCommand (min, max, minMsg, maxMsg) { - argsert('[number] [number|string] [string|null|undefined] [string|null|undefined]', [min, max, minMsg, maxMsg], arguments.length) - - if (typeof min === 'undefined') min = 1 - - if (typeof max !== 'number') { - minMsg = max - max = Infinity - } - - self.global('_', false) - - options.demandedCommands._ = { - min, - max, - minMsg, - maxMsg - } - - return self - } - - self.getDemandedOptions = () => { - argsert([], 0) - return options.demandedOptions - } - - self.getDemandedCommands = () => { - argsert([], 0) - return options.demandedCommands - } - - self.deprecateOption = function deprecateOption (option, message) { - argsert(' [string|boolean]', [option, message], arguments.length) - options.deprecatedOptions[option] = message - return self - } - - self.getDeprecatedOptions = () => { - argsert([], 0) - return options.deprecatedOptions - } - - self.implies = function (key, value) { - argsert(' [number|string|array]', [key, value], arguments.length) - validation.implies(key, value) - return self - } - - self.conflicts = function (key1, key2) { - argsert(' [string|array]', [key1, key2], arguments.length) - validation.conflicts(key1, key2) - return self - } - - self.usage = function (msg, description, builder, handler) { - argsert(' [string|boolean] [function|object] [function]', [msg, description, builder, handler], arguments.length) - - if (description !== undefined) { - // .usage() can be used as an alias for defining - // a default command. - if ((msg || '').match(/^\$0( |$)/)) { - return self.command(msg, description, builder, handler) - } else { - throw new YError('.usage() description must start with $0 if being used as alias for .command()') - } - } else { - usage.usage(msg) - return self - } - } - - self.epilogue = self.epilog = function (msg) { - argsert('', [msg], arguments.length) - usage.epilog(msg) - return self - } - - self.fail = function (f) { - argsert('', [f], arguments.length) - usage.failFn(f) - return self - } - - self.onFinishCommand = function (f) { - argsert('', [f], arguments.length) - handlerFinishCommand = f - return self - } - - self.getHandlerFinishCommand = () => handlerFinishCommand - - self.check = function (f, _global) { - argsert(' [boolean]', [f, _global], arguments.length) - validation.check(f, _global !== false) - return self - } - - self.global = function global (globals, global) { - argsert(' [boolean]', [globals, global], arguments.length) - globals = [].concat(globals) - if (global !== false) { - options.local = options.local.filter(l => globals.indexOf(l) === -1) - } else { - globals.forEach((g) => { - if (options.local.indexOf(g) === -1) options.local.push(g) - }) - } - return self - } - - self.pkgConf = function pkgConf (key, rootPath) { - argsert(' [string]', [key, rootPath], arguments.length) - let conf = null - // prefer cwd to require-main-filename in this method - // since we're looking for e.g. "nyc" config in nyc consumer - // rather than "yargs" config in nyc (where nyc is the main filename) - const obj = pkgUp(rootPath || cwd) - - // If an object exists in the key, add it to options.configObjects - if (obj[key] && typeof obj[key] === 'object') { - conf = applyExtends(obj[key], rootPath || cwd, self.getParserConfiguration()['deep-merge-config']) - options.configObjects = (options.configObjects || []).concat(conf) - } - - return self - } - - const pkgs = {} - function pkgUp (rootPath) { - const npath = rootPath || '*' - if (pkgs[npath]) return pkgs[npath] - const findUp = require('find-up') - - let obj = {} - try { - let startDir = rootPath || require('require-main-filename')(parentRequire || require) - - // When called in an environment that lacks require.main.filename, such as a jest test runner, - // startDir is already process.cwd(), and should not be shortened. - // Whether or not it is _actually_ a directory (e.g., extensionless bin) is irrelevant, find-up handles it. - if (!rootPath && path.extname(startDir)) { - startDir = path.dirname(startDir) - } - - const pkgJsonPath = findUp.sync('package.json', { - cwd: startDir - }) - obj = JSON.parse(fs.readFileSync(pkgJsonPath)) - } catch (noop) {} - - pkgs[npath] = obj || {} - return pkgs[npath] - } - - let parseFn = null - let parseContext = null - self.parse = function parse (args, shortCircuit, _parseFn) { - argsert('[string|array] [function|boolean|object] [function]', [args, shortCircuit, _parseFn], arguments.length) - freeze() - if (typeof args === 'undefined') { - const argv = self._parseArgs(processArgs) - const tmpParsed = self.parsed - unfreeze() - // TODO: remove this compatibility hack when we release yargs@15.x: - self.parsed = tmpParsed - return argv - } - - // a context object can optionally be provided, this allows - // additional information to be passed to a command handler. - if (typeof shortCircuit === 'object') { - parseContext = shortCircuit - shortCircuit = _parseFn - } - - // by providing a function as a second argument to - // parse you can capture output that would otherwise - // default to printing to stdout/stderr. - if (typeof shortCircuit === 'function') { - parseFn = shortCircuit - shortCircuit = null - } - // completion short-circuits the parsing process, - // skipping validation, etc. - if (!shortCircuit) processArgs = args - - if (parseFn) exitProcess = false - - const parsed = self._parseArgs(args, shortCircuit) - completion.setParsed(self.parsed) - if (parseFn) parseFn(exitError, parsed, output) - unfreeze() - - return parsed - } - - self._getParseContext = () => parseContext || {} - - self._hasParseCallback = () => !!parseFn - - self.option = self.options = function option (key, opt) { - argsert(' [object]', [key, opt], arguments.length) - if (typeof key === 'object') { - Object.keys(key).forEach((k) => { - self.options(k, key[k]) - }) - } else { - if (typeof opt !== 'object') { - opt = {} - } - - options.key[key] = true // track manually set keys. - - if (opt.alias) self.alias(key, opt.alias) - - const deprecate = opt.deprecate || opt.deprecated - - if (deprecate) { - self.deprecateOption(key, deprecate) - } - - const demand = opt.demand || opt.required || opt.require - - // A required option can be specified via "demand: true". - if (demand) { - self.demand(key, demand) - } - - if (opt.demandOption) { - self.demandOption(key, typeof opt.demandOption === 'string' ? opt.demandOption : undefined) - } - - if ('conflicts' in opt) { - self.conflicts(key, opt.conflicts) - } - - if ('default' in opt) { - self.default(key, opt.default) - } - - if ('implies' in opt) { - self.implies(key, opt.implies) - } - - if ('nargs' in opt) { - self.nargs(key, opt.nargs) - } - - if (opt.config) { - self.config(key, opt.configParser) - } - - if (opt.normalize) { - self.normalize(key) - } - - if ('choices' in opt) { - self.choices(key, opt.choices) - } - - if ('coerce' in opt) { - self.coerce(key, opt.coerce) - } - - if ('group' in opt) { - self.group(key, opt.group) - } - - if (opt.boolean || opt.type === 'boolean') { - self.boolean(key) - if (opt.alias) self.boolean(opt.alias) - } - - if (opt.array || opt.type === 'array') { - self.array(key) - if (opt.alias) self.array(opt.alias) - } - - if (opt.number || opt.type === 'number') { - self.number(key) - if (opt.alias) self.number(opt.alias) - } - - if (opt.string || opt.type === 'string') { - self.string(key) - if (opt.alias) self.string(opt.alias) - } - - if (opt.count || opt.type === 'count') { - self.count(key) - } - - if (typeof opt.global === 'boolean') { - self.global(key, opt.global) - } - - if (opt.defaultDescription) { - options.defaultDescription[key] = opt.defaultDescription - } - - if (opt.skipValidation) { - self.skipValidation(key) - } - - const desc = opt.describe || opt.description || opt.desc - self.describe(key, desc) - if (opt.hidden) { - self.hide(key) - } - - if (opt.requiresArg) { - self.requiresArg(key) - } - } - - return self - } - self.getOptions = () => options - - self.positional = function (key, opts) { - argsert(' ', [key, opts], arguments.length) - if (context.resets === 0) { - throw new YError(".positional() can only be called in a command's builder function") - } - - // .positional() only supports a subset of the configuration - // options available to .option(). - const supportedOpts = ['default', 'defaultDescription', 'implies', 'normalize', - 'choices', 'conflicts', 'coerce', 'type', 'describe', - 'desc', 'description', 'alias'] - opts = objFilter(opts, (k, v) => { - let accept = supportedOpts.indexOf(k) !== -1 - // type can be one of string|number|boolean. - if (k === 'type' && ['string', 'number', 'boolean'].indexOf(v) === -1) accept = false - return accept - }) - - // copy over any settings that can be inferred from the command string. - const fullCommand = context.fullCommands[context.fullCommands.length - 1] - const parseOptions = fullCommand ? command.cmdToParseOptions(fullCommand) : { - array: [], - alias: {}, - default: {}, - demand: {} - } - Object.keys(parseOptions).forEach((pk) => { - if (Array.isArray(parseOptions[pk])) { - if (parseOptions[pk].indexOf(key) !== -1) opts[pk] = true - } else { - if (parseOptions[pk][key] && !(pk in opts)) opts[pk] = parseOptions[pk][key] - } - }) - self.group(key, usage.getPositionalGroupName()) - return self.option(key, opts) - } - - self.group = function group (opts, groupName) { - argsert(' ', [opts, groupName], arguments.length) - const existing = preservedGroups[groupName] || groups[groupName] - if (preservedGroups[groupName]) { - // we now only need to track this group name in groups. - delete preservedGroups[groupName] - } - - const seen = {} - groups[groupName] = (existing || []).concat(opts).filter((key) => { - if (seen[key]) return false - return (seen[key] = true) - }) - return self - } - // combine explicit and preserved groups. explicit groups should be first - self.getGroups = () => Object.assign({}, groups, preservedGroups) - - // as long as options.envPrefix is not undefined, - // parser will apply env vars matching prefix to argv - self.env = function (prefix) { - argsert('[string|boolean]', [prefix], arguments.length) - if (prefix === false) options.envPrefix = undefined - else options.envPrefix = prefix || '' - return self - } - - self.wrap = function (cols) { - argsert('', [cols], arguments.length) - usage.wrap(cols) - return self - } - - let strict = false - self.strict = function (enabled) { - argsert('[boolean]', [enabled], arguments.length) - strict = enabled !== false - return self - } - self.getStrict = () => strict - - let strictCommands = false - self.strictCommands = function (enabled) { - argsert('[boolean]', [enabled], arguments.length) - strictCommands = enabled !== false - return self - } - self.getStrictCommands = () => strictCommands - - let parserConfig = {} - self.parserConfiguration = function parserConfiguration (config) { - argsert('', [config], arguments.length) - parserConfig = config - return self - } - self.getParserConfiguration = () => parserConfig - - self.showHelp = function (level) { - argsert('[string|function]', [level], arguments.length) - if (!self.parsed) self._parseArgs(processArgs) // run parser, if it has not already been executed. - if (command.hasDefaultCommand()) { - context.resets++ // override the restriction on top-level positoinals. - command.runDefaultBuilderOn(self, true) - } - usage.showHelp(level) - return self - } - - let versionOpt = null - self.version = function version (opt, msg, ver) { - const defaultVersionOpt = 'version' - argsert('[boolean|string] [string] [string]', [opt, msg, ver], arguments.length) - - // nuke the key previously configured - // to return version #. - if (versionOpt) { - deleteFromParserHintObject(versionOpt) - usage.version(undefined) - versionOpt = null - } - - if (arguments.length === 0) { - ver = guessVersion() - opt = defaultVersionOpt - } else if (arguments.length === 1) { - if (opt === false) { // disable default 'version' key. - return self - } - ver = opt - opt = defaultVersionOpt - } else if (arguments.length === 2) { - ver = msg - msg = null - } - - versionOpt = typeof opt === 'string' ? opt : defaultVersionOpt - msg = msg || usage.deferY18nLookup('Show version number') - - usage.version(ver || undefined) - self.boolean(versionOpt) - self.describe(versionOpt, msg) - return self - } - - function guessVersion () { - const obj = pkgUp() - - return obj.version || 'unknown' - } - - let helpOpt = null - self.addHelpOpt = self.help = function addHelpOpt (opt, msg) { - const defaultHelpOpt = 'help' - argsert('[string|boolean] [string]', [opt, msg], arguments.length) - - // nuke the key previously configured - // to return help. - if (helpOpt) { - deleteFromParserHintObject(helpOpt) - helpOpt = null - } - - if (arguments.length === 1) { - if (opt === false) return self - } - - // use arguments, fallback to defaults for opt and msg - helpOpt = typeof opt === 'string' ? opt : defaultHelpOpt - self.boolean(helpOpt) - self.describe(helpOpt, msg || usage.deferY18nLookup('Show help')) - return self - } - - const defaultShowHiddenOpt = 'show-hidden' - options.showHiddenOpt = defaultShowHiddenOpt - self.addShowHiddenOpt = self.showHidden = function addShowHiddenOpt (opt, msg) { - argsert('[string|boolean] [string]', [opt, msg], arguments.length) - - if (arguments.length === 1) { - if (opt === false) return self - } - - const showHiddenOpt = typeof opt === 'string' ? opt : defaultShowHiddenOpt - self.boolean(showHiddenOpt) - self.describe(showHiddenOpt, msg || usage.deferY18nLookup('Show hidden options')) - options.showHiddenOpt = showHiddenOpt - return self - } - - self.hide = function hide (key) { - argsert('', [key], arguments.length) - options.hiddenOptions.push(key) - return self - } - - self.showHelpOnFail = function showHelpOnFail (enabled, message) { - argsert('[boolean|string] [string]', [enabled, message], arguments.length) - usage.showHelpOnFail(enabled, message) - return self - } - - var exitProcess = true - self.exitProcess = function (enabled) { - argsert('[boolean]', [enabled], arguments.length) - if (typeof enabled !== 'boolean') { - enabled = true - } - exitProcess = enabled - return self - } - self.getExitProcess = () => exitProcess - - var completionCommand = null - self.completion = function (cmd, desc, fn) { - argsert('[string] [string|boolean|function] [function]', [cmd, desc, fn], arguments.length) - - // a function to execute when generating - // completions can be provided as the second - // or third argument to completion. - if (typeof desc === 'function') { - fn = desc - desc = null - } - - // register the completion command. - completionCommand = cmd || completionCommand || 'completion' - if (!desc && desc !== false) { - desc = 'generate completion script' - } - self.command(completionCommand, desc) - - // a function can be provided - if (fn) completion.registerFunction(fn) - - return self - } - - self.showCompletionScript = function ($0, cmd) { - argsert('[string] [string]', [$0, cmd], arguments.length) - $0 = $0 || self.$0 - _logger.log(completion.generateCompletionScript($0, cmd || completionCommand || 'completion')) - return self - } - - self.getCompletion = function (args, done) { - argsert(' ', [args, done], arguments.length) - completion.getCompletion(args, done) - } - - self.locale = function (locale) { - argsert('[string]', [locale], arguments.length) - if (arguments.length === 0) { - guessLocale() - return y18n.getLocale() - } - detectLocale = false - y18n.setLocale(locale) - return self - } - - self.updateStrings = self.updateLocale = function (obj) { - argsert('', [obj], arguments.length) - detectLocale = false - y18n.updateLocale(obj) - return self - } - - let detectLocale = true - self.detectLocale = function (detect) { - argsert('', [detect], arguments.length) - detectLocale = detect - return self - } - self.getDetectLocale = () => detectLocale - - var hasOutput = false - var exitError = null - // maybe exit, always capture - // context about why we wanted to exit. - self.exit = (code, err) => { - hasOutput = true - exitError = err - if (exitProcess) process.exit(code) - } - - // we use a custom logger that buffers output, - // so that we can print to non-CLIs, e.g., chat-bots. - const _logger = { - log () { - const args = [] - for (let i = 0; i < arguments.length; i++) args.push(arguments[i]) - if (!self._hasParseCallback()) console.log.apply(console, args) - hasOutput = true - if (output.length) output += '\n' - output += args.join(' ') - }, - error () { - const args = [] - for (let i = 0; i < arguments.length; i++) args.push(arguments[i]) - if (!self._hasParseCallback()) console.error.apply(console, args) - hasOutput = true - if (output.length) output += '\n' - output += args.join(' ') - } - } - self._getLoggerInstance = () => _logger - // has yargs output an error our help - // message in the current execution context. - self._hasOutput = () => hasOutput - - self._setHasOutput = () => { - hasOutput = true - } - - let recommendCommands - self.recommendCommands = function (recommend) { - argsert('[boolean]', [recommend], arguments.length) - recommendCommands = typeof recommend === 'boolean' ? recommend : true - return self - } - - self.getUsageInstance = () => usage - - self.getValidationInstance = () => validation - - self.getCommandInstance = () => command - - self.terminalWidth = () => { - argsert([], 0) - return typeof process.stdout.columns !== 'undefined' ? process.stdout.columns : null - } - - Object.defineProperty(self, 'argv', { - get: () => self._parseArgs(processArgs), - enumerable: true - }) - - self._parseArgs = function parseArgs (args, shortCircuit, _calledFromCommand, commandIndex) { - let skipValidation = !!_calledFromCommand - args = args || processArgs - - options.__ = y18n.__ - options.configuration = self.getParserConfiguration() - - const populateDoubleDash = !!options.configuration['populate--'] - const config = Object.assign({}, options.configuration, { - 'populate--': true - }) - const parsed = Parser.detailed(args, Object.assign({}, options, { - configuration: config - })) - - let argv = parsed.argv - if (parseContext) argv = Object.assign({}, argv, parseContext) - const aliases = parsed.aliases - - argv.$0 = self.$0 - self.parsed = parsed - - try { - guessLocale() // guess locale lazily, so that it can be turned off in chain. - - // while building up the argv object, there - // are two passes through the parser. If completion - // is being performed short-circuit on the first pass. - if (shortCircuit) { - return (populateDoubleDash || _calledFromCommand) ? argv : self._copyDoubleDash(argv) - } - - // if there's a handler associated with a - // command defer processing to it. - if (helpOpt) { - // consider any multi-char helpOpt alias as a valid help command - // unless all helpOpt aliases are single-char - // note that parsed.aliases is a normalized bidirectional map :) - const helpCmds = [helpOpt] - .concat(aliases[helpOpt] || []) - .filter(k => k.length > 1) - // check if help should trigger and strip it from _. - if (~helpCmds.indexOf(argv._[argv._.length - 1])) { - argv._.pop() - argv[helpOpt] = true - } - } - - const handlerKeys = command.getCommands() - const requestCompletions = completion.completionKey in argv - const skipRecommendation = argv[helpOpt] || requestCompletions - const skipDefaultCommand = skipRecommendation && (handlerKeys.length > 1 || handlerKeys[0] !== '$0') - - if (argv._.length) { - if (handlerKeys.length) { - let firstUnknownCommand - for (let i = (commandIndex || 0), cmd; argv._[i] !== undefined; i++) { - cmd = String(argv._[i]) - if (~handlerKeys.indexOf(cmd) && cmd !== completionCommand) { - // commands are executed using a recursive algorithm that executes - // the deepest command first; we keep track of the position in the - // argv._ array that is currently being executed. - const innerArgv = command.runCommand(cmd, self, parsed, i + 1) - return populateDoubleDash ? innerArgv : self._copyDoubleDash(innerArgv) - } else if (!firstUnknownCommand && cmd !== completionCommand) { - firstUnknownCommand = cmd - break - } - } - - // run the default command, if defined - if (command.hasDefaultCommand() && !skipDefaultCommand) { - const innerArgv = command.runCommand(null, self, parsed) - return populateDoubleDash ? innerArgv : self._copyDoubleDash(innerArgv) - } - - // recommend a command if recommendCommands() has - // been enabled, and no commands were found to execute - if (recommendCommands && firstUnknownCommand && !skipRecommendation) { - validation.recommendCommands(firstUnknownCommand, handlerKeys) - } - } - - // generate a completion script for adding to ~/.bashrc. - if (completionCommand && ~argv._.indexOf(completionCommand) && !requestCompletions) { - if (exitProcess) setBlocking(true) - self.showCompletionScript() - self.exit(0) - } - } else if (command.hasDefaultCommand() && !skipDefaultCommand) { - const innerArgv = command.runCommand(null, self, parsed) - return populateDoubleDash ? innerArgv : self._copyDoubleDash(innerArgv) - } - - // we must run completions first, a user might - // want to complete the --help or --version option. - if (requestCompletions) { - if (exitProcess) setBlocking(true) - - // we allow for asynchronous completions, - // e.g., loading in a list of commands from an API. - const completionArgs = args.slice(args.indexOf(`--${completion.completionKey}`) + 1) - completion.getCompletion(completionArgs, (completions) => { - ;(completions || []).forEach((completion) => { - _logger.log(completion) - }) - - self.exit(0) - }) - return (populateDoubleDash || _calledFromCommand) ? argv : self._copyDoubleDash(argv) - } - - // Handle 'help' and 'version' options - // if we haven't already output help! - if (!hasOutput) { - Object.keys(argv).forEach((key) => { - if (key === helpOpt && argv[key]) { - if (exitProcess) setBlocking(true) - - skipValidation = true - self.showHelp('log') - self.exit(0) - } else if (key === versionOpt && argv[key]) { - if (exitProcess) setBlocking(true) - - skipValidation = true - usage.showVersion() - self.exit(0) - } - }) - } - - // Check if any of the options to skip validation were provided - if (!skipValidation && options.skipValidation.length > 0) { - skipValidation = Object.keys(argv).some(key => options.skipValidation.indexOf(key) >= 0 && argv[key] === true) - } - - // If the help or version options where used and exitProcess is false, - // or if explicitly skipped, we won't run validations. - if (!skipValidation) { - if (parsed.error) throw new YError(parsed.error.message) - - // if we're executed via bash completion, don't - // bother with validation. - if (!requestCompletions) { - self._runValidation(argv, aliases, {}, parsed.error) - } - } - } catch (err) { - if (err instanceof YError) usage.fail(err.message, err) - else throw err - } - - return (populateDoubleDash || _calledFromCommand) ? argv : self._copyDoubleDash(argv) - } - - // to simplify the parsing of positionals in commands, - // we temporarily populate '--' rather than _, with arguments - // after the '--' directive. After the parse, we copy these back. - self._copyDoubleDash = function (argv) { - if (!argv._ || !argv['--']) return argv - argv._.push.apply(argv._, argv['--']) - - // TODO(bcoe): refactor command parsing such that this delete is not - // necessary: https://github.com/yargs/yargs/issues/1482 - try { - delete argv['--'] - } catch (_err) {} - - return argv - } - - self._runValidation = function runValidation (argv, aliases, positionalMap, parseErrors, isDefaultCommand = false) { - if (parseErrors) throw new YError(parseErrors.message) - validation.nonOptionCount(argv) - validation.requiredArguments(argv) - let failedStrictCommands = false - if (strictCommands) { - failedStrictCommands = validation.unknownCommands(argv) - } - if (strict && !failedStrictCommands) { - validation.unknownArguments(argv, aliases, positionalMap, isDefaultCommand) - } - validation.customChecks(argv, aliases) - validation.limitedChoices(argv) - validation.implications(argv) - validation.conflicting(argv) - } - - function guessLocale () { - if (!detectLocale) return - const locale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE || 'en_US' - self.locale(locale.replace(/[.:].*/, '')) - } - - // an app should almost always have --version and --help, - // if you *really* want to disable this use .help(false)/.version(false). - self.help() - self.version() - - return self -} +exports.rebase = rebase // allow consumers to directly use the version of yargs-parser used by yargs exports.Parser = Parser - -// rebase an absolute path to a relative one with respect to a base directory -// exported for tests -exports.rebase = rebase -function rebase (base, dir) { - return path.relative(base, dir) -}