├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.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 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [6.x, 8.x, 10.x, 12.x] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm install 18 | - run: npm test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | - '6' 6 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {Opts as MinimistOptions} from 'minimist'; 2 | 3 | export type OptionType = 'string' | 'boolean' | 'number' | 'array' | 'string-array' | 'boolean-array' | 'number-array'; 4 | 5 | export interface BaseOption< 6 | TypeOptionType extends OptionType, 7 | DefaultOptionType 8 | > { 9 | /** 10 | * The data type the option should be parsed to. 11 | */ 12 | readonly type?: TypeOptionType; 13 | 14 | /** 15 | * An alias/list of aliases for the option. 16 | */ 17 | readonly alias?: string | ReadonlyArray; 18 | 19 | /** 20 | * The default value for the option. 21 | */ 22 | readonly default?: DefaultOptionType; 23 | } 24 | 25 | export type StringOption = BaseOption<'string', string>; 26 | export type BooleanOption = BaseOption<'boolean', boolean>; 27 | export type NumberOption = BaseOption<'number', number>; 28 | export type DefaultArrayOption = BaseOption<'array', ReadonlyArray>; 29 | export type StringArrayOption = BaseOption<'string-array', ReadonlyArray>; 30 | export type BooleanArrayOption = BaseOption<'boolean-array', ReadonlyArray>; 31 | export type NumberArrayOption = BaseOption<'number-array', ReadonlyArray>; 32 | 33 | type MinimistOption = NonNullable< 34 | | MinimistOptions['stopEarly'] 35 | | MinimistOptions['unknown'] 36 | | MinimistOptions['--'] 37 | >; 38 | 39 | export type Options = { 40 | [key: string]: 41 | | OptionType 42 | | StringOption 43 | | BooleanOption 44 | | NumberOption 45 | | DefaultArrayOption 46 | | StringArrayOption 47 | | BooleanArrayOption 48 | | NumberArrayOption 49 | | MinimistOption; // Workaround for https://github.com/microsoft/TypeScript/issues/17867 50 | }; 51 | 52 | /** 53 | * Write options for [minimist](https://npmjs.org/package/minimist) in a comfortable way. Support string, boolean, number and array options. 54 | */ 55 | export default function buildOptions(options?: Options): MinimistOptions; 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isPlainObject = require('is-plain-obj'); 4 | const arrify = require('arrify'); 5 | const kindOf = require('kind-of'); 6 | 7 | const push = (obj, prop, value) => { 8 | if (!obj[prop]) { 9 | obj[prop] = []; 10 | } 11 | 12 | obj[prop].push(value); 13 | }; 14 | 15 | const insert = (obj, prop, key, value) => { 16 | if (!obj[prop]) { 17 | obj[prop] = {}; 18 | } 19 | 20 | obj[prop][key] = value; 21 | }; 22 | 23 | const prettyPrint = output => { 24 | return Array.isArray(output) ? 25 | `[${output.map(prettyPrint).join(', ')}]` : 26 | kindOf(output) === 'string' ? JSON.stringify(output) : output; 27 | }; 28 | 29 | const resolveType = value => { 30 | if (Array.isArray(value) && value.length > 0) { 31 | const [element] = value; 32 | return `${kindOf(element)}-array`; 33 | } 34 | 35 | return kindOf(value); 36 | }; 37 | 38 | const normalizeExpectedType = (type, defaultValue) => { 39 | const inferredType = type === 'array' ? 'string-array' : type; 40 | 41 | if (arrayTypes.includes(inferredType) && Array.isArray(defaultValue) && defaultValue.length === 0) { 42 | return 'array'; 43 | } 44 | 45 | return inferredType; 46 | }; 47 | 48 | const passthroughOptions = ['stopEarly', 'unknown', '--']; 49 | const primitiveTypes = ['string', 'boolean', 'number']; 50 | const arrayTypes = primitiveTypes.map(t => `${t}-array`); 51 | const availableTypes = [...primitiveTypes, 'array', ...arrayTypes]; 52 | 53 | const buildOptions = options => { 54 | options = options || {}; 55 | 56 | const result = {}; 57 | 58 | passthroughOptions.forEach(key => { 59 | if (options[key]) { 60 | result[key] = options[key]; 61 | } 62 | }); 63 | 64 | Object.keys(options).forEach(key => { 65 | let value = options[key]; 66 | 67 | if (key === 'arguments') { 68 | key = '_'; 69 | } 70 | 71 | // If short form is used 72 | // convert it to long form 73 | // e.g. { 'name': 'string' } 74 | if (typeof value === 'string') { 75 | value = {type: value}; 76 | } 77 | 78 | if (isPlainObject(value)) { 79 | const props = value; 80 | const {type} = props; 81 | 82 | if (type) { 83 | if (!availableTypes.includes(type)) { 84 | throw new TypeError(`Expected type of "${key}" to be one of ${prettyPrint(availableTypes)}, got ${prettyPrint(type)}`); 85 | } 86 | 87 | if (arrayTypes.includes(type)) { 88 | const [elementType] = type.split('-'); 89 | push(result, 'array', {key, [elementType]: true}); 90 | } else { 91 | push(result, type, key); 92 | } 93 | } 94 | 95 | if ({}.hasOwnProperty.call(props, 'default')) { 96 | const {default: defaultValue} = props; 97 | const defaultType = resolveType(defaultValue); 98 | const expectedType = normalizeExpectedType(type, defaultValue); 99 | 100 | if (expectedType && expectedType !== defaultType) { 101 | throw new TypeError(`Expected "${key}" default value to be of type "${expectedType}", got ${prettyPrint(defaultType)}`); 102 | } 103 | 104 | insert(result, 'default', key, defaultValue); 105 | } 106 | 107 | arrify(props.alias).forEach(alias => { 108 | insert(result, 'alias', alias, key); 109 | }); 110 | } 111 | }); 112 | 113 | return result; 114 | }; 115 | 116 | module.exports = buildOptions; 117 | module.exports.default = buildOptions; 118 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import * as minimist from 'minimist'; 2 | import buildOptions from '.'; 3 | 4 | buildOptions({name: 'string'}); 5 | buildOptions({force: 'boolean'}); 6 | buildOptions({score: 'number'}); 7 | buildOptions({array: 'array'}); 8 | buildOptions({array: 'string-array'}); 9 | buildOptions({array: 'boolean-array'}); 10 | buildOptions({array: 'number-array'}); 11 | buildOptions({ 12 | name: { 13 | type: 'string', 14 | alias: 'n', 15 | default: 'john' 16 | } 17 | }); 18 | buildOptions({ 19 | force: { 20 | type: 'boolean', 21 | alias: ['f', 'o'], 22 | default: false 23 | } 24 | }); 25 | buildOptions({ 26 | score: { 27 | type: 'number', 28 | alias: 's', 29 | default: 0 30 | } 31 | }); 32 | buildOptions({ 33 | arr: { 34 | type: 'array', 35 | alias: 'a', 36 | default: [] 37 | } 38 | }); 39 | buildOptions({ 40 | strings: { 41 | type: 'string-array', 42 | alias: 's', 43 | default: ['a', 'b'] 44 | } 45 | }); 46 | buildOptions({ 47 | booleans: { 48 | type: 'boolean-array', 49 | alias: 'b', 50 | default: [true, false] 51 | } 52 | }); 53 | buildOptions({ 54 | numbers: { 55 | type: 'number-array', 56 | alias: 'n', 57 | default: [0, 1] 58 | } 59 | }); 60 | 61 | const options = buildOptions({ 62 | name: { 63 | type: 'string', 64 | alias: 'n', 65 | default: 'john' 66 | }, 67 | 68 | force: { 69 | type: 'boolean', 70 | alias: ['f', 'o'], 71 | default: false 72 | }, 73 | 74 | score: { 75 | type: 'number', 76 | alias: 's', 77 | default: 0 78 | }, 79 | 80 | array: { 81 | type: 'array', 82 | alias: 'a', 83 | default: [] 84 | }, 85 | 86 | strings: { 87 | type: 'string-array', 88 | alias: 's', 89 | default: ['a', 'b'] 90 | }, 91 | 92 | booleans: { 93 | type: 'boolean-array', 94 | alias: 'b', 95 | default: [true, false] 96 | }, 97 | 98 | numbers: { 99 | type: 'number-array', 100 | alias: 'n', 101 | default: [0, 1] 102 | }, 103 | 104 | published: 'boolean', 105 | 106 | arguments: 'string', 107 | 108 | stopEarly: true, 109 | 110 | unknown: (arg: string) => arg.startsWith('-') 111 | }); 112 | 113 | minimist(['--option', 'value', 'input'], options); 114 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Vadim Demedes (vadimdemedes.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimist-options", 3 | "version": "4.1.0", 4 | "description": "Pretty options for minimist", 5 | "repository": "vadimdemedes/minimist-options", 6 | "author": "Vadim Demedes ", 7 | "license": "MIT", 8 | "keywords": [ 9 | "minimist", 10 | "argv", 11 | "args" 12 | ], 13 | "scripts": { 14 | "test": "xo && ava && tsd-check" 15 | }, 16 | "engines": { 17 | "node": ">= 6" 18 | }, 19 | "files": [ 20 | "index.js", 21 | "index.d.ts" 22 | ], 23 | "dependencies": { 24 | "arrify": "^1.0.1", 25 | "is-plain-obj": "^1.1.0", 26 | "kind-of": "^6.0.3" 27 | }, 28 | "devDependencies": { 29 | "@types/minimist": "^1.2.0", 30 | "ava": "^1.0.1", 31 | "tsd-check": "^0.3.0", 32 | "xo": "^0.24.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # minimist-options ![test](https://github.com/vadimdemedes/minimist-options/workflows/test/badge.svg) 2 | 3 | > Write options for [minimist](https://npmjs.org/package/minimist) and [yargs](https://npmjs.org/package/yargs) in a comfortable way. 4 | > Supports string, boolean, number and array options. 5 | 6 | ## Installation 7 | 8 | ``` 9 | $ npm install --save minimist-options 10 | ``` 11 | 12 | ## Usage 13 | 14 | ```js 15 | const buildOptions = require('minimist-options'); 16 | const minimist = require('minimist'); 17 | 18 | const options = buildOptions({ 19 | name: { 20 | type: 'string', 21 | alias: 'n', 22 | default: 'john' 23 | }, 24 | 25 | force: { 26 | type: 'boolean', 27 | alias: ['f', 'o'], 28 | default: false 29 | }, 30 | 31 | score: { 32 | type: 'number', 33 | alias: 's', 34 | default: 0 35 | }, 36 | 37 | arr: { 38 | type: 'array', 39 | alias: 'a', 40 | default: [] 41 | }, 42 | 43 | strings: { 44 | type: 'string-array', 45 | alias: 's', 46 | default: ['a', 'b'] 47 | }, 48 | 49 | booleans: { 50 | type: 'boolean-array', 51 | alias: 'b', 52 | default: [true, false] 53 | }, 54 | 55 | numbers: { 56 | type: 'number-array', 57 | alias: 'n', 58 | default: [0, 1] 59 | }, 60 | 61 | published: 'boolean', 62 | 63 | // Special option for positional arguments (`_` in minimist) 64 | arguments: 'string' 65 | }); 66 | 67 | const args = minimist(process.argv.slice(2), options); 68 | ``` 69 | 70 | instead of: 71 | 72 | ```js 73 | const minimist = require('minimist'); 74 | 75 | const options = { 76 | string: ['name', '_'], 77 | number: ['score'], 78 | array: [ 79 | 'arr', 80 | {key: 'strings', string: true}, 81 | {key: 'booleans', boolean: true}, 82 | {key: 'numbers', number: true} 83 | ], 84 | boolean: ['force', 'published'], 85 | alias: { 86 | n: 'name', 87 | f: 'force', 88 | s: 'score', 89 | a: 'arr' 90 | }, 91 | default: { 92 | name: 'john', 93 | f: false, 94 | score: 0, 95 | arr: [] 96 | } 97 | }; 98 | 99 | const args = minimist(process.argv.slice(2), options); 100 | ``` 101 | 102 | ## Array options 103 | 104 | The `array` types are only supported by [yargs](https://npmjs.org/package/yargs). 105 | 106 | [minimist](https://npmjs.org/package/minimist) does _not_ explicitly support array type options. If you set an option multiple times, it will indeed yield an array of values. However, if you only set it once, it will simply give the value as is, without wrapping it in an array. Thus, effectively ignoring `{type: 'array'}`. 107 | 108 | `{type: 'array'}` is shorthand for `{type: 'string-array'}`. To have values coerced to `boolean` or `number`, use `boolean-array` or `number-array`, respectively. 109 | 110 | ## License 111 | 112 | MIT © [Vadim Demedes](https://vadimdemedes.com) 113 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import minimistOptions from '.'; 3 | 4 | const validate = (t, input, expected) => { 5 | t.deepEqual(minimistOptions(input), expected); 6 | }; 7 | 8 | test('empty input', validate, {}, {}); 9 | 10 | test('string option', validate, { 11 | name: 'string' 12 | }, { 13 | string: ['name'] 14 | }); 15 | 16 | test('boolean option', validate, { 17 | force: 'boolean' 18 | }, { 19 | boolean: ['force'] 20 | }); 21 | 22 | test('number option', validate, { 23 | score: 'number' 24 | }, { 25 | number: ['score'] 26 | }); 27 | 28 | test('default array option', validate, { 29 | arr: 'array' 30 | }, { 31 | array: ['arr'] 32 | }); 33 | 34 | test('string array option', validate, { 35 | arr: 'string-array' 36 | }, { 37 | array: [{key: 'arr', string: true}] 38 | }); 39 | 40 | test('number array option', validate, { 41 | arr: 'number-array' 42 | }, { 43 | array: [{key: 'arr', number: true}] 44 | }); 45 | 46 | test('boolean array option', validate, { 47 | arr: 'boolean-array' 48 | }, { 49 | array: [{key: 'arr', boolean: true}] 50 | }); 51 | 52 | test('multiple array options', validate, { 53 | xs: 'number-array', 54 | ys: 'boolean-array' 55 | }, { 56 | array: [{key: 'xs', number: true}, {key: 'ys', boolean: true}] 57 | }); 58 | 59 | test('string array default value is not string array fails', t => { 60 | const error = t.throws(() => { 61 | minimistOptions({ 62 | arr: { 63 | type: 'string-array', 64 | default: [2] 65 | } 66 | }); 67 | }, TypeError); 68 | 69 | t.is(error.message, 'Expected "arr" default value to be of type "string-array", got "number-array"'); 70 | }); 71 | 72 | test('boolean array default value is not boolean array fails', t => { 73 | const error = t.throws(() => { 74 | minimistOptions({ 75 | arr: { 76 | type: 'boolean-array', 77 | default: ['score'] 78 | } 79 | }); 80 | }, TypeError); 81 | 82 | t.is(error.message, 'Expected "arr" default value to be of type "boolean-array", got "string-array"'); 83 | }); 84 | 85 | test('number array default value is not number array fails', t => { 86 | const error = t.throws(() => { 87 | minimistOptions({ 88 | arr: { 89 | type: 'number-array', 90 | default: [true] 91 | } 92 | }); 93 | }, TypeError); 94 | 95 | t.is(error.message, 'Expected "arr" default value to be of type "number-array", got "boolean-array"'); 96 | }); 97 | 98 | test('alias', validate, { 99 | score: { 100 | alias: 's' 101 | } 102 | }, { 103 | alias: { 104 | s: 'score' 105 | } 106 | }); 107 | 108 | test('alias array', validate, { 109 | score: { 110 | alias: ['s', 'sc'] 111 | } 112 | }, { 113 | alias: { 114 | s: 'score', 115 | sc: 'score' 116 | } 117 | }); 118 | 119 | test('alias and string', validate, { 120 | name: { 121 | type: 'string', 122 | alias: 'n' 123 | } 124 | }, { 125 | string: ['name'], 126 | alias: { 127 | n: 'name' 128 | } 129 | }); 130 | 131 | test('alias and boolean', validate, { 132 | force: { 133 | type: 'boolean', 134 | alias: 'f' 135 | } 136 | }, { 137 | boolean: ['force'], 138 | alias: { 139 | f: 'force' 140 | } 141 | }); 142 | 143 | test('alias and number', validate, { 144 | score: { 145 | type: 'number', 146 | alias: 's' 147 | } 148 | }, { 149 | number: ['score'], 150 | alias: { 151 | s: 'score' 152 | } 153 | }); 154 | 155 | test('alias and array', validate, { 156 | arr: { 157 | type: 'array', 158 | alias: 'a' 159 | } 160 | }, { 161 | array: ['arr'], 162 | alias: { 163 | a: 'arr' 164 | } 165 | }); 166 | 167 | test('default value', validate, { 168 | score: { 169 | default: 10 170 | } 171 | }, { 172 | default: { 173 | score: 10 174 | } 175 | }); 176 | 177 | test('default falsy value', validate, { 178 | falsePrimitive: { 179 | default: false 180 | }, 181 | zero: { 182 | default: 0 183 | }, 184 | empty: { 185 | default: '' 186 | }, 187 | nan: { 188 | default: NaN 189 | }, 190 | nullPrimitive: { 191 | default: null 192 | }, 193 | undefinedPrimitive: { 194 | default: undefined 195 | } 196 | }, { 197 | default: { 198 | falsePrimitive: false, 199 | zero: 0, 200 | empty: '', 201 | nan: NaN, 202 | nullPrimitive: null, 203 | undefinedPrimitive: undefined 204 | } 205 | }); 206 | 207 | test('default array value', validate, { 208 | arr: { 209 | type: 'array', 210 | default: ['a'] 211 | } 212 | }, { 213 | array: ['arr'], 214 | default: { 215 | arr: ['a'] 216 | } 217 | }); 218 | 219 | test('default empty array value', validate, { 220 | arr: { 221 | type: 'array', 222 | default: [] 223 | }, 224 | strings: { 225 | type: 'string-array', 226 | default: [] 227 | }, 228 | booleans: { 229 | type: 'boolean-array', 230 | default: [] 231 | }, 232 | numbers: { 233 | type: 'number-array', 234 | default: [] 235 | } 236 | }, { 237 | array: [ 238 | 'arr', 239 | {key: 'strings', string: true}, 240 | {key: 'booleans', boolean: true}, 241 | {key: 'numbers', number: true} 242 | ], 243 | default: { 244 | arr: [], 245 | strings: [], 246 | booleans: [], 247 | numbers: [] 248 | } 249 | }); 250 | 251 | test('arguments type', validate, { 252 | arguments: 'string' 253 | }, { 254 | string: ['_'] 255 | }); 256 | 257 | test('passthrough options', validate, { 258 | '--': true, 259 | stopEarly: true, 260 | unknown: true 261 | }, { 262 | '--': true, 263 | stopEarly: true, 264 | unknown: true 265 | }); 266 | 267 | test('fail if type is not boolean, string, number or array', t => { 268 | const error = t.throws(() => { 269 | minimistOptions({ 270 | force: { 271 | type: 'bool' 272 | } 273 | }); 274 | }, TypeError); 275 | 276 | t.is(error.message, 'Expected type of "force" to be one of ["string", "boolean", "number", "array", "string-array", "boolean-array", "number-array"], got "bool"'); 277 | }); 278 | 279 | test('fail if boolean default value is not a boolean', t => { 280 | const error = t.throws(() => { 281 | minimistOptions({ 282 | force: { 283 | type: 'boolean', 284 | default: 'true' 285 | } 286 | }); 287 | }, TypeError); 288 | 289 | t.is(error.message, 'Expected "force" default value to be of type "boolean", got "string"'); 290 | }); 291 | 292 | test('fail if number default value is not a number', t => { 293 | const error = t.throws(() => { 294 | minimistOptions({ 295 | score: { 296 | type: 'number', 297 | default: '1' 298 | } 299 | }); 300 | }, TypeError); 301 | 302 | t.is(error.message, 'Expected "score" default value to be of type "number", got "string"'); 303 | }); 304 | 305 | test('fail if string default value is not a string', t => { 306 | const error = t.throws(() => { 307 | minimistOptions({ 308 | score: { 309 | type: 'string', 310 | default: 1 311 | } 312 | }); 313 | }, TypeError); 314 | 315 | t.is(error.message, 'Expected "score" default value to be of type "string", got "number"'); 316 | }); 317 | 318 | test('fail if array default value is not an array', t => { 319 | const error = t.throws(() => { 320 | minimistOptions({ 321 | score: { 322 | type: 'array', 323 | default: '' 324 | } 325 | }); 326 | }, TypeError); 327 | 328 | t.is(error.message, 'Expected "score" default value to be of type "string-array", got "string"'); 329 | }); 330 | 331 | test('fail if array default value element type is not string', t => { 332 | const error = t.throws(() => { 333 | minimistOptions({ 334 | score: { 335 | type: 'array', 336 | default: [1] 337 | } 338 | }); 339 | }, TypeError); 340 | 341 | t.is(error.message, 'Expected "score" default value to be of type "string-array", got "number-array"'); 342 | }); 343 | --------------------------------------------------------------------------------