'use strict'; const path = require('path'); const buildParserOptions = require('minimist-options'); const parseArguments = require('yargs-parser'); const camelCaseKeys = require('camelcase-keys'); const decamelize = require('decamelize'); const decamelizeKeys = require('decamelize-keys'); const trimNewlines = require('trim-newlines'); const redent = require('redent'); const readPkgUp = require('read-pkg-up'); const hardRejection = require('hard-rejection'); const normalizePackageData = require('normalize-package-data'); // Prevent caching of this module so module.parent is always accurate delete require.cache[__filename]; const parentDir = path.dirname(module.parent && module.parent.filename ? module.parent.filename : '.'); const isFlagMissing = (flagName, definedFlags, receivedFlags, input) => { const flag = definedFlags[flagName]; let isFlagRequired = true; if (typeof flag.isRequired === 'function') { isFlagRequired = flag.isRequired(receivedFlags, input); if (typeof isFlagRequired !== 'boolean') { throw new TypeError(`Return value for isRequired callback should be of type boolean, but ${typeof isFlagRequired} was returned.`); } } if (typeof receivedFlags[flagName] === 'undefined') { return isFlagRequired; } return flag.isMultiple && receivedFlags[flagName].length === 0; }; const getMissingRequiredFlags = (flags, receivedFlags, input) => { const missingRequiredFlags = []; if (typeof flags === 'undefined') { return []; } for (const flagName of Object.keys(flags)) { if (flags[flagName].isRequired && isFlagMissing(flagName, flags, receivedFlags, input)) { missingRequiredFlags.push({key: flagName, ...flags[flagName]}); } } return missingRequiredFlags; }; const reportMissingRequiredFlags = missingRequiredFlags => { console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`); for (const flag of missingRequiredFlags) { console.error(`\t--${decamelize(flag.key, '-')}${flag.alias ? `, -${flag.alias}` : ''}`); } }; const validateOptions = ({flags}) => { const invalidFlags = Object.keys(flags).filter(flagKey => flagKey.includes('-') && flagKey !== '--'); if (invalidFlags.length > 0) { throw new Error(`Flag keys may not contain '-': ${invalidFlags.join(', ')}`); } }; const reportUnknownFlags = unknownFlags => { console.error([ `Unknown flag${unknownFlags.length > 1 ? 's' : ''}`, ...unknownFlags ].join('\n')); }; const buildParserFlags = ({flags, booleanDefault}) => { const parserFlags = {}; for (const [flagKey, flagValue] of Object.entries(flags)) { const flag = {...flagValue}; if ( typeof booleanDefault !== 'undefined' && flag.type === 'boolean' && !Object.prototype.hasOwnProperty.call(flag, 'default') ) { flag.default = flag.isMultiple ? [booleanDefault] : booleanDefault; } if (flag.isMultiple) { flag.type = flag.type ? `${flag.type}-array` : 'array'; flag.default = flag.default || []; delete flag.isMultiple; } parserFlags[flagKey] = flag; } return parserFlags; }; const validateFlags = (flags, options) => { for (const [flagKey, flagValue] of Object.entries(options.flags)) { if (flagKey !== '--' && !flagValue.isMultiple && Array.isArray(flags[flagKey])) { throw new Error(`The flag --${flagKey} can only be set once.`); } } }; const meow = (helpText, options) => { if (typeof helpText !== 'string') { options = helpText; helpText = ''; } const foundPkg = readPkgUp.sync({ cwd: parentDir, normalize: false }); options = { pkg: foundPkg ? foundPkg.packageJson : {}, argv: process.argv.slice(2), flags: {}, inferType: false, input: 'string', help: helpText, autoHelp: true, autoVersion: true, booleanDefault: false, hardRejection: true, allowUnknownFlags: true, ...options }; if (options.hardRejection) { hardRejection(); } validateOptions(options); let parserOptions = { arguments: options.input, ...buildParserFlags(options) }; parserOptions = decamelizeKeys(parserOptions, '-', {exclude: ['stopEarly', '--']}); if (options.inferType) { delete parserOptions.arguments; } parserOptions = buildParserOptions(parserOptions); parserOptions.configuration = { ...parserOptions.configuration, 'greedy-arrays': false }; if (parserOptions['--']) { parserOptions.configuration['populate--'] = true; } if (!options.allowUnknownFlags) { // Collect unknown options in `argv._` to be checked later. parserOptions.configuration['unknown-options-as-args'] = true; } const {pkg} = options; const argv = parseArguments(options.argv, parserOptions); let help = redent(trimNewlines((options.help || '').replace(/\t+\n*$/, '')), 2); normalizePackageData(pkg); process.title = pkg.bin ? Object.keys(pkg.bin)[0] : pkg.name; let {description} = options; if (!description && description !== false) { ({description} = pkg); } help = (description ? `\n ${description}\n` : '') + (help ? `\n${help}\n` : '\n'); const showHelp = code => { console.log(help); process.exit(typeof code === 'number' ? code : 2); }; const showVersion = () => { console.log(typeof options.version === 'string' ? options.version : pkg.version); process.exit(0); }; if (argv._.length === 0 && options.argv.length === 1) { if (argv.version === true && options.autoVersion) { showVersion(); } if (argv.help === true && options.autoHelp) { showHelp(0); } } const input = argv._; delete argv._; if (!options.allowUnknownFlags) { const unknownFlags = input.filter(item => typeof item === 'string' && item.startsWith('-')); if (unknownFlags.length > 0) { reportUnknownFlags(unknownFlags); process.exit(2); } } const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]}); const unnormalizedFlags = {...flags}; validateFlags(flags, options); for (const flagValue of Object.values(options.flags)) { delete flags[flagValue.alias]; } const missingRequiredFlags = getMissingRequiredFlags(options.flags, flags, input); if (missingRequiredFlags.length > 0) { reportMissingRequiredFlags(missingRequiredFlags); process.exit(2); } return { input, flags, unnormalizedFlags, pkg, help, showHelp, showVersion }; }; module.exports = meow;