├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── license ├── meow.gif ├── package.json ├── readme.md ├── rollup.config.js ├── source ├── index.d.ts ├── index.js ├── options.js ├── parser.js ├── utils.js └── validate.js ├── test-d └── index.ts ├── test ├── _utils.js ├── build.js ├── fixtures │ ├── allow-unknown-flags │ │ ├── fixture-with-help.js │ │ └── fixture.js │ ├── build.js │ ├── fixture.js │ ├── help │ │ ├── fixture.js │ │ └── package.json │ ├── required │ │ ├── fixture-conditional-required-multiple.js │ │ ├── fixture-required-function.js │ │ ├── fixture-required-multiple.js │ │ └── fixture.js │ ├── version │ │ ├── fixture.js │ │ └── package.json │ └── with-package-json │ │ ├── custom-bin │ │ ├── fixture.js │ │ └── package.json │ │ ├── default │ │ ├── fixture.js │ │ └── package.json │ │ └── no-bin │ │ ├── fixture.js │ │ └── package.json ├── flags │ ├── _utils.js │ ├── aliases.js │ ├── allow-unknown-flags.js │ ├── boolean-default.js │ ├── choices.js │ ├── is-multiple.js │ ├── is-required.js │ ├── short-flag.js │ └── test.js ├── options │ ├── help.js │ ├── import-meta.js │ ├── infer-type.js │ ├── pkg.js │ └── version.js └── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 21 14 | - 20 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | yarn.lock 4 | test-d/build.ts 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /meow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/meow/87d1bc394d9a1d73b76a81d4874db4f1190259ef/meow.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meow", 3 | "version": "13.2.0", 4 | "description": "CLI app helper", 5 | "license": "MIT", 6 | "repository": "sindresorhus/meow", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./build/index.d.ts", 16 | "default": "./build/index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "prepare": "npm run build", 24 | "build": "rollup --config", 25 | "test": "xo && npm run build && ava && tsd --typings build/index.d.ts" 26 | }, 27 | "files": [ 28 | "build" 29 | ], 30 | "keywords": [ 31 | "cli", 32 | "bin", 33 | "util", 34 | "utility", 35 | "helper", 36 | "argv", 37 | "command", 38 | "line", 39 | "meow", 40 | "cat", 41 | "kitten", 42 | "parser", 43 | "option", 44 | "flags", 45 | "input", 46 | "cmd", 47 | "console" 48 | ], 49 | "_actualDependencies": [ 50 | "@types/minimist", 51 | "camelcase-keys", 52 | "decamelize", 53 | "decamelize-keys", 54 | "minimist-options", 55 | "normalize-package-data", 56 | "read-package-up", 57 | "redent", 58 | "trim-newlines", 59 | "type-fest", 60 | "yargs-parser" 61 | ], 62 | "devDependencies": { 63 | "@rollup/plugin-commonjs": "^25.0.7", 64 | "@rollup/plugin-json": "^6.1.0", 65 | "@rollup/plugin-node-resolve": "^15.2.3", 66 | "@types/minimist": "^1.2.5", 67 | "ava": "^6.1.1", 68 | "camelcase-keys": "^9.1.3", 69 | "common-tags": "^2.0.0-alpha.1", 70 | "decamelize": "^6.0.0", 71 | "decamelize-keys": "^2.0.1", 72 | "delete_comments": "^0.0.2", 73 | "execa": "^8.0.1", 74 | "globby": "^14.0.1", 75 | "indent-string": "^5.0.0", 76 | "minimist-options": "4.1.0", 77 | "normalize-package-data": "^6.0.0", 78 | "read-package-up": "^11.0.0", 79 | "read-pkg": "^9.0.1", 80 | "redent": "^4.0.0", 81 | "rollup": "^4.12.0", 82 | "rollup-plugin-dts": "^6.1.0", 83 | "rollup-plugin-license": "^3.2.0", 84 | "stack-utils": "^2.0.6", 85 | "trim-newlines": "^5.0.0", 86 | "tsd": "^0.30.7", 87 | "type-fest": "^4.10.3", 88 | "typescript": "~5.3.3", 89 | "xo": "^0.57.0", 90 | "yargs-parser": "^21.1.1" 91 | }, 92 | "xo": { 93 | "rules": { 94 | "unicorn/no-process-exit": "off", 95 | "unicorn/error-message": "off" 96 | }, 97 | "ignores": [ 98 | "build" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # meow 2 | 3 | > CLI app helper 4 | 5 | ![](meow.gif) 6 | 7 | *I would recommend reading this [guide](https://clig.dev) on how to make user-friendly command-line tools.* 8 | 9 | ## Features 10 | 11 | - Parses arguments 12 | - Converts flags to [camelCase](https://github.com/sindresorhus/camelcase) 13 | - Negates flags when using the `--no-` prefix 14 | - Outputs version when `--version` 15 | - Outputs description and supplied help text when `--help` 16 | - Sets the process title to the binary name defined in package.json 17 | - No dependencies! 18 | 19 | ## Install 20 | 21 | ```sh 22 | npm install meow 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```sh 28 | ./foo-app.js unicorns --rainbow 29 | ``` 30 | 31 | ```js 32 | #!/usr/bin/env node 33 | import meow from 'meow'; 34 | import foo from './lib/index.js'; 35 | 36 | const cli = meow(` 37 | Usage 38 | $ foo 39 | 40 | Options 41 | --rainbow, -r Include a rainbow 42 | 43 | Examples 44 | $ foo unicorns --rainbow 45 | 🌈 unicorns 🌈 46 | `, { 47 | importMeta: import.meta, // This is required 48 | flags: { 49 | rainbow: { 50 | type: 'boolean', 51 | shortFlag: 'r' 52 | } 53 | } 54 | }); 55 | /* 56 | { 57 | input: ['unicorns'], 58 | flags: {rainbow: true}, 59 | ... 60 | } 61 | */ 62 | 63 | foo(cli.input.at(0), cli.flags); 64 | ``` 65 | 66 | ## API 67 | 68 | ### meow(helpText, options) 69 | ### meow(options) 70 | 71 | Returns an `object` with: 72 | 73 | - `input` *(Array)* - Non-flag arguments 74 | - `flags` *(Object)* - Flags converted to camelCase excluding aliases 75 | - `unnormalizedFlags` *(Object)* - Flags converted to camelCase including aliases 76 | - `pkg` *(Object)* - The `package.json` object 77 | - `help` *(string)* - The help text used with `--help` 78 | - `showHelp([exitCode=2])` *(Function)* - Show the help text and exit with `exitCode` 79 | - `showVersion()` *(Function)* - Show the version text and exit 80 | 81 | #### helpText 82 | 83 | Type: `string` 84 | 85 | Shortcut for the `help` option. 86 | 87 | #### options 88 | 89 | Type: `object` 90 | 91 | ##### importMeta 92 | 93 | **Required**\ 94 | Type: `object` 95 | 96 | Pass in [`import.meta`](https://nodejs.org/dist/latest/docs/api/esm.html#esm_import_meta). This is used to find the correct package.json file. 97 | 98 | ##### flags 99 | 100 | Type: `object` 101 | 102 | Define argument flags. 103 | 104 | The key is the flag name in camel-case and the value is an object with any of: 105 | 106 | - `type`: Type of value. (Possible values: `string` `boolean` `number`) 107 | - `choices`: Limit valid values to a predefined set of choices. 108 | - `default`: Default value when the flag is not specified. 109 | - `shortFlag`: A short flag alias. 110 | - `aliases`: Other names for the flag. 111 | - `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false) 112 | - Multiple values are provided by specifying the flag multiple times, for example, `$ foo -u rainbow -u cat`. Space- or comma-separated values are [currently *not* supported](https://github.com/sindresorhus/meow/issues/164). 113 | - `isRequired`: Determine if the flag is required. (Default: false) 114 | - If it's only known at runtime whether the flag is required or not, you can pass a `Function` instead of a `boolean`, which based on the given flags and other non-flag arguments, should decide if the flag is required. Two arguments are passed to the function: 115 | - The first argument is the **flags** object, which contains the flags converted to camel-case excluding aliases. 116 | - The second argument is the **input** string array, which contains the non-flag arguments. 117 | - The function should return a `boolean`, true if the flag is required, otherwise false. 118 | 119 | Note that flags are always defined using a camel-case key (`myKey`), but will match arguments in kebab-case (`--my-key`). 120 | 121 | Example: 122 | 123 | ```js 124 | flags: { 125 | unicorn: { 126 | type: 'string', 127 | choices: ['rainbow', 'cat', 'unicorn'], 128 | default: ['rainbow', 'cat'], 129 | shortFlag: 'u', 130 | aliases: ['unicorns'], 131 | isMultiple: true, 132 | isRequired: (flags, input) => { 133 | if (flags.otherFlag) { 134 | return true; 135 | } 136 | 137 | return false; 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | ##### description 144 | 145 | Type: `string | false`\ 146 | Default: The package.json `"description"` property 147 | 148 | Description to show above the help text. 149 | 150 | Set it to `false` to disable it altogether. 151 | 152 | ##### help 153 | 154 | Type: `string | false` 155 | 156 | The help text you want shown. 157 | 158 | The input is reindented and starting/ending newlines are trimmed which means you can use a [template literal](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/template_strings) without having to care about using the correct amount of indent. 159 | 160 | The description will be shown above your help text automatically. 161 | 162 | Set it to `false` to disable it altogether. 163 | 164 | ##### version 165 | 166 | Type: `string`\ 167 | Default: The package.json `"version"` property 168 | 169 | Set a custom version output. 170 | 171 | ##### autoHelp 172 | 173 | Type: `boolean`\ 174 | Default: `true` 175 | 176 | Automatically show the help text when the `--help` flag is present. Useful to set this value to `false` when a CLI manages child CLIs with their own help text. 177 | 178 | This option is only considered when there is only one argument in `process.argv`. 179 | 180 | ##### autoVersion 181 | 182 | Type: `boolean`\ 183 | Default: `true` 184 | 185 | Automatically show the version text when the `--version` flag is present. Useful to set this value to `false` when a CLI manages child CLIs with their own version text. 186 | 187 | This option is only considered when there is only one argument in `process.argv`. 188 | 189 | ##### pkg 190 | 191 | Type: `object`\ 192 | Default: Closest package.json upwards 193 | 194 | package.json as an `object`. 195 | 196 | Note: Setting this stops `meow` from finding a package.json. 197 | 198 | *You most likely don't need this option.* 199 | 200 | ##### argv 201 | 202 | Type: `string[]`\ 203 | Default: `process.argv.slice(2)` 204 | 205 | Custom arguments object. 206 | 207 | ##### inferType 208 | 209 | Type: `boolean`\ 210 | Default: `false` 211 | 212 | Infer the argument type. 213 | 214 | By default, the argument `5` in `$ foo 5` becomes a string. Enabling this would infer it as a number. 215 | 216 | ##### booleanDefault 217 | 218 | Type: `boolean | undefined`\ 219 | Default: `false` 220 | 221 | Value of `boolean` flags not defined in `argv`. 222 | 223 | If set to `undefined`, the flags not defined in `argv` will be excluded from the result. 224 | The `default` value set in `boolean` flags take precedence over `booleanDefault`. 225 | 226 | _Note: If used in conjunction with `isMultiple`, the default flag value is set to `[]`._ 227 | 228 | __Caution: Explicitly specifying `undefined` for `booleanDefault` has different meaning from omitting key itself.__ 229 | 230 | Example: 231 | 232 | ```js 233 | import meow from 'meow'; 234 | 235 | const cli = meow(` 236 | Usage 237 | $ foo 238 | 239 | Options 240 | --rainbow, -r Include a rainbow 241 | --unicorn, -u Include a unicorn 242 | --no-sparkles Exclude sparkles 243 | 244 | Examples 245 | $ foo 246 | 🌈 unicorns✨🌈 247 | `, { 248 | importMeta: import.meta, 249 | booleanDefault: undefined, 250 | flags: { 251 | rainbow: { 252 | type: 'boolean', 253 | default: true, 254 | shortFlag: 'r' 255 | }, 256 | unicorn: { 257 | type: 'boolean', 258 | default: false, 259 | shortFlag: 'u' 260 | }, 261 | cake: { 262 | type: 'boolean', 263 | shortFlag: 'c' 264 | }, 265 | sparkles: { 266 | type: 'boolean', 267 | default: true 268 | } 269 | } 270 | }); 271 | /* 272 | { 273 | flags: { 274 | rainbow: true, 275 | unicorn: false, 276 | sparkles: true 277 | }, 278 | unnormalizedFlags: { 279 | rainbow: true, 280 | r: true, 281 | unicorn: false, 282 | u: false, 283 | sparkles: true 284 | }, 285 | … 286 | } 287 | */ 288 | ``` 289 | 290 | ##### allowUnknownFlags 291 | 292 | Type `boolean`\ 293 | Default: `true` 294 | 295 | Whether to allow unknown flags or not. 296 | 297 | ##### helpIndent 298 | 299 | Type `number`\ 300 | Default: `2` 301 | 302 | The number of spaces to use for indenting the help text. 303 | 304 | ## Tips 305 | 306 | See [`chalk`](https://github.com/chalk/chalk) if you want to colorize the terminal output. 307 | 308 | See [`get-stdin`](https://github.com/sindresorhus/get-stdin) if you want to accept input from stdin. 309 | 310 | See [`conf`](https://github.com/sindresorhus/conf) if you need to persist some data. 311 | 312 | [More useful CLI utilities…](https://github.com/sindresorhus/awesome-nodejs#command-line-utilities) 313 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import json from '@rollup/plugin-json'; 5 | import license from 'rollup-plugin-license'; 6 | import {dts} from 'rollup-plugin-dts'; 7 | import {globby} from 'globby'; 8 | import {createTag, replaceResultTransformer} from 'common-tags'; 9 | import {delete_comments as deleteComments} from 'delete_comments'; 10 | import {defineConfig} from 'rollup'; 11 | 12 | /** Matches empty lines: https://stackoverflow.com/q/16369642/10292952 */ 13 | const emptyLineRegex = /^\s*[\r\n]/gm; 14 | 15 | const stripComments = createTag( 16 | {onEndResult: deleteComments}, 17 | replaceResultTransformer(emptyLineRegex, ''), 18 | ); 19 | 20 | const sourceDirectory = 'source'; 21 | const outputDirectory = 'build'; 22 | 23 | const config = defineConfig({ 24 | input: await globby(`${sourceDirectory}/**/*.js`), 25 | output: { 26 | dir: outputDirectory, 27 | interop: 'esModule', 28 | generatedCode: { 29 | preset: 'es2015', 30 | }, 31 | chunkFileNames: '[name].js', 32 | manualChunks(id) { 33 | if (id.includes('node_modules')) { 34 | return 'dependencies'; 35 | } 36 | }, 37 | hoistTransitiveImports: false, 38 | plugins: [{ 39 | name: 'strip-dependency-comments', 40 | renderChunk(code, chunk) { 41 | return chunk.name === 'dependencies' ? stripComments(code) : null; 42 | }, 43 | }], 44 | }, 45 | treeshake: { 46 | moduleSideEffects: 'no-external', 47 | }, 48 | plugins: [ 49 | nodeResolve({exportConditions: ['node']}), 50 | commonjs({ 51 | include: 'node_modules/**', 52 | }), 53 | json(), 54 | license({ 55 | thirdParty: { 56 | output: `${outputDirectory}/licenses.md`, 57 | }, 58 | }), 59 | ], 60 | }); 61 | 62 | const dtsConfig = defineConfig({ 63 | input: `./${sourceDirectory}/index.d.ts`, 64 | output: { 65 | file: `./${outputDirectory}/index.d.ts`, 66 | format: 'es', 67 | }, 68 | plugins: [ 69 | dts({ 70 | respectExternal: true, 71 | }), 72 | { 73 | name: 'copy-tsd', 74 | async generateBundle() { 75 | let tsdFile = await fs.readFile('./test-d/index.ts', 'utf8'); 76 | tsdFile = tsdFile.replace( 77 | `import meow from '../${sourceDirectory}/index.js'`, 78 | `import meow from '../${outputDirectory}/index.js'`, 79 | ); 80 | 81 | await fs.writeFile(`./test-d/${outputDirectory}.ts`, tsdFile); 82 | }, 83 | }, 84 | ], 85 | }); 86 | 87 | // eslint-disable-next-line import/no-anonymous-default-export 88 | export default [config, dtsConfig]; 89 | -------------------------------------------------------------------------------- /source/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CamelCasedProperties, 3 | PackageJson, 4 | } from 'type-fest'; 5 | 6 | export type FlagType = 'string' | 'boolean' | 'number'; 7 | 8 | /** 9 | Callback function to determine if a flag is required during runtime. 10 | 11 | @param flags - Contains the flags converted to camel-case excluding aliases. 12 | @param input - Contains the non-flag arguments. 13 | 14 | @returns True if the flag is required, otherwise false. 15 | */ 16 | export type IsRequiredPredicate = (flags: Readonly, input: readonly string[]) => boolean; 17 | 18 | export type Flag = { 19 | /** 20 | Type of value. (Possible values: `string` `boolean` `number`) 21 | */ 22 | readonly type?: PrimitiveType; 23 | 24 | /** 25 | Limit valid values to a predefined set of choices. 26 | 27 | @example 28 | ``` 29 | unicorn: { 30 | isMultiple: true, 31 | choices: ['rainbow', 'cat', 'unicorn'] 32 | } 33 | ``` 34 | */ 35 | readonly choices?: Type extends unknown[] ? Type : Type[]; 36 | 37 | /** 38 | Default value when the flag is not specified. 39 | 40 | @example 41 | ``` 42 | unicorn: { 43 | type: 'boolean', 44 | default: true 45 | } 46 | ``` 47 | */ 48 | readonly default?: Type; 49 | 50 | /** 51 | A short flag alias. 52 | 53 | @example 54 | ``` 55 | unicorn: { 56 | shortFlag: 'u' 57 | } 58 | ``` 59 | */ 60 | readonly shortFlag?: string; 61 | 62 | /** 63 | Other names for the flag. 64 | 65 | @example 66 | ``` 67 | unicorn: { 68 | aliases: ['unicorns', 'uni'] 69 | } 70 | ``` 71 | */ 72 | readonly aliases?: string[]; 73 | 74 | /** 75 | Indicates a flag can be set multiple times. Values are turned into an array. 76 | 77 | Multiple values are provided by specifying the flag multiple times, for example, `$ foo -u rainbow -u cat`. Space- or comma-separated values [currently *not* supported](https://github.com/sindresorhus/meow/issues/164). 78 | 79 | @default false 80 | */ 81 | readonly isMultiple?: IsMultiple; 82 | 83 | /** 84 | Determine if the flag is required. 85 | 86 | If it's only known at runtime whether the flag is required or not you can pass a Function instead of a boolean, which based on the given flags and other non-flag arguments should decide if the flag is required. 87 | 88 | - The first argument is the **flags** object, which contains the flags converted to camel-case excluding aliases. 89 | - The second argument is the **input** string array, which contains the non-flag arguments. 90 | - The function should return a `boolean`, true if the flag is required, otherwise false. 91 | 92 | @default false 93 | 94 | @example 95 | ``` 96 | isRequired: (flags, input) => { 97 | if (flags.otherFlag) { 98 | return true; 99 | } 100 | 101 | return false; 102 | } 103 | ``` 104 | */ 105 | readonly isRequired?: boolean | IsRequiredPredicate; 106 | }; 107 | 108 | type StringFlag = Flag<'string', string> | Flag<'string', string[], true>; 109 | type BooleanFlag = Flag<'boolean', boolean> | Flag<'boolean', boolean[], true>; 110 | type NumberFlag = Flag<'number', number> | Flag<'number', number[], true>; 111 | type AnyFlag = StringFlag | BooleanFlag | NumberFlag; 112 | type AnyFlags = Record; 113 | 114 | export type Options = { 115 | /** 116 | Pass in [`import.meta`](https://nodejs.org/dist/latest/docs/api/esm.html#esm_import_meta). This is used to find the correct package.json file. 117 | */ 118 | readonly importMeta: ImportMeta; 119 | 120 | /** 121 | Define argument flags. 122 | 123 | The key is the flag name in camel-case and the value is an object with any of: 124 | 125 | - `type`: Type of value. (Possible values: `string` `boolean` `number`) 126 | - `choices`: Limit valid values to a predefined set of choices. 127 | - `default`: Default value when the flag is not specified. 128 | - `shortFlag`: A short flag alias. 129 | - `aliases`: Other names for the flag. 130 | - `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false) 131 | - Multiple values are provided by specifying the flag multiple times, for example, `$ foo -u rainbow -u cat`. Space- or comma-separated values [currently *not* supported](https://github.com/sindresorhus/meow/issues/164). 132 | - `isRequired`: Determine if the flag is required. (Default: false) 133 | - If it's only known at runtime whether the flag is required or not, you can pass a `Function` instead of a `boolean`, which based on the given flags and other non-flag arguments, should decide if the flag is required. Two arguments are passed to the function: 134 | - The first argument is the **flags** object, which contains the flags converted to camel-case excluding aliases. 135 | - The second argument is the **input** string array, which contains the non-flag arguments. 136 | - The function should return a `boolean`, true if the flag is required, otherwise false. 137 | 138 | Note that flags are always defined using a camel-case key (`myKey`), but will match arguments in kebab-case (`--my-key`). 139 | 140 | @example 141 | ``` 142 | flags: { 143 | unicorn: { 144 | type: 'string', 145 | choices: ['rainbow', 'cat', 'unicorn'], 146 | default: ['rainbow', 'cat'], 147 | shortFlag: 'u', 148 | aliases: ['unicorns'] 149 | isMultiple: true, 150 | isRequired: (flags, input) => { 151 | if (flags.otherFlag) { 152 | return true; 153 | } 154 | 155 | return false; 156 | } 157 | } 158 | } 159 | ``` 160 | */ 161 | readonly flags?: Flags; 162 | 163 | /** 164 | Description to show above the help text. Default: The package.json `"description"` property. 165 | 166 | Set it to `false` to disable it altogether. 167 | */ 168 | readonly description?: string | false; 169 | 170 | /** 171 | The help text you want shown. 172 | 173 | The input is reindented and starting/ending newlines are trimmed which means you can use a [template literal](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/template_strings) without having to care about using the correct amount of indent. 174 | 175 | The description will be shown above your help text automatically. 176 | 177 | Set it to `false` to disable it altogether. 178 | */ 179 | readonly help?: string | false; 180 | 181 | /** 182 | Set a custom version output. Default: The package.json `"version"` property. 183 | */ 184 | readonly version?: string; 185 | 186 | /** 187 | Automatically show the help text when the `--help` flag is present. Useful to set this value to `false` when a CLI manages child CLIs with their own help text. 188 | 189 | This option is only considered when there is only one argument in `process.argv`. 190 | */ 191 | readonly autoHelp?: boolean; 192 | 193 | /** 194 | Automatically show the version text when the `--version` flag is present. Useful to set this value to `false` when a CLI manages child CLIs with their own version text. 195 | 196 | This option is only considered when there is only one argument in `process.argv`. 197 | */ 198 | readonly autoVersion?: boolean; 199 | 200 | /** 201 | `package.json` as an `Object`. Default: Closest `package.json` upwards. 202 | 203 | Note: Setting this stops `meow` from finding a package.json. 204 | 205 | _You most likely don't need this option._ 206 | */ 207 | readonly pkg?: Record; 208 | 209 | /** 210 | Custom arguments object. 211 | 212 | @default process.argv.slice(2) 213 | */ 214 | readonly argv?: readonly string[]; 215 | 216 | /** 217 | Infer the argument type. 218 | 219 | By default, the argument `5` in `$ foo 5` becomes a string. Enabling this would infer it as a number. 220 | 221 | @default false 222 | */ 223 | readonly inferType?: boolean; 224 | 225 | /** 226 | Value of `boolean` flags not defined in `argv`. 227 | 228 | If set to `undefined`, the flags not defined in `argv` will be excluded from the result. The `default` value set in `boolean` flags take precedence over `booleanDefault`. 229 | 230 | _Note: If used in conjunction with `isMultiple`, the default flag value is set to `[]`._ 231 | 232 | __Caution: Explicitly specifying `undefined` for `booleanDefault` has different meaning from omitting key itself.__ 233 | 234 | @example 235 | ``` 236 | import meow from 'meow'; 237 | 238 | const cli = meow(` 239 | Usage 240 | $ foo 241 | 242 | Options 243 | --rainbow, -r Include a rainbow 244 | --unicorn, -u Include a unicorn 245 | --no-sparkles Exclude sparkles 246 | 247 | Examples 248 | $ foo 249 | 🌈 unicorns✨🌈 250 | `, { 251 | importMeta: import.meta, 252 | booleanDefault: undefined, 253 | flags: { 254 | rainbow: { 255 | type: 'boolean', 256 | default: true, 257 | shortFlag: 'r' 258 | }, 259 | unicorn: { 260 | type: 'boolean', 261 | default: false, 262 | shortFlag: 'u' 263 | }, 264 | cake: { 265 | type: 'boolean', 266 | shortFlag: 'c' 267 | }, 268 | sparkles: { 269 | type: 'boolean', 270 | default: true 271 | } 272 | } 273 | }); 274 | 275 | //{ 276 | // flags: { 277 | // rainbow: true, 278 | // unicorn: false, 279 | // sparkles: true 280 | // }, 281 | // unnormalizedFlags: { 282 | // rainbow: true, 283 | // r: true, 284 | // unicorn: false, 285 | // u: false, 286 | // sparkles: true 287 | // }, 288 | // … 289 | //} 290 | ``` 291 | */ 292 | readonly booleanDefault?: boolean | undefined; 293 | 294 | // TODO: Remove this in meow 14. 295 | /** 296 | Whether to use [hard-rejection](https://github.com/sindresorhus/hard-rejection) or not. Disabling this can be useful if you need to handle `process.on('unhandledRejection')` yourself. 297 | 298 | @deprecated This is the default behavior since Node.js 16, so this option is moot. 299 | @default true 300 | */ 301 | readonly hardRejection?: boolean; 302 | 303 | /** 304 | Whether to allow unknown flags or not. 305 | 306 | @default true 307 | */ 308 | readonly allowUnknownFlags?: boolean; 309 | 310 | /** 311 | The number of spaces to use for indenting the help text. 312 | 313 | @default 2 314 | */ 315 | readonly helpIndent?: number; 316 | }; 317 | 318 | type TypedFlag = 319 | Flag extends {type: 'number'} 320 | ? number 321 | : Flag extends {type: 'string'} 322 | ? string 323 | : Flag extends {type: 'boolean'} 324 | ? boolean 325 | : unknown; 326 | 327 | type PossiblyOptionalFlag = 328 | Flag extends {isRequired: true} 329 | ? FlagType 330 | : Flag extends {default: any} 331 | ? FlagType 332 | : FlagType | undefined; 333 | 334 | export type TypedFlags = { 335 | [F in keyof Flags]: Flags[F] extends {isMultiple: true} 336 | ? PossiblyOptionalFlag>> 337 | : PossiblyOptionalFlag> 338 | }; 339 | 340 | export type Result = { 341 | /** 342 | Non-flag arguments. 343 | */ 344 | input: string[]; 345 | 346 | /** 347 | Flags converted to camelCase excluding aliases. 348 | */ 349 | flags: CamelCasedProperties> & Record; 350 | 351 | /** 352 | Flags converted camelCase including aliases. 353 | */ 354 | unnormalizedFlags: TypedFlags & Record; 355 | 356 | /** 357 | The `package.json` object. 358 | */ 359 | pkg: PackageJson; 360 | 361 | /** 362 | The help text used with `--help`. 363 | */ 364 | help: string; 365 | 366 | /** 367 | Show the help text and exit with code. 368 | 369 | @param exitCode - The exit code to use. Default: `2`. 370 | */ 371 | showHelp: (exitCode?: number) => never; 372 | 373 | /** 374 | Show the version text and exit. 375 | */ 376 | showVersion: () => void; 377 | }; 378 | /** 379 | @param helpMessage - Shortcut for the `help` option. 380 | 381 | @example 382 | ``` 383 | #!/usr/bin/env node 384 | import meow from 'meow'; 385 | import foo from './index.js'; 386 | 387 | const cli = meow(` 388 | Usage 389 | $ foo 390 | 391 | Options 392 | --rainbow, -r Include a rainbow 393 | 394 | Examples 395 | $ foo unicorns --rainbow 396 | 🌈 unicorns 🌈 397 | `, { 398 | importMeta: import.meta, // This is required 399 | flags: { 400 | rainbow: { 401 | type: 'boolean', 402 | shortFlag: 'r' 403 | } 404 | } 405 | }); 406 | 407 | //{ 408 | // input: ['unicorns'], 409 | // flags: {rainbow: true}, 410 | // ... 411 | //} 412 | 413 | foo(cli.input.at(0), cli.flags); 414 | ``` 415 | */ 416 | export default function meow(helpMessage: string, options: Options): Result; 417 | export default function meow(options: Options): Result; 418 | -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import parseArguments from 'yargs-parser'; 3 | import camelCaseKeys from 'camelcase-keys'; 4 | import {trimNewlines} from 'trim-newlines'; 5 | import redent from 'redent'; 6 | import {buildOptions} from './options.js'; 7 | import {buildParserOptions} from './parser.js'; 8 | import {validate, checkUnknownFlags, checkMissingRequiredFlags} from './validate.js'; 9 | 10 | const buildResult = ({pkg: packageJson, ...options}, parserOptions) => { 11 | const argv = parseArguments(options.argv, parserOptions); 12 | let help = ''; 13 | 14 | if (options.help) { 15 | help = trimNewlines((options.help || '').replace(/\t+\n*$/, '')); 16 | 17 | if (help.includes('\n')) { 18 | help = redent(help, options.helpIndent); 19 | } 20 | 21 | help = `\n${help}`; 22 | } 23 | 24 | if (options.description !== false) { 25 | let {description} = options; 26 | 27 | if (description) { 28 | description = help ? redent(`\n${description}\n`, options.helpIndent) : `\n${description}`; 29 | help = `${description}${help}`; 30 | } 31 | } 32 | 33 | help += '\n'; 34 | 35 | const showHelp = code => { 36 | console.log(help); 37 | process.exit(typeof code === 'number' ? code : 2); // Default to code 2 for incorrect usage (#47) 38 | }; 39 | 40 | const showVersion = () => { 41 | console.log(options.version); 42 | process.exit(0); 43 | }; 44 | 45 | if (argv._.length === 0 && options.argv.length === 1) { 46 | if (argv.version === true && options.autoVersion) { 47 | showVersion(); 48 | } else if (argv.help === true && options.autoHelp) { 49 | showHelp(0); 50 | } 51 | } 52 | 53 | const input = argv._; 54 | delete argv._; 55 | 56 | if (!options.allowUnknownFlags) { 57 | checkUnknownFlags(input); 58 | } 59 | 60 | const flags = camelCaseKeys(argv, {exclude: ['--', /^\w$/]}); 61 | const unnormalizedFlags = {...flags}; 62 | 63 | validate(flags, options); 64 | 65 | for (const flagValue of Object.values(options.flags)) { 66 | if (Array.isArray(flagValue.aliases)) { 67 | for (const alias of flagValue.aliases) { 68 | delete flags[alias]; 69 | } 70 | } 71 | 72 | delete flags[flagValue.shortFlag]; 73 | } 74 | 75 | checkMissingRequiredFlags(options.flags, flags, input); 76 | 77 | return { 78 | input, 79 | flags, 80 | unnormalizedFlags, 81 | pkg: packageJson, 82 | help, 83 | showHelp, 84 | showVersion, 85 | }; 86 | }; 87 | 88 | const meow = (helpText, options = {}) => { 89 | const parsedOptions = buildOptions(helpText, options); 90 | const parserOptions = buildParserOptions(parsedOptions); 91 | const result = buildResult(parsedOptions, parserOptions); 92 | 93 | process.title = result.pkg.bin ? Object.keys(result.pkg.bin).at(0) : result.pkg.name; 94 | 95 | return result; 96 | }; 97 | 98 | export default meow; 99 | -------------------------------------------------------------------------------- /source/options.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {dirname} from 'node:path'; 3 | import {fileURLToPath} from 'node:url'; 4 | import {readPackageUpSync} from 'read-package-up'; 5 | import normalizePackageData from 'normalize-package-data'; 6 | import {decamelizeFlagKey, joinFlagKeys} from './utils.js'; 7 | 8 | const validateOptions = options => { 9 | const invalidOptionFilters = { 10 | flags: { 11 | keyContainsDashes: { 12 | filter: ([flagKey]) => flagKey.includes('-') && flagKey !== '--', 13 | message: flagKeys => `Flag keys may not contain '-'. Invalid flags: ${joinFlagKeys(flagKeys, '')}`, 14 | }, 15 | aliasIsSet: { 16 | filter: ([, flag]) => Object.hasOwn(flag, 'alias'), 17 | message: flagKeys => `The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: ${joinFlagKeys(flagKeys)}`, 18 | }, 19 | choicesNotAnArray: { 20 | filter: ([, flag]) => Object.hasOwn(flag, 'choices') && !Array.isArray(flag.choices), 21 | message: flagKeys => `The option \`choices\` must be an array. Invalid flags: ${joinFlagKeys(flagKeys)}`, 22 | }, 23 | choicesNotMatchFlagType: { 24 | filter: ([, flag]) => flag.type && Array.isArray(flag.choices) && flag.choices.some(choice => typeof choice !== flag.type), 25 | message(flagKeys) { 26 | const flagKeysAndTypes = flagKeys.map(flagKey => `(\`${decamelizeFlagKey(flagKey)}\`, type: '${options.flags[flagKey].type}')`); 27 | return `Each value of the option \`choices\` must be of the same type as its flag. Invalid flags: ${flagKeysAndTypes.join(', ')}`; 28 | }, 29 | }, 30 | defaultNotInChoices: { 31 | filter: ([, flag]) => flag.default && Array.isArray(flag.choices) && ![flag.default].flat().every(value => flag.choices.includes(value)), 32 | message: flagKeys => `Each value of the option \`default\` must exist within the option \`choices\`. Invalid flags: ${joinFlagKeys(flagKeys)}`, 33 | }, 34 | }, 35 | }; 36 | 37 | const errorMessages = []; 38 | 39 | for (const [optionKey, filters] of Object.entries(invalidOptionFilters)) { 40 | const optionEntries = Object.entries(options[optionKey]); 41 | 42 | for (const {filter, message} of Object.values(filters)) { 43 | const invalidOptions = optionEntries.filter(option => filter(option)); 44 | const invalidOptionKeys = invalidOptions.map(([key]) => key); 45 | 46 | if (invalidOptions.length > 0) { 47 | errorMessages.push(message(invalidOptionKeys)); 48 | } 49 | } 50 | } 51 | 52 | if (errorMessages.length > 0) { 53 | throw new Error(errorMessages.join('\n')); 54 | } 55 | }; 56 | 57 | export const buildOptions = (helpText, options) => { 58 | if (typeof helpText !== 'string') { 59 | options = helpText; 60 | helpText = ''; 61 | } 62 | 63 | if (!options.importMeta?.url) { 64 | throw new TypeError('The `importMeta` option is required. Its value must be `import.meta`.'); 65 | } 66 | 67 | const foundPackage = options.pkg ?? readPackageUpSync({ 68 | cwd: dirname(fileURLToPath(options.importMeta.url)), 69 | normalize: false, 70 | })?.packageJson; 71 | 72 | // eslint-disable-next-line unicorn/prevent-abbreviations 73 | const pkg = foundPackage ?? {}; 74 | normalizePackageData(pkg); 75 | 76 | const parsedOptions = { 77 | argv: process.argv.slice(2), 78 | flags: {}, 79 | inferType: false, 80 | input: 'string', 81 | description: pkg.description ?? false, 82 | help: helpText, 83 | version: pkg.version || 'No version found', 84 | autoHelp: true, 85 | autoVersion: true, 86 | booleanDefault: false, 87 | allowUnknownFlags: true, 88 | allowParentFlags: true, 89 | helpIndent: 2, 90 | ...options, 91 | pkg, 92 | }; 93 | 94 | validateOptions(parsedOptions); 95 | 96 | return parsedOptions; 97 | }; 98 | -------------------------------------------------------------------------------- /source/parser.js: -------------------------------------------------------------------------------- 1 | import constructParserOptions from 'minimist-options'; 2 | import decamelizeKeys from 'decamelize-keys'; 3 | 4 | const buildParserFlags = ({flags, booleanDefault}) => { 5 | const parserFlags = {}; 6 | 7 | for (const [flagKey, flagValue] of Object.entries(flags)) { 8 | const flag = {...flagValue}; 9 | 10 | // `minimist-options` expects `flag.alias` 11 | if (flag.shortFlag) { 12 | flag.alias = flag.shortFlag; 13 | delete flag.shortFlag; 14 | } 15 | 16 | if (booleanDefault !== undefined && flag.type === 'boolean' && !Object.hasOwn(flag, 'default')) { 17 | flag.default = flag.isMultiple ? [booleanDefault] : booleanDefault; 18 | } 19 | 20 | if (flag.isMultiple) { 21 | flag.type = flag.type ? `${flag.type}-array` : 'array'; 22 | flag.default ??= []; 23 | delete flag.isMultiple; 24 | } 25 | 26 | if (Array.isArray(flag.aliases)) { 27 | if (flag.alias) { 28 | flag.aliases.push(flag.alias); 29 | } 30 | 31 | flag.alias = flag.aliases; 32 | delete flag.aliases; 33 | } 34 | 35 | parserFlags[flagKey] = flag; 36 | } 37 | 38 | return parserFlags; 39 | }; 40 | 41 | export const buildParserOptions = options => { 42 | let parserOptions = buildParserFlags(options); 43 | parserOptions.arguments = options.input; 44 | 45 | parserOptions = decamelizeKeys(parserOptions, {separator: '-', exclude: ['stopEarly', '--']}); 46 | 47 | if (options.inferType) { 48 | delete parserOptions.arguments; 49 | } 50 | 51 | // Add --help and --version to known flags if autoHelp or autoVersion are set 52 | if (!options.allowUnknownFlags) { 53 | if (options.autoHelp && !parserOptions.help) { 54 | parserOptions.help = {type: 'boolean'}; 55 | } 56 | 57 | if (options.autoVersion && !parserOptions.version) { 58 | parserOptions.version = {type: 'boolean'}; 59 | } 60 | } 61 | 62 | parserOptions = constructParserOptions(parserOptions); 63 | 64 | parserOptions.configuration = { 65 | ...parserOptions.configuration, 66 | 'greedy-arrays': false, 67 | }; 68 | 69 | if (parserOptions['--']) { 70 | parserOptions.configuration['populate--'] = true; 71 | } 72 | 73 | if (!options.allowUnknownFlags) { 74 | // Collect unknown options in `argv._` to be checked later. 75 | parserOptions.configuration['unknown-options-as-args'] = true; 76 | } 77 | 78 | return parserOptions; 79 | }; 80 | -------------------------------------------------------------------------------- /source/utils.js: -------------------------------------------------------------------------------- 1 | import decamelize from 'decamelize'; 2 | 3 | export const decamelizeFlagKey = flagKey => `--${decamelize(flagKey, {separator: '-'})}`; 4 | 5 | export const joinFlagKeys = (flagKeys, prefix = '--') => `\`${prefix}${flagKeys.join(`\`, \`${prefix}`)}\``; 6 | -------------------------------------------------------------------------------- /source/validate.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {decamelizeFlagKey} from './utils.js'; 3 | 4 | const validateFlags = (flags, options) => { 5 | for (const [flagKey, flagValue] of Object.entries(options.flags)) { 6 | if (flagKey !== '--' && !flagValue.isMultiple && Array.isArray(flags[flagKey])) { 7 | throw new Error(`The flag --${flagKey} can only be set once.`); 8 | } 9 | } 10 | }; 11 | 12 | const validateChoicesByFlag = (flagKey, flagValue, receivedInput) => { 13 | const {choices, isRequired} = flagValue; 14 | 15 | if (!choices) { 16 | return; 17 | } 18 | 19 | const valueMustBeOneOf = `Value must be one of: [\`${choices.join('`, `')}\`]`; 20 | 21 | if (!receivedInput) { 22 | if (isRequired) { 23 | return `Flag \`${decamelizeFlagKey(flagKey)}\` has no value. ${valueMustBeOneOf}`; 24 | } 25 | 26 | return; 27 | } 28 | 29 | if (Array.isArray(receivedInput)) { 30 | const unknownValues = receivedInput.filter(index => !choices.includes(index)); 31 | 32 | if (unknownValues.length > 0) { 33 | const valuesText = unknownValues.length > 1 ? 'values' : 'value'; 34 | 35 | return `Unknown ${valuesText} for flag \`${decamelizeFlagKey(flagKey)}\`: \`${unknownValues.join('`, `')}\`. ${valueMustBeOneOf}`; 36 | } 37 | } else if (!choices.includes(receivedInput)) { 38 | return `Unknown value for flag \`${decamelizeFlagKey(flagKey)}\`: \`${receivedInput}\`. ${valueMustBeOneOf}`; 39 | } 40 | }; 41 | 42 | const validateChoices = (flags, receivedFlags) => { 43 | const errors = []; 44 | 45 | for (const [flagKey, flagValue] of Object.entries(flags)) { 46 | const receivedInput = receivedFlags[flagKey]; 47 | const errorMessage = validateChoicesByFlag(flagKey, flagValue, receivedInput); 48 | 49 | if (errorMessage) { 50 | errors.push(errorMessage); 51 | } 52 | } 53 | 54 | if (errors.length > 0) { 55 | throw new Error(`${errors.join('\n')}`); 56 | } 57 | }; 58 | 59 | export const validate = (flags, options) => { 60 | validateFlags(flags, options); 61 | validateChoices(options.flags, flags); 62 | }; 63 | 64 | const reportUnknownFlags = unknownFlags => { 65 | console.error([ 66 | `Unknown flag${unknownFlags.length > 1 ? 's' : ''}`, 67 | ...unknownFlags, 68 | ].join('\n')); 69 | }; 70 | 71 | export const checkUnknownFlags = input => { 72 | const unknownFlags = input.filter(item => typeof item === 'string' && item.startsWith('-')); 73 | if (unknownFlags.length > 0) { 74 | reportUnknownFlags(unknownFlags); 75 | process.exit(2); 76 | } 77 | }; 78 | 79 | const isFlagMissing = (flagName, definedFlags, receivedFlags, input) => { 80 | const flag = definedFlags[flagName]; 81 | let isFlagRequired = true; 82 | 83 | if (typeof flag.isRequired === 'function') { 84 | isFlagRequired = flag.isRequired(receivedFlags, input); 85 | if (typeof isFlagRequired !== 'boolean') { 86 | throw new TypeError(`Return value for isRequired callback should be of type boolean, but ${typeof isFlagRequired} was returned.`); 87 | } 88 | } 89 | 90 | if (receivedFlags[flagName] === undefined) { 91 | return isFlagRequired; 92 | } 93 | 94 | return flag.isMultiple && receivedFlags[flagName].length === 0 && isFlagRequired; 95 | }; 96 | 97 | const reportMissingRequiredFlags = missingRequiredFlags => { 98 | console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`); 99 | for (const flag of missingRequiredFlags) { 100 | console.error(`\t${decamelizeFlagKey(flag.key)}${flag.shortFlag ? `, -${flag.shortFlag}` : ''}`); 101 | } 102 | }; 103 | 104 | export const checkMissingRequiredFlags = (flags, receivedFlags, input) => { 105 | const missingRequiredFlags = []; 106 | if (flags === undefined) { 107 | return []; 108 | } 109 | 110 | for (const flagName of Object.keys(flags)) { 111 | if (flags[flagName].isRequired && isFlagMissing(flagName, flags, receivedFlags, input)) { 112 | missingRequiredFlags.push({key: flagName, ...flags[flagName]}); 113 | } 114 | } 115 | 116 | if (missingRequiredFlags.length > 0) { 117 | reportMissingRequiredFlags(missingRequiredFlags); 118 | process.exit(2); 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /test-d/index.ts: -------------------------------------------------------------------------------- 1 | import {expectAssignable, expectError, expectType} from 'tsd'; 2 | import type {PackageJson} from 'type-fest'; 3 | import meow, {type Result} from '../source/index.js'; 4 | 5 | type AnyFlag = NonNullable[0]>['flags']>[string]; 6 | 7 | const importMeta = import.meta; 8 | 9 | expectError(meow('Help text')); 10 | expectError(meow('Help text', {})); 11 | expectType>(meow('Help text', {importMeta, hardRejection: false})); 12 | expectAssignable<{flags: {foo: number}}>( 13 | meow({importMeta: import.meta, flags: {foo: {type: 'number', isRequired: true}}}), 14 | ); 15 | expectAssignable<{flags: {foo: string}}>( 16 | meow({importMeta, flags: {foo: {type: 'string', isRequired: true}}}), 17 | ); 18 | expectAssignable<{flags: {foo: boolean}}>( 19 | meow({importMeta, flags: {foo: {type: 'boolean', isRequired: true}}}), 20 | ); 21 | expectAssignable<{flags: {foo: number | undefined}}>( 22 | meow({importMeta, flags: {foo: {type: 'number'}}}), 23 | ); 24 | expectAssignable<{flags: {foo: string | undefined}}>( 25 | meow({importMeta, flags: {foo: {type: 'string'}}}), 26 | ); 27 | expectAssignable<{flags: {foo: boolean | undefined}}>( 28 | meow({importMeta, flags: {foo: {type: 'boolean'}}}), 29 | ); 30 | expectAssignable<{flags: {foo: number[] | undefined}}>( 31 | meow({importMeta, flags: {foo: {type: 'number', isMultiple: true}}}), 32 | ); 33 | expectAssignable<{flags: {foo: string[] | undefined}}>( 34 | meow({importMeta, flags: {foo: {type: 'string', isMultiple: true}}}), 35 | ); 36 | expectAssignable<{flags: {foo: boolean[] | undefined}}>( 37 | meow({importMeta, flags: {foo: {type: 'boolean', isMultiple: true}}}), 38 | ); 39 | expectType>(meow({importMeta, description: 'foo'})); 40 | expectType>(meow({importMeta, description: false})); 41 | expectType>(meow({importMeta, help: 'foo'})); 42 | expectType>(meow({importMeta, help: false})); 43 | expectType>(meow({importMeta, version: 'foo'})); 44 | expectType>(meow({importMeta, autoHelp: false})); 45 | expectType>(meow({importMeta, autoVersion: false})); 46 | expectType>(meow({importMeta, pkg: {foo: 'bar'}})); 47 | expectType>(meow({importMeta, argv: ['foo', 'bar']})); 48 | expectType>(meow({importMeta, inferType: true})); 49 | expectType>(meow({importMeta, booleanDefault: true})); 50 | expectType>(meow({importMeta, booleanDefault: undefined})); 51 | expectType>(meow({importMeta, hardRejection: false})); 52 | 53 | const result = meow('Help text', { 54 | importMeta, 55 | flags: { 56 | foo: {type: 'boolean', shortFlag: 'f'}, 57 | 'foo-bar': {type: 'number', aliases: ['foobar', 'fooBar']}, 58 | bar: {type: 'string', default: ''}, 59 | abc: {type: 'string', isMultiple: true}, 60 | baz: {type: 'string', choices: ['rainbow', 'cat', 'unicorn']}, 61 | }, 62 | }); 63 | 64 | expectType(result.input); 65 | expectType(result.pkg); 66 | expectType(result.help); 67 | 68 | expectType(result.flags.foo); 69 | expectType(result.flags.fooBar); 70 | expectType(result.flags.bar); 71 | expectType(result.flags.abc); 72 | expectType(result.flags.baz); 73 | expectType(result.unnormalizedFlags.foo); 74 | expectType(result.unnormalizedFlags.f); 75 | expectType(result.unnormalizedFlags['foo-bar']); 76 | expectType(result.unnormalizedFlags.foobar); 77 | expectType(result.unnormalizedFlags.fooBar); 78 | expectType(result.unnormalizedFlags.bar); 79 | expectType(result.unnormalizedFlags.abc); 80 | expectType(result.unnormalizedFlags.baz); 81 | 82 | result.showHelp(); 83 | result.showHelp(1); 84 | result.showVersion(); 85 | 86 | const options = { 87 | importMeta, 88 | flags: { 89 | rainbow: { 90 | type: 'boolean', 91 | shortFlag: 'r', 92 | }, 93 | }, 94 | } as const; 95 | 96 | meow('', options); 97 | 98 | expectAssignable({type: 'string', default: 'cat'}); 99 | expectAssignable({type: 'number', default: 42}); 100 | expectAssignable({type: 'boolean', default: true}); 101 | 102 | expectAssignable({type: 'string', default: undefined}); 103 | expectAssignable({type: 'number', default: undefined}); 104 | expectAssignable({type: 'boolean', default: undefined}); 105 | 106 | expectAssignable({type: 'string', isMultiple: true, default: ['cat']}); 107 | expectAssignable({type: 'number', isMultiple: true, default: [42]}); 108 | expectAssignable({type: 'boolean', isMultiple: true, default: [false]}); 109 | 110 | expectError({type: 'string', isMultiple: true, default: 'cat'}); 111 | expectError({type: 'number', isMultiple: true, default: 42}); 112 | expectError({type: 'boolean', isMultiple: true, default: false}); 113 | 114 | expectAssignable({type: 'string', choices: ['cat', 'unicorn']}); 115 | expectAssignable({type: 'number', choices: [1, 2]}); 116 | expectAssignable({type: 'boolean', choices: [true, false]}); 117 | expectAssignable({type: 'string', isMultiple: true, choices: ['cat']}); 118 | expectAssignable({type: 'string', isMultiple: false, choices: ['cat']}); 119 | 120 | expectError({type: 'string', choices: 'cat'}); 121 | expectError({type: 'number', choices: 1}); 122 | expectError({type: 'boolean', choices: true}); 123 | 124 | expectError({type: 'string', choices: [1]}); 125 | expectError({type: 'number', choices: ['cat']}); 126 | expectError({type: 'boolean', choices: ['cat']}); 127 | 128 | expectAssignable({choices: ['cat']}); 129 | expectAssignable({choices: [1]}); 130 | expectAssignable({choices: [true]}); 131 | expectError({choices: ['cat', 1, true]}); 132 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable ava/no-ignored-test-files */ 2 | import {fileURLToPath} from 'node:url'; 3 | import test from 'ava'; 4 | import {execa} from 'execa'; 5 | import {readPackage} from 'read-pkg'; 6 | import {createTag, stripIndentTransformer, trimResultTransformer} from 'common-tags'; 7 | import StackUtils from 'stack-utils'; 8 | 9 | export const defaultFixture = 'fixture.js'; 10 | 11 | const getFixture = fixture => fileURLToPath(new URL(`fixtures/${fixture}`, import.meta.url)); 12 | 13 | export const spawnFixture = async (fixture = defaultFixture, arguments_ = [], options = {}) => { 14 | // Allow calling with arguments first 15 | if (Array.isArray(fixture)) { 16 | arguments_ = fixture; 17 | fixture = defaultFixture; 18 | } 19 | 20 | return execa(getFixture(fixture), arguments_, options); 21 | }; 22 | 23 | export {stripIndent} from 'common-tags'; 24 | 25 | // Use old behavior prior to zspecza/common-tags#165 26 | export const stripIndentTrim = createTag( 27 | stripIndentTransformer(), 28 | trimResultTransformer(), 29 | ); 30 | 31 | export const meowPackage = await readPackage(); 32 | export const meowVersion = meowPackage.version; 33 | 34 | const stackUtils = new StackUtils(); 35 | 36 | export const stackToErrorMessage = stack => stackUtils.clean(stack).split('\n').at(0); 37 | 38 | export const _verifyCli = (baseFixture = defaultFixture) => test.macro( 39 | async (t, {fixture = baseFixture, args, execaOptions, expected, error}) => { 40 | const assertions = await t.try(async tt => { 41 | const arguments_ = args ? args.split(' ') : []; 42 | const {all: output, exitCode} = await spawnFixture(fixture, arguments_, {reject: false, all: true, ...execaOptions}); 43 | tt.log('args:', arguments_); 44 | 45 | if (error) { 46 | tt.log(`error (code ${exitCode}):\n`, output); 47 | 48 | if (typeof error === 'string') { 49 | tt.is(output, error); 50 | tt.is(exitCode, 2); 51 | } else { 52 | const error_ = error.clean ? stackToErrorMessage(output) : output; 53 | 54 | tt.is(error_, error.message); 55 | tt.is(exitCode, error.code); 56 | } 57 | } else { 58 | tt.log('output:\n', output); 59 | 60 | if (expected) { 61 | tt.is(output, expected); 62 | } else { 63 | tt.pass(); 64 | } 65 | } 66 | }); 67 | 68 | assertions.commit({retainLogs: !assertions.passed}); 69 | }, 70 | ); 71 | -------------------------------------------------------------------------------- /test/build.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import meow from '../build/index.js'; 3 | import {_verifyCli, meowVersion} from './_utils.js'; 4 | 5 | const verifyCli = _verifyCli(); 6 | 7 | test('main', t => { 8 | const cli = meow(` 9 | Usage 10 | foo 11 | `, { 12 | importMeta: import.meta, 13 | argv: 'foo --foo-bar --u cat -- unicorn cake'.split(' '), 14 | flags: { 15 | unicorn: { 16 | shortFlag: 'u', 17 | }, 18 | meow: { 19 | default: 'dog', 20 | }, 21 | '--': true, 22 | }, 23 | }); 24 | 25 | t.like(cli, { 26 | input: ['foo'], 27 | flags: { 28 | fooBar: true, 29 | meow: 'dog', 30 | unicorn: 'cat', 31 | '--': [ 32 | 'unicorn', 33 | 'cake', 34 | ], 35 | }, 36 | pkg: { 37 | name: 'meow', 38 | }, 39 | help: '\n CLI app helper\n\n Usage\n foo \n', 40 | }); 41 | }); 42 | 43 | test('spawn cli and show version', verifyCli, { 44 | args: '--version', 45 | expected: meowVersion, 46 | }); 47 | -------------------------------------------------------------------------------- /test/fixtures/allow-unknown-flags/fixture-with-help.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import meow from '../../../source/index.js'; 3 | 4 | const cli = meow({ 5 | importMeta: import.meta, 6 | description: 'Custom description', 7 | help: ` 8 | Usage 9 | foo 10 | `, 11 | allowUnknownFlags: false, 12 | flags: { 13 | help: { 14 | shortFlag: 'h', 15 | type: 'boolean', 16 | }, 17 | version: { 18 | shortFlag: 'v', 19 | type: 'boolean', 20 | }, 21 | }, 22 | }); 23 | 24 | console.log(cli.flags.help); 25 | -------------------------------------------------------------------------------- /test/fixtures/allow-unknown-flags/fixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import meow from '../../../source/index.js'; 4 | 5 | const cli = meow({ 6 | importMeta: import.meta, 7 | description: 'Custom description', 8 | help: ` 9 | Usage 10 | foo 11 | `, 12 | autoVersion: !process.argv.includes('--no-auto-version'), 13 | autoHelp: !process.argv.includes('--no-auto-help'), 14 | allowUnknownFlags: false, 15 | flags: { 16 | foo: { 17 | type: 'string', 18 | }, 19 | // For testing we need those as known flags 20 | noAutoHelp: { 21 | type: 'boolean', 22 | }, 23 | noAutoVersion: { 24 | type: 'boolean', 25 | }, 26 | }, 27 | }); 28 | 29 | console.log(cli.flags.foo); 30 | -------------------------------------------------------------------------------- /test/fixtures/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import meow from '../../build/index.js'; 4 | 5 | const cli = meow(` 6 | Usage 7 | foo 8 | `, { 9 | importMeta: import.meta, 10 | description: 'Custom description', 11 | autoVersion: !process.argv.includes('--no-auto-version'), 12 | autoHelp: !process.argv.includes('--no-auto-help'), 13 | flags: { 14 | unicorn: { 15 | shortFlag: 'u', 16 | }, 17 | meow: { 18 | default: 'dog', 19 | }, 20 | camelCaseOption: { 21 | default: 'foo', 22 | }, 23 | }, 24 | }); 25 | 26 | if (cli.flags.camelCaseOption === 'foo') { 27 | for (const flagKey of Object.keys(cli.flags)) { 28 | console.log(flagKey); 29 | } 30 | } else { 31 | console.log(cli.flags.camelCaseOption); 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/fixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import meow from '../../source/index.js'; 4 | 5 | const cli = meow({ 6 | importMeta: import.meta, 7 | description: 'Custom description', 8 | help: ` 9 | Usage 10 | foo 11 | `, 12 | autoVersion: !process.argv.includes('--no-auto-version'), 13 | autoHelp: !process.argv.includes('--no-auto-help'), 14 | flags: { 15 | unicorn: {shortFlag: 'u'}, 16 | meow: {default: 'dog'}, 17 | camelCaseOption: {default: 'foo'}, 18 | }, 19 | }); 20 | 21 | if (cli.flags.camelCaseOption === 'foo') { 22 | for (const x of Object.keys(cli.flags)) { 23 | console.log(x); 24 | } 25 | } else { 26 | console.log(cli.flags.camelCaseOption); 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/help/fixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import meow from '../../../source/index.js'; 3 | 4 | const cli = meow({ 5 | importMeta: import.meta, 6 | help: 'foo', 7 | flags: { 8 | showHelp: {type: 'boolean'}, 9 | code: {type: 'number'}, 10 | }, 11 | }); 12 | 13 | const {code} = cli.flags; 14 | 15 | if (cli.flags.showHelp) { 16 | if (code !== undefined) { 17 | cli.showHelp(code); 18 | } 19 | 20 | cli.showHelp(); 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/help/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "version": "1.0.0", 4 | "bin": "./fixture.js", 5 | "type": "module" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/required/fixture-conditional-required-multiple.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import meow from '../../../source/index.js'; 3 | 4 | const cli = meow({ 5 | importMeta: import.meta, 6 | description: 'Custom description', 7 | help: ` 8 | Usage 9 | foo 10 | `, 11 | flags: { 12 | test: { 13 | type: 'number', 14 | shortFlag: 't', 15 | isRequired: () => false, 16 | isMultiple: true, 17 | }, 18 | }, 19 | }); 20 | 21 | console.log(cli.flags.test); 22 | -------------------------------------------------------------------------------- /test/fixtures/required/fixture-required-function.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import meow from '../../../source/index.js'; 3 | 4 | const cli = meow({ 5 | importMeta: import.meta, 6 | description: 'Custom description', 7 | help: ` 8 | Usage 9 | foo 10 | `, 11 | flags: { 12 | trigger: { 13 | type: 'boolean', 14 | shortFlag: 't', 15 | }, 16 | withTrigger: { 17 | type: 'string', 18 | isRequired: (flags, _) => flags.trigger, 19 | }, 20 | allowError: { 21 | type: 'boolean', 22 | shortFlag: 'a', 23 | }, 24 | shouldError: { 25 | type: 'boolean', 26 | isRequired: (flags, _) => 27 | flags.allowError ? 'should error' : false 28 | , 29 | }, 30 | }, 31 | }); 32 | 33 | console.log(`${cli.flags.trigger},${cli.flags.withTrigger}`); 34 | -------------------------------------------------------------------------------- /test/fixtures/required/fixture-required-multiple.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import meow from '../../../source/index.js'; 3 | 4 | const cli = meow({ 5 | importMeta: import.meta, 6 | description: 'Custom description', 7 | help: ` 8 | Usage 9 | foo 10 | `, 11 | flags: { 12 | test: { 13 | type: 'number', 14 | shortFlag: 't', 15 | isRequired: true, 16 | isMultiple: true, 17 | }, 18 | }, 19 | }); 20 | 21 | console.log(cli.flags.test); 22 | -------------------------------------------------------------------------------- /test/fixtures/required/fixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import meow from '../../../source/index.js'; 3 | 4 | const cli = meow({ 5 | importMeta: import.meta, 6 | description: 'Custom description', 7 | help: ` 8 | Usage 9 | foo 10 | `, 11 | flags: { 12 | test: { 13 | type: 'string', 14 | shortFlag: 't', 15 | isRequired: true, 16 | }, 17 | number: { 18 | type: 'number', 19 | isRequired: true, 20 | }, 21 | kebabCase: { 22 | type: 'string', 23 | isRequired: true, 24 | }, 25 | notRequired: { 26 | type: 'string', 27 | }, 28 | }, 29 | }); 30 | 31 | console.log(`${cli.flags.test},${cli.flags.number}`); 32 | -------------------------------------------------------------------------------- /test/fixtures/version/fixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import meow from '../../../source/index.js'; 4 | 5 | const version = process.env.VERSION === 'false' ? false : process.env.VERSION; 6 | 7 | const options = { 8 | importMeta: import.meta, 9 | version, 10 | autoVersion: !process.argv.includes('--no-auto-version'), 11 | flags: { 12 | showVersion: {type: 'boolean'}, 13 | }, 14 | }; 15 | 16 | if (options.version === undefined) { 17 | delete options.version; 18 | } 19 | 20 | const cli = meow(options); 21 | 22 | if (cli.flags.showVersion) { 23 | cli.showVersion(); 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/version/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "version": "1.0.0", 4 | "bin": "./fixture.js", 5 | "type": "module" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/with-package-json/custom-bin/fixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import meow from '../../../../source/index.js'; 4 | 5 | meow({importMeta: import.meta}); 6 | console.log(process.title); 7 | -------------------------------------------------------------------------------- /test/fixtures/with-package-json/custom-bin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "bin": { 4 | "bar": "./fixture.js" 5 | }, 6 | "type": "module" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/with-package-json/default/fixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import meow from '../../../../source/index.js'; 4 | 5 | meow({importMeta: import.meta}); 6 | console.log(process.title); 7 | -------------------------------------------------------------------------------- /test/fixtures/with-package-json/default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "bin": "./fixture.js", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/with-package-json/no-bin/fixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import meow from '../../../../source/index.js'; 4 | 5 | meow({importMeta: import.meta}); 6 | console.log(process.title); 7 | -------------------------------------------------------------------------------- /test/fixtures/with-package-json/no-bin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "type": "module" 4 | } 5 | -------------------------------------------------------------------------------- /test/flags/_utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable ava/no-ignored-test-files */ 2 | import test from 'ava'; 3 | import meow from '../../source/index.js'; 4 | 5 | export const _verifyFlags = importMeta => test.macro(async (t, {flags = {}, args, expected, error, ...meowOptions}) => { 6 | const assertions = await t.try(async tt => { 7 | const arguments_ = args?.split(' ') ?? []; 8 | 9 | meowOptions = { 10 | ...meowOptions, 11 | importMeta, 12 | argv: arguments_, 13 | flags, 14 | }; 15 | 16 | tt.log('arguments:', arguments_); 17 | 18 | if (error) { 19 | tt.throws(() => meow(meowOptions), { 20 | message(message) { 21 | tt.log('error:\n', message); 22 | return tt.is(message, error); 23 | }, 24 | }); 25 | } else { 26 | const cli = meow(meowOptions); 27 | 28 | if (expected) { 29 | tt.like(cli.flags, expected); 30 | } else { 31 | tt.pass(); 32 | } 33 | } 34 | }); 35 | 36 | assertions.commit({retainLogs: !assertions.passed}); 37 | }); 38 | -------------------------------------------------------------------------------- /test/flags/aliases.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import meow from '../../source/index.js'; 3 | 4 | const importMeta = import.meta; 5 | 6 | test('accepts one', t => { 7 | const cli = meow({ 8 | importMeta, 9 | argv: ['--foo=baz'], 10 | flags: { 11 | fooBar: { 12 | type: 'string', 13 | aliases: ['foo'], 14 | }, 15 | }, 16 | }); 17 | 18 | t.like(cli.flags, { 19 | fooBar: 'baz', 20 | }); 21 | }); 22 | 23 | test('accepts multiple', t => { 24 | const cli = meow({ 25 | importMeta, 26 | argv: ['--foo=baz1', '--bar=baz2'], 27 | flags: { 28 | fooBar: { 29 | type: 'string', 30 | aliases: ['foo', 'bar'], 31 | isMultiple: true, 32 | }, 33 | }, 34 | }); 35 | 36 | t.like(cli.flags, { 37 | fooBar: ['baz1', 'baz2'], 38 | }); 39 | }); 40 | 41 | test('can be a short flag', t => { 42 | const cli = meow({ 43 | importMeta, 44 | argv: ['--f=baz'], 45 | flags: { 46 | fooBar: { 47 | type: 'string', 48 | aliases: ['f'], 49 | }, 50 | }, 51 | }); 52 | 53 | t.like(cli.flags, { 54 | fooBar: 'baz', 55 | }); 56 | }); 57 | 58 | test('works with short flag', t => { 59 | const cli = meow({ 60 | importMeta, 61 | argv: ['--foo=baz1', '--bar=baz2', '-f=baz3'], 62 | flags: { 63 | fooBar: { 64 | type: 'string', 65 | shortFlag: 'f', 66 | aliases: ['foo', 'bar'], 67 | isMultiple: true, 68 | }, 69 | }, 70 | }); 71 | 72 | t.like(cli.flags, { 73 | fooBar: ['baz1', 'baz2', 'baz3'], 74 | }); 75 | }); 76 | 77 | test('unnormalized flags', t => { 78 | const cli = meow({ 79 | importMeta, 80 | argv: ['--foo=baz'], 81 | flags: { 82 | fooBar: { 83 | type: 'string', 84 | aliases: ['foo'], 85 | shortFlag: 'f', 86 | }, 87 | }, 88 | }); 89 | 90 | t.like(cli.unnormalizedFlags, { 91 | fooBar: 'baz', 92 | foo: 'baz', 93 | f: 'baz', 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/flags/allow-unknown-flags.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import indentString from 'indent-string'; 3 | import {_verifyCli, stripIndentTrim, meowVersion} from '../_utils.js'; 4 | 5 | const fixtureFolder = 'allow-unknown-flags'; 6 | 7 | const allowUnknownFlags = `${fixtureFolder}/fixture.js`; 8 | const allowUnknownFlagsWithHelp = `${fixtureFolder}/fixture-with-help.js`; 9 | 10 | const verifyFlags = _verifyCli(allowUnknownFlags); 11 | 12 | test('specifying unknown flags', verifyFlags, { 13 | args: '--foo bar --unspecified-a --unspecified-b input-is-allowed', 14 | error: stripIndentTrim` 15 | Unknown flags 16 | --unspecified-a 17 | --unspecified-b 18 | `, 19 | 20 | }); 21 | 22 | test('specifying known flags', verifyFlags, { 23 | args: '--foo bar', 24 | expected: 'bar', 25 | }); 26 | 27 | test('help as a known flag', verifyFlags, { 28 | args: '--help', 29 | expected: indentString('\nCustom description\n\nUsage\n foo \n\n', 2), 30 | }); 31 | 32 | test('version as a known flag', verifyFlags, { 33 | args: '--version', 34 | expected: meowVersion, 35 | }); 36 | 37 | test('help as an unknown flag', verifyFlags, { 38 | args: '--help --no-auto-help', 39 | error: stripIndentTrim` 40 | Unknown flag 41 | --help 42 | `, 43 | }); 44 | 45 | test('version as an unknown flag', verifyFlags, { 46 | args: '--version --no-auto-version', 47 | error: stripIndentTrim` 48 | Unknown flag 49 | --version 50 | `, 51 | }); 52 | 53 | test('help with custom config', verifyFlags, { 54 | fixture: allowUnknownFlagsWithHelp, 55 | args: '-h', 56 | expected: indentString('\nCustom description\n\nUsage\n foo \n\n', 2), 57 | }); 58 | 59 | test('version with custom config', verifyFlags, { 60 | fixture: allowUnknownFlagsWithHelp, 61 | args: '-v', 62 | expected: meowVersion, 63 | }); 64 | -------------------------------------------------------------------------------- /test/flags/boolean-default.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {_verifyFlags} from './_utils.js'; 3 | 4 | const verifyFlags = _verifyFlags(import.meta); 5 | 6 | test('undefined - filter out unset boolean args', verifyFlags, { 7 | booleanDefault: undefined, 8 | flags: { 9 | foo: { 10 | type: 'boolean', 11 | }, 12 | bar: { 13 | type: 'boolean', 14 | }, 15 | baz: { 16 | type: 'boolean', 17 | default: false, 18 | }, 19 | }, 20 | args: '--foo', 21 | expected: { 22 | foo: true, 23 | bar: undefined, 24 | baz: false, 25 | }, 26 | }); 27 | 28 | test('boolean args are false by default', verifyFlags, { 29 | flags: { 30 | foo: { 31 | type: 'boolean', 32 | }, 33 | bar: { 34 | type: 'boolean', 35 | default: true, 36 | }, 37 | baz: { 38 | type: 'boolean', 39 | }, 40 | }, 41 | args: '--foo', 42 | expected: { 43 | foo: true, 44 | bar: true, 45 | baz: false, 46 | }, 47 | }); 48 | 49 | test('throws if default is null', verifyFlags, { 50 | booleanDefault: null, 51 | flags: { 52 | foo: { 53 | type: 'boolean', 54 | }, 55 | }, 56 | args: '--foo', 57 | error: 'Expected "foo" default value to be of type "boolean", got "null"', 58 | }); 59 | -------------------------------------------------------------------------------- /test/flags/choices.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {oneLine} from 'common-tags'; 3 | import {stripIndentTrim} from '../_utils.js'; 4 | import {_verifyFlags} from './_utils.js'; 5 | 6 | const verifyChoices = _verifyFlags(import.meta); 7 | 8 | test('success case', verifyChoices, { 9 | flags: { 10 | animal: { 11 | choices: ['dog', 'cat', 'unicorn'], 12 | }, 13 | number: { 14 | type: 'number', 15 | choices: [1.1, 2.2, 3.3], 16 | }, 17 | }, 18 | args: '--animal cat --number=2.2', 19 | expected: { 20 | animal: 'cat', 21 | number: 2.2, 22 | }, 23 | }); 24 | 25 | test('throws if input does not match choices', verifyChoices, { 26 | flags: { 27 | animal: { 28 | choices: ['dog', 'cat', 'unicorn'], 29 | }, 30 | number: { 31 | choices: [1, 2, 3], 32 | }, 33 | }, 34 | args: '--animal rainbow --number 5', 35 | error: stripIndentTrim` 36 | Unknown value for flag \`--animal\`: \`rainbow\`. Value must be one of: [\`dog\`, \`cat\`, \`unicorn\`] 37 | Unknown value for flag \`--number\`: \`5\`. Value must be one of: [\`1\`, \`2\`, \`3\`] 38 | `, 39 | }); 40 | 41 | test('throws if choices is not array', verifyChoices, { 42 | flags: { 43 | animal: { 44 | choices: 'cat', 45 | }, 46 | }, 47 | args: '--animal cat', 48 | error: 'The option `choices` must be an array. Invalid flags: `--animal`', 49 | }); 50 | 51 | test('does not throw error when isRequired is false', verifyChoices, { 52 | flags: { 53 | animal: { 54 | isRequired: false, 55 | choices: ['dog', 'cat', 'unicorn'], 56 | }, 57 | }, 58 | }); 59 | 60 | test('throw error when isRequired is true', verifyChoices, { 61 | flags: { 62 | animal: { 63 | isRequired: true, 64 | choices: ['dog', 'cat', 'unicorn'], 65 | }, 66 | }, 67 | error: 'Flag `--animal` has no value. Value must be one of: [`dog`, `cat`, `unicorn`]', 68 | }); 69 | 70 | test('success with isMultiple', verifyChoices, { 71 | flags: { 72 | animal: { 73 | type: 'string', 74 | isMultiple: true, 75 | choices: ['dog', 'cat', 'unicorn'], 76 | }, 77 | }, 78 | args: '--animal=dog --animal=unicorn', 79 | expected: { 80 | animal: ['dog', 'unicorn'], 81 | }, 82 | }); 83 | 84 | test('throws with isMultiple, one unknown value', verifyChoices, { 85 | flags: { 86 | animal: { 87 | type: 'string', 88 | isMultiple: true, 89 | choices: ['dog', 'cat', 'unicorn'], 90 | }, 91 | }, 92 | args: '--animal=dog --animal=rabbit', 93 | error: 'Unknown value for flag `--animal`: `rabbit`. Value must be one of: [`dog`, `cat`, `unicorn`]', 94 | }); 95 | 96 | test('throws with isMultiple, multiple unknown value', verifyChoices, { 97 | flags: { 98 | animal: { 99 | type: 'string', 100 | isMultiple: true, 101 | choices: ['cat', 'unicorn'], 102 | }, 103 | }, 104 | args: '--animal=dog --animal=rabbit', 105 | error: 'Unknown values for flag `--animal`: `dog`, `rabbit`. Value must be one of: [`cat`, `unicorn`]', 106 | }); 107 | 108 | test('throws with multiple flags', verifyChoices, { 109 | flags: { 110 | animal: { 111 | type: 'string', 112 | choices: ['cat', 'unicorn'], 113 | }, 114 | plant: { 115 | type: 'string', 116 | choices: ['tree', 'flower'], 117 | }, 118 | }, 119 | args: '--animal=dog --plant=succulent', 120 | error: stripIndentTrim` 121 | Unknown value for flag \`--animal\`: \`dog\`. Value must be one of: [\`cat\`, \`unicorn\`] 122 | Unknown value for flag \`--plant\`: \`succulent\`. Value must be one of: [\`tree\`, \`flower\`] 123 | `, 124 | }); 125 | 126 | test('choices must be of the same type', verifyChoices, { 127 | flags: { 128 | number: { 129 | type: 'number', 130 | choices: [1, '2'], 131 | }, 132 | boolean: { 133 | type: 'boolean', 134 | choices: [true, 'false'], 135 | }, 136 | }, 137 | error: oneLine` 138 | Each value of the option \`choices\` must be of the same type as its flag. 139 | Invalid flags: (\`--number\`, type: 'number'), (\`--boolean\`, type: 'boolean') 140 | `, 141 | }); 142 | 143 | test('success when each value of default exist within the option choices', verifyChoices, { 144 | flags: { 145 | number: { 146 | type: 'number', 147 | choices: [1, 2, 3], 148 | default: 1, 149 | }, 150 | string: { 151 | type: 'string', 152 | choices: ['dog', 'cat', 'unicorn'], 153 | default: 'dog', 154 | }, 155 | multiString: { 156 | type: 'string', 157 | choices: ['dog', 'cat', 'unicorn'], 158 | default: ['dog', 'cat'], 159 | isMultiple: true, 160 | }, 161 | }, 162 | }); 163 | 164 | test('throws when default does not only include valid choices', verifyChoices, { 165 | flags: { 166 | number: { 167 | type: 'number', 168 | choices: [1, 2, 3], 169 | default: 8, 170 | }, 171 | string: { 172 | type: 'string', 173 | choices: ['dog', 'cat'], 174 | default: 'unicorn', 175 | }, 176 | multiString: { 177 | type: 'string', 178 | choices: ['dog', 'cat'], 179 | default: ['dog', 'unicorn'], 180 | isMultiple: true, 181 | }, 182 | }, 183 | error: oneLine` 184 | Each value of the option \`default\` must exist within the option \`choices\`. 185 | Invalid flags: \`--number\`, \`--string\`, \`--multiString\` 186 | `, 187 | }); 188 | -------------------------------------------------------------------------------- /test/flags/is-multiple.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import meow from '../../source/index.js'; 3 | import {_verifyFlags} from './_utils.js'; 4 | 5 | const importMeta = import.meta; 6 | const verifyFlags = _verifyFlags(importMeta); 7 | 8 | test('unset flag returns empty array', verifyFlags, { 9 | flags: { 10 | foo: { 11 | type: 'string', 12 | isMultiple: true, 13 | }, 14 | }, 15 | expected: { 16 | foo: [], 17 | }, 18 | }); 19 | 20 | test('flag set once returns array', verifyFlags, { 21 | flags: { 22 | foo: { 23 | type: 'string', 24 | isMultiple: true, 25 | }, 26 | }, 27 | args: '--foo=bar', 28 | expected: { 29 | foo: ['bar'], 30 | }, 31 | }); 32 | 33 | test('flag set multiple times', verifyFlags, { 34 | flags: { 35 | foo: { 36 | type: 'string', 37 | isMultiple: true, 38 | }, 39 | }, 40 | args: '--foo=bar --foo=baz', 41 | expected: { 42 | foo: ['bar', 'baz'], 43 | }, 44 | }); 45 | 46 | test('flag with space separated values', t => { 47 | const cli = meow({ 48 | importMeta, 49 | argv: ['--foo', 'bar', 'baz'], 50 | flags: { 51 | foo: { 52 | type: 'string', 53 | isMultiple: true, 54 | }, 55 | }, 56 | }); 57 | 58 | t.like(cli, { 59 | input: ['baz'], 60 | flags: { 61 | foo: ['bar'], 62 | }, 63 | }); 64 | }); 65 | 66 | test('does not support comma separated values', verifyFlags, { 67 | flags: { 68 | foo: { 69 | type: 'string', 70 | isMultiple: true, 71 | }, 72 | }, 73 | args: '--foo bar,baz', 74 | expected: { 75 | foo: ['bar,baz'], 76 | }, 77 | }); 78 | 79 | test('default to type string', verifyFlags, { 80 | flags: { 81 | foo: { 82 | isMultiple: true, 83 | }, 84 | }, 85 | args: '--foo=bar', 86 | expected: { 87 | foo: ['bar'], 88 | }, 89 | }); 90 | 91 | test('boolean flag', verifyFlags, { 92 | flags: { 93 | foo: { 94 | type: 'boolean', 95 | isMultiple: true, 96 | }, 97 | }, 98 | args: '--foo --foo=false', 99 | expected: { 100 | foo: [true, false], 101 | }, 102 | }); 103 | 104 | test('boolean flag is false by default', verifyFlags, { 105 | flags: { 106 | foo: { 107 | type: 'boolean', 108 | isMultiple: true, 109 | }, 110 | }, 111 | expected: { 112 | foo: [false], 113 | }, 114 | }); 115 | 116 | test('number flag', verifyFlags, { 117 | flags: { 118 | foo: { 119 | type: 'number', 120 | isMultiple: true, 121 | }, 122 | }, 123 | args: '--foo=1.3 --foo=-1', 124 | expected: { 125 | foo: [1.3, -1], 126 | }, 127 | }); 128 | 129 | test('flag default values', verifyFlags, { 130 | flags: { 131 | string: { 132 | type: 'string', 133 | isMultiple: true, 134 | default: ['foo'], 135 | }, 136 | boolean: { 137 | type: 'boolean', 138 | isMultiple: true, 139 | default: [true], 140 | }, 141 | number: { 142 | type: 'number', 143 | isMultiple: true, 144 | default: [0.5], 145 | }, 146 | }, 147 | expected: { 148 | string: ['foo'], 149 | boolean: [true], 150 | number: [0.5], 151 | }, 152 | }); 153 | 154 | test('multiple flag default values', verifyFlags, { 155 | flags: { 156 | string: { 157 | type: 'string', 158 | isMultiple: true, 159 | default: ['foo', 'bar'], 160 | }, 161 | boolean: { 162 | type: 'boolean', 163 | isMultiple: true, 164 | default: [true, false], 165 | }, 166 | number: { 167 | type: 'number', 168 | isMultiple: true, 169 | default: [0.5, 1], 170 | }, 171 | }, 172 | expected: { 173 | string: ['foo', 'bar'], 174 | boolean: [true, false], 175 | number: [0.5, 1], 176 | }, 177 | }); 178 | 179 | // Happened in production 2020-05-10: https://github.com/sindresorhus/meow/pull/143#issuecomment-626287226 180 | test('handles multi-word flag name', verifyFlags, { 181 | flags: { 182 | fooBar: { 183 | type: 'string', 184 | isMultiple: true, 185 | }, 186 | }, 187 | args: '--foo-bar=baz', 188 | expected: { 189 | fooBar: ['baz'], 190 | }, 191 | }); 192 | 193 | test('works with short flags', verifyFlags, { 194 | flags: { 195 | foo: { 196 | type: 'string', 197 | shortFlag: 'f', 198 | isMultiple: true, 199 | }, 200 | }, 201 | args: '-f bar -f baz', 202 | expected: { 203 | foo: ['bar', 'baz'], 204 | }, 205 | }); 206 | -------------------------------------------------------------------------------- /test/flags/is-required.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {_verifyCli, stripIndentTrim} from '../_utils.js'; 3 | 4 | const fixtureFolder = 'required'; 5 | 6 | const required = `${fixtureFolder}/fixture.js`; 7 | const requiredFunction = `${fixtureFolder}/fixture-required-function.js`; 8 | const requiredMultiple = `${fixtureFolder}/fixture-required-multiple.js`; 9 | const conditionalRequiredMultiple = `${fixtureFolder}/fixture-conditional-required-multiple.js`; 10 | 11 | const verifyFlags = _verifyCli(required); 12 | 13 | test('not specifying required flags', verifyFlags, { 14 | error: stripIndentTrim` 15 | Missing required flags 16 | --test, -t 17 | --number 18 | --kebab-case 19 | `, 20 | }); 21 | 22 | test('specifying all required flags', verifyFlags, { 23 | args: '--test test --number 6 --kebab-case test', 24 | expected: 'test,6', 25 | }); 26 | 27 | test('specifying required string flag with an empty string as value', verifyFlags, { 28 | args: '--test ', 29 | error: stripIndentTrim` 30 | Missing required flags 31 | --number 32 | --kebab-case 33 | `, 34 | }); 35 | 36 | test('specifying required number flag without a number', verifyFlags, { 37 | args: '--number', 38 | error: stripIndentTrim` 39 | Missing required flags 40 | --test, -t 41 | --number 42 | --kebab-case 43 | `, 44 | }); 45 | 46 | test('setting isRequired as a function and not specifying any flags', verifyFlags, { 47 | fixture: requiredFunction, 48 | expected: 'false,undefined', 49 | }); 50 | 51 | test('setting isRequired as a function and specifying only the flag that activates the isRequired condition for the other flag', verifyFlags, { 52 | fixture: requiredFunction, 53 | args: '--trigger', 54 | error: stripIndentTrim` 55 | Missing required flag 56 | --with-trigger 57 | `, 58 | }); 59 | 60 | test('setting isRequired as a function and specifying both the flags', verifyFlags, { 61 | fixture: requiredFunction, 62 | args: '--trigger --withTrigger specified', 63 | expected: 'true,specified', 64 | }); 65 | 66 | test('setting isRequired as a function and check if returning a non-boolean value throws an error', verifyFlags, { 67 | fixture: requiredFunction, 68 | args: '--allowError --shouldError specified', 69 | error: { 70 | clean: true, 71 | message: 'TypeError: Return value for isRequired callback should be of type boolean, but string was returned.', 72 | code: 1, 73 | }, 74 | }); 75 | 76 | test('isRequired with isMultiple giving a single value', verifyFlags, { 77 | fixture: requiredMultiple, 78 | args: '--test 1', 79 | expected: '[ 1 ]', 80 | }); 81 | 82 | test('isRequired with isMultiple giving multiple values', verifyFlags, { 83 | fixture: requiredMultiple, 84 | args: '--test 1 --test 2', 85 | expected: '[ 1, 2 ]', 86 | }); 87 | 88 | test('isRequired with isMultiple giving no values, but flag is given', verifyFlags, { 89 | fixture: requiredMultiple, 90 | args: '--test', 91 | error: stripIndentTrim` 92 | Missing required flag 93 | --test, -t 94 | `, 95 | }); 96 | 97 | test('isRequired with isMultiple giving no values, but flag is not given', verifyFlags, { 98 | fixture: requiredMultiple, 99 | error: stripIndentTrim` 100 | Missing required flag 101 | --test, -t 102 | `, 103 | }); 104 | 105 | test('isRequire function that returns false with isMultiple given no values, but flag is not given', verifyFlags, { 106 | fixture: conditionalRequiredMultiple, 107 | expected: '[]', 108 | }); 109 | -------------------------------------------------------------------------------- /test/flags/short-flag.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import meow from '../../source/index.js'; 3 | import {_verifyFlags} from './_utils.js'; 4 | 5 | const importMeta = import.meta; 6 | const verifyFlags = _verifyFlags(importMeta); 7 | 8 | test('can be used in place of long flags', t => { 9 | const cli = meow({ 10 | importMeta, 11 | argv: ['-f'], 12 | flags: { 13 | foo: { 14 | type: 'boolean', 15 | shortFlag: 'f', 16 | }, 17 | }, 18 | }); 19 | 20 | t.like(cli.flags, { 21 | foo: true, 22 | f: undefined, 23 | }); 24 | 25 | t.like(cli.unnormalizedFlags, { 26 | foo: true, 27 | f: true, 28 | }); 29 | }); 30 | 31 | test('grouped flags work', t => { 32 | const cli = meow({ 33 | importMeta, 34 | argv: ['-cl'], 35 | flags: { 36 | coco: { 37 | type: 'boolean', 38 | shortFlag: 'c', 39 | }, 40 | loco: { 41 | type: 'boolean', 42 | shortFlag: 'l', 43 | }, 44 | }, 45 | }); 46 | 47 | t.like(cli.flags, { 48 | coco: true, 49 | loco: true, 50 | c: undefined, 51 | l: undefined, 52 | }); 53 | 54 | t.like(cli.unnormalizedFlags, { 55 | coco: true, 56 | loco: true, 57 | c: true, 58 | l: true, 59 | }); 60 | }); 61 | 62 | test('suggests renaming alias to shortFlag', verifyFlags, { 63 | flags: { 64 | foo: { 65 | type: 'string', 66 | alias: 'f', 67 | }, 68 | bar: { 69 | type: 'string', 70 | alias: 'b', 71 | }, 72 | baz: { 73 | type: 'string', 74 | shortFlag: 'z', 75 | }, 76 | }, 77 | error: 'The option `alias` has been renamed to `shortFlag`. The following flags need to be updated: `--foo`, `--bar`', 78 | }); 79 | -------------------------------------------------------------------------------- /test/flags/test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {stripIndentTrim} from '../_utils.js'; 3 | import {_verifyFlags} from './_utils.js'; 4 | 5 | const verifyFlags = _verifyFlags(import.meta); 6 | 7 | test('flag types', verifyFlags, { 8 | flags: { 9 | foo: {type: 'string'}, 10 | bar: {type: 'number'}, 11 | baz: {type: 'boolean'}, 12 | }, 13 | args: '--foo=bar --bar=1.3 --baz=false', 14 | expected: { 15 | foo: 'bar', 16 | bar: 1.3, 17 | baz: false, 18 | }, 19 | }); 20 | 21 | test('supports negation via --no', verifyFlags, { 22 | flags: { 23 | foo: { 24 | type: 'boolean', 25 | default: true, 26 | }, 27 | }, 28 | args: '--no-foo', 29 | expected: { 30 | foo: false, 31 | }, 32 | }); 33 | 34 | test('throws if default value is not of the correct type', verifyFlags, { 35 | flags: { 36 | foo: { 37 | type: 'number', 38 | default: 'x', 39 | }, 40 | }, 41 | error: 'Expected "foo" default value to be of type "number", got "string"', 42 | }); 43 | 44 | test('flag, no value', verifyFlags, { 45 | flags: { 46 | foo: {type: 'string'}, 47 | bar: {type: 'number'}, 48 | }, 49 | args: '--foo --bar', 50 | expected: { 51 | foo: '', 52 | bar: undefined, 53 | }, 54 | }); 55 | 56 | test('default - flag, no value', verifyFlags, { 57 | flags: { 58 | foo: {type: 'string', default: 'bar'}, 59 | bar: {type: 'number', default: 1.3}, 60 | }, 61 | args: '--foo --bar', 62 | expected: { 63 | foo: 'bar', 64 | bar: 1.3, 65 | }, 66 | }); 67 | 68 | test('default - no flag', verifyFlags, { 69 | flags: { 70 | foo: {type: 'string', default: 'bar'}, 71 | bar: {type: 'number', default: 1.3}, 72 | }, 73 | args: '', 74 | expected: { 75 | foo: 'bar', 76 | bar: 1.3, 77 | }, 78 | }); 79 | 80 | test('single character flag casing should be preserved', verifyFlags, { 81 | args: '-F', 82 | expected: { 83 | F: true, 84 | }, 85 | }); 86 | 87 | test('flag declared in kebab-case is an error', verifyFlags, { 88 | flags: { 89 | 'kebab-case': {type: 'boolean'}, 90 | test: {type: 'boolean'}, 91 | 'another-one': {type: 'boolean'}, 92 | }, 93 | error: 'Flag keys may not contain \'-\'. Invalid flags: `kebab-case`, `another-one`', 94 | }); 95 | 96 | test('single flag set more than once is an error', verifyFlags, { 97 | flags: { 98 | foo: { 99 | type: 'string', 100 | }, 101 | }, 102 | args: '--foo=bar --foo=baz', 103 | error: 'The flag --foo can only be set once.', 104 | }); 105 | 106 | test('options - multiple validation errors', verifyFlags, { 107 | flags: { 108 | animal: { 109 | type: 'string', 110 | choices: 'cat', 111 | }, 112 | plant: { 113 | type: 'string', 114 | alias: 'p', 115 | }, 116 | 'some-thing': { 117 | type: 'string', 118 | }, 119 | }, 120 | error: stripIndentTrim` 121 | Flag keys may not contain '-'. Invalid flags: \`some-thing\` 122 | The option \`alias\` has been renamed to \`shortFlag\`. The following flags need to be updated: \`--plant\` 123 | The option \`choices\` must be an array. Invalid flags: \`--animal\` 124 | `, 125 | }); 126 | -------------------------------------------------------------------------------- /test/options/help.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import indentString from 'indent-string'; 3 | import meow from '../../source/index.js'; 4 | import {_verifyCli, stripIndent, stripIndentTrim} from '../_utils.js'; 5 | 6 | const importMeta = import.meta; 7 | 8 | const verifyCli = _verifyCli(); 9 | 10 | const verifyHelp = test.macro(async (t, {cli: cliArguments, expected}) => { 11 | const assertions = await t.try(async tt => { 12 | const cli = Array.isArray(cliArguments) 13 | ? meow(cliArguments.at(0), {importMeta, ...cliArguments.at(1)}) 14 | : meow({importMeta, ...cliArguments}); 15 | 16 | tt.log('help text:\n', cli.help); 17 | tt.is(cli.help, expected); 18 | }); 19 | 20 | assertions.commit({retainLogs: !assertions.passed}); 21 | }); 22 | 23 | test('support help shortcut', verifyHelp, { 24 | cli: [` 25 | unicorn 26 | cat 27 | `], 28 | expected: indentString(stripIndent` 29 | 30 | CLI app helper 31 | 32 | unicorn 33 | cat 34 | `, 2), 35 | }); 36 | 37 | test('spawn cli and show help screen', verifyCli, { 38 | args: '--help', 39 | expected: indentString(stripIndent` 40 | 41 | Custom description 42 | 43 | Usage 44 | foo 45 | 46 | `, 2), 47 | }); 48 | 49 | test('spawn cli and disabled autoHelp', verifyCli, { 50 | args: '--help --no-auto-help', 51 | expected: stripIndentTrim` 52 | help 53 | autoHelp 54 | meow 55 | camelCaseOption 56 | `, 57 | }); 58 | 59 | test('spawn cli and not show help', verifyCli, { 60 | args: '--help=all', 61 | expected: stripIndentTrim` 62 | help 63 | meow 64 | camelCaseOption 65 | `, 66 | }); 67 | 68 | test('single line help messages are not indented', verifyHelp, { 69 | cli: { 70 | description: false, 71 | help: 'single line', 72 | }, 73 | expected: stripIndent` 74 | 75 | single line 76 | `, 77 | }); 78 | 79 | test('descriptions with no help are not indented', verifyHelp, { 80 | cli: { 81 | help: false, 82 | description: 'single line', 83 | }, 84 | expected: stripIndent` 85 | 86 | single line 87 | `, 88 | 89 | }); 90 | 91 | test('support help shortcut with no indentation', verifyHelp, { 92 | cli: [` 93 | unicorn 94 | cat 95 | `, { 96 | helpIndent: 0, 97 | }], 98 | expected: stripIndent` 99 | 100 | CLI app helper 101 | 102 | unicorn 103 | cat 104 | `, 105 | }); 106 | 107 | test('no description and no indentation', verifyHelp, { 108 | cli: [` 109 | unicorn 110 | cat 111 | `, { 112 | helpIndent: 0, 113 | description: false, 114 | }], 115 | expected: stripIndent` 116 | 117 | unicorn 118 | cat 119 | `, 120 | }); 121 | 122 | test('exits with code 0 by default', verifyCli, { 123 | args: '--help', 124 | }); 125 | 126 | test('showHelp exits with code 2 by default', verifyCli, { 127 | fixture: 'help/fixture.js', 128 | args: '--show-help', 129 | error: { 130 | message: stripIndent` 131 | 132 | foo 133 | `, 134 | code: 2, 135 | }, 136 | }); 137 | 138 | test('showHelp exits with given code', verifyCli, { 139 | fixture: 'help/fixture.js', 140 | args: '--show-help --code=0', 141 | expected: stripIndent` 142 | 143 | foo 144 | `, 145 | }); 146 | -------------------------------------------------------------------------------- /test/options/import-meta.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import meow from '../../source/index.js'; 3 | 4 | const verifyImportMeta = test.macro((t, {cli, error}) => { 5 | if (error) { 6 | t.throws(cli, {message: 'The `importMeta` option is required. Its value must be `import.meta`.'}); 7 | } else { 8 | t.notThrows(cli); 9 | } 10 | }); 11 | 12 | test('main', verifyImportMeta, { 13 | cli: () => meow({ 14 | importMeta: import.meta, 15 | }), 16 | }); 17 | 18 | test('with help shortcut', verifyImportMeta, { 19 | cli: () => meow(` 20 | unicorns 21 | rainbows 22 | `, { 23 | importMeta: import.meta, 24 | }), 25 | }); 26 | 27 | test('invalid package url', verifyImportMeta, { 28 | cli: () => meow({importMeta: '/path/to/package'}), 29 | error: true, 30 | }); 31 | 32 | test('throws if unset', verifyImportMeta, { 33 | cli: () => meow('foo', {}), 34 | error: true, 35 | }); 36 | 37 | test('throws if unset - options only', verifyImportMeta, { 38 | cli: () => meow({}), 39 | error: true, 40 | }); 41 | 42 | test('throws if unset - help shortcut only', verifyImportMeta, { 43 | cli: () => meow('foo'), 44 | error: true, 45 | }); 46 | -------------------------------------------------------------------------------- /test/options/infer-type.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import meow from '../../source/index.js'; 3 | 4 | const verifyTypeInference = test.macro((t, {cli: cliArguments, expected}) => { 5 | const cli = meow({importMeta: import.meta, ...cliArguments}); 6 | t.like(cli.input, [expected]); 7 | }); 8 | 9 | test('no inference by default', verifyTypeInference, { 10 | cli: { 11 | argv: ['5'], 12 | }, 13 | expected: '5', 14 | }); 15 | 16 | test('type inference', verifyTypeInference, { 17 | cli: { 18 | argv: ['5'], 19 | inferType: true, 20 | }, 21 | expected: 5, 22 | }); 23 | 24 | test('with input type', verifyTypeInference, { 25 | cli: { 26 | argv: ['5'], 27 | input: 'number', 28 | }, 29 | expected: 5, 30 | }); 31 | 32 | test('works with flags', verifyTypeInference, { 33 | cli: { 34 | argv: ['5'], 35 | inferType: true, 36 | flags: {foo: 'string'}, 37 | }, 38 | expected: 5, 39 | }); 40 | -------------------------------------------------------------------------------- /test/options/pkg.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {_verifyCli, stripIndent} from '../_utils.js'; 3 | import meow from '../../source/index.js'; 4 | 5 | const importMeta = import.meta; 6 | 7 | const verifyPackage = _verifyCli('with-package-json/default/fixture.js'); 8 | 9 | test('description', t => { 10 | const cli = meow({ 11 | importMeta, 12 | pkg: { 13 | description: 'Unicorn and rainbow creator', 14 | }, 15 | }); 16 | 17 | t.is(cli.help, stripIndent` 18 | 19 | Unicorn and rainbow creator 20 | `); 21 | }); 22 | 23 | test.todo('version'); 24 | 25 | test('overriding pkg still normalizes', t => { 26 | const cli = meow({ 27 | importMeta, 28 | pkg: { 29 | name: 'browser-sync', 30 | bin: './bin/browser-sync.js', 31 | }, 32 | }); 33 | 34 | t.like(cli, { 35 | pkg: { 36 | name: 'browser-sync', 37 | version: '', 38 | }, 39 | }); 40 | 41 | // TODO: test that showVersion logs undefined 42 | }); 43 | 44 | test('process title - bin default', verifyPackage, { 45 | expected: 'foo', 46 | }); 47 | 48 | test('process title - bin custom', verifyPackage, { 49 | fixture: 'with-package-json/custom-bin/fixture.js', 50 | expected: 'bar', 51 | }); 52 | 53 | test('process title - name backup', verifyPackage, { 54 | fixture: 'with-package-json/no-bin/fixture.js', 55 | expected: 'foo', 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /test/options/version.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import {_verifyCli, defaultFixture, stripIndentTrim} from '../_utils.js'; 3 | 4 | const verifyVersion = _verifyCli('version/fixture.js'); 5 | 6 | test('spawn cli and show version', verifyVersion, { 7 | args: '--version', 8 | expected: '1.0.0', 9 | }); 10 | 11 | test('spawn cli and disabled autoVersion', verifyVersion, { 12 | fixture: defaultFixture, 13 | args: '--version --no-auto-version', 14 | expected: stripIndentTrim` 15 | version 16 | autoVersion 17 | meow 18 | camelCaseOption 19 | `, 20 | }); 21 | 22 | test('spawn cli and not show version', verifyVersion, { 23 | fixture: defaultFixture, 24 | args: '--version=beta', 25 | expected: stripIndentTrim` 26 | version 27 | meow 28 | camelCaseOption 29 | `, 30 | }); 31 | 32 | test('custom version', verifyVersion, { 33 | args: '--version', 34 | execaOptions: {env: {VERSION: 'beta'}}, 35 | expected: 'beta', 36 | }); 37 | 38 | test('version = false has no effect', verifyVersion, { 39 | args: '--version', 40 | execaOptions: {env: {VERSION: 'false'}}, 41 | expected: 'false', 42 | }); 43 | 44 | test('manual showVersion', verifyVersion, { 45 | args: '--show-version', 46 | expected: '1.0.0', 47 | }); 48 | 49 | test('no version fallback message', verifyVersion, { 50 | fixture: 'with-package-json/default/fixture.js', 51 | args: '--version', 52 | expected: 'No version found', 53 | }); 54 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import indentString from 'indent-string'; 3 | import meow from '../source/index.js'; 4 | import {_verifyCli, stripIndentTrim} from './_utils.js'; 5 | 6 | const importMeta = import.meta; 7 | const verifyCli = _verifyCli(); 8 | 9 | test('return object', t => { 10 | const cli = meow({ 11 | importMeta, 12 | argv: ['foo', '--foo-bar', '-u', 'cat', '--', 'unicorn', 'cake'], 13 | help: ` 14 | Usage 15 | foo 16 | `, 17 | flags: { 18 | unicorn: {shortFlag: 'u'}, 19 | meow: {default: 'dog'}, 20 | '--': true, 21 | }, 22 | }); 23 | 24 | t.like(cli, { 25 | input: ['foo'], 26 | flags: { 27 | fooBar: true, 28 | meow: 'dog', 29 | unicorn: 'cat', 30 | '--': ['unicorn', 'cake'], 31 | }, 32 | pkg: { 33 | name: 'meow', 34 | }, 35 | help: indentString('\nCLI app helper\n\nUsage\n foo \n', 2), 36 | }); 37 | }); 38 | 39 | test('spawn cli and disabled autoVersion and autoHelp', verifyCli, { 40 | args: '--version --help', 41 | expected: stripIndentTrim` 42 | version 43 | help 44 | meow 45 | camelCaseOption 46 | `, 47 | }); 48 | 49 | test('spawn cli and test input', verifyCli, { 50 | args: '-u cat', 51 | expected: stripIndentTrim` 52 | unicorn 53 | meow 54 | camelCaseOption 55 | `, 56 | }); 57 | 58 | test('spawn cli and test input flag', verifyCli, { 59 | args: '--camel-case-option bar', 60 | expected: 'bar', 61 | }); 62 | 63 | test('disable autoVersion/autoHelp if `cli.input.length > 0`', t => { 64 | t.is(meow({importMeta, argv: ['bar', '--version']}).input.at(0), 'bar'); 65 | t.is(meow({importMeta, argv: ['bar', '--help']}).input.at(0), 'bar'); 66 | t.is(meow({importMeta, argv: ['bar', '--version', '--help']}).input.at(0), 'bar'); 67 | }); 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "source/index.d.ts", 4 | "test-d", 5 | ], 6 | "compilerOptions": { 7 | "strict": true, 8 | "jsx": "react", 9 | "target": "ES2021", // Node.js 16 10 | "lib": [ 11 | "ES2021" 12 | ], 13 | "module": "ES2020", 14 | "moduleResolution": "node", 15 | "noImplicitAny": true, 16 | "strictNullChecks": true, 17 | "noUnusedLocals": true 18 | } 19 | } 20 | --------------------------------------------------------------------------------