[READ-ONLY] Mirror of https://github.com/bombshell-dev/args. <1kB CLI flag parser
args args-parser cli command-line command-line-parser node
5

Configure Feed

Select the types of activity you want to include in your feed.

Optimize performance for v0.1.0

Nate Moore (Feb 6, 2023, 8:10 AM -0600) 07f23c74 581f12fb

+108 -80
+5
.changeset/violet-taxis-try.md
··· 1 + --- 2 + "ultraflag": minor 3 + --- 4 + 5 + Optimize performance by using a single loop for parsing
+5 -4
README.md
··· 1 1 # `ultraflag` 2 2 3 - A <1kB library for parsing CLI flags. Inspired by Deno's `std` [`flags`](https://github.com/denoland/deno_std/blob/main/flags/mod.ts) module. 3 + A 730B library for parsing CLI flags. Inspired by Deno's `std` [`flags`](https://github.com/denoland/deno_std/blob/main/flags/mod.ts) module. 4 4 5 5 ### Features 6 6 ··· 37 37 ## Benchmarks 38 38 39 39 ``` 40 - ultraflag x 801,993 ops/sec ±0.40% (95 runs sampled) 41 - minimist x 318,623 ops/sec ±0.49% (95 runs sampled) 42 - yargs-parser x 23,560 ops/sec ±3.77% (91 runs sampled) 40 + mri x 1,285,159 ops/sec ±0.29% (90 runs sampled) 41 + ultraflag x 986,699 ops/sec ±0.38% (91 runs sampled) 42 + minimist x 250,866 ops/sec ±0.59% (92 runs sampled) 43 + yargs-parser x 18,153 ops/sec ±4.30% (85 runs sampled) 43 44 ```
+1
package.json
··· 54 54 "globby": "^13.1.2", 55 55 "gzip-size": "^7.0.0", 56 56 "minimist": "^1.2.7", 57 + "mri": "^1.2.0", 57 58 "npm-run-all": "^4.1.5", 58 59 "prettier": "^2.5.1", 59 60 "pretty-bytes": "^6.0.0",
+7
pnpm-lock.yaml
··· 9 9 globby: ^13.1.2 10 10 gzip-size: ^7.0.0 11 11 minimist: ^1.2.7 12 + mri: ^1.2.0 12 13 npm-run-all: ^4.1.5 13 14 prettier: ^2.5.1 14 15 pretty-bytes: ^6.0.0 ··· 25 26 globby: 13.1.2 26 27 gzip-size: 7.0.0 27 28 minimist: 1.2.7 29 + mri: 1.2.0 28 30 npm-run-all: 4.1.5 29 31 prettier: 2.7.1 30 32 pretty-bytes: 6.0.0 ··· 1476 1478 /mixme/0.5.4: 1477 1479 resolution: {integrity: sha512-3KYa4m4Vlqx98GPdOHghxSdNtTvcP8E0kkaJ5Dlh+h2DRzF7zpuVVcA8B0QpKd11YJeP9QQ7ASkKzOeu195Wzw==} 1478 1480 engines: {node: '>= 8.0.0'} 1481 + dev: true 1482 + 1483 + /mri/1.2.0: 1484 + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 1485 + engines: {node: '>=4'} 1479 1486 dev: true 1480 1487 1481 1488 /ms/2.1.2:
+10 -19
scripts/bench.js
··· 2 2 import { parse as ultraflag } from "../dist/index.js"; 3 3 import minimist from 'minimist'; 4 4 import yargs from 'yargs-parser'; 5 + import mri from 'mri'; 5 6 6 - // @ts-ignore 7 - const suite = new benchmark.Suite(); 7 + const bench = new benchmark.Suite(); 8 + const args = ['--a=1', '-b', '--bool', '--no-boop', '--multi=foo', '--multi=baz', '-xyz']; 8 9 9 - const args = `--a=1 --b=2 -c 3 -xyz -c 4`.split(' '); 10 - 11 - suite 12 - .add("ultraflag", () => { 13 - ultraflag(args); 14 - }) 15 - .add("minimist", () => { 16 - minimist(args); 17 - }) 18 - .add("yargs-parser", () => { 19 - yargs(args) 20 - }) 21 - .on("cycle", (event) => { 22 - console.log(String(event.target)); 23 - }); 24 - 25 - suite.run(); 10 + bench 11 + .add('ultraflag ', () => ultraflag(args)) 12 + .add('mri ', () => mri(args)) 13 + .add('minimist ', () => minimist(args)) 14 + .add('yargs-parser ', () => yargs(args)) 15 + .on('cycle', e => console.log(String(e.target))) 16 + .run();
+80 -57
src/index.ts
··· 6 6 BooleanType, 7 7 StringType, 8 8 Collectable, 9 - 10 9 Aliases, 11 10 } from "./types.js"; 12 11 export { ParseOptions, Args } from "./types.js"; 13 12 14 - const FLAG_RE = /(?:--?([^\s=]+))(?:\s+|=|$)("[^"]+"|'[^']+'|[^-\s]+)?|\S+/gm; 15 13 const BOOL_RE = /^(true|false)$/; 16 - const NUMBER_RE = /^(\.?\d)/; 17 14 const QUOTED_RE = /^('|").*\1$/; 18 - const NEGATED_RE = /^no-/; 19 - const SINGLE_RE = /^-[^-]/; 20 15 21 16 const set = (obj: NestedMapping, key: string, value: any, type?: string) => { 22 17 if (key.includes(".")) { ··· 29 24 } 30 25 key = parts[parts.length - 1]; 31 26 } 32 - if (type === 'array' && obj[key] !== undefined) { 27 + if (type === "array" && obj[key] !== undefined) { 33 28 if (Array.isArray(obj[key])) { 34 29 (obj[key] as any[]).push(value); 35 30 } else { 36 31 obj[key] = [obj[key], value]; 37 32 } 38 33 } else { 39 - obj[key] = type === 'array' ? [value] : value; 34 + obj[key] = type === "array" ? [value] : value; 40 35 } 41 36 }; 42 37 43 - const type = (key: string, opts: Record<'boolean' | 'string' | 'array', string[]>): 'boolean' | 'string' | 'array' | undefined => { 44 - for (const [t, keys] of Object.entries(opts)) { 45 - if (keys.includes(key)) return t as keyof typeof opts; 46 - } 38 + const type = ( 39 + key: string, 40 + opts: Record<"boolean" | "string" | "array", string[]> 41 + ): "boolean" | "string" | "array" | undefined => { 42 + if (opts.array && opts.array.length > 0 && opts.array.includes(key)) 43 + return "array"; 44 + if (opts.string && opts.string.length > 0 && opts.string.includes(key)) 45 + return "string"; 46 + if (opts.boolean && opts.boolean.length > 0 && opts.boolean.includes(key)) 47 + return "boolean"; 47 48 return; 48 - } 49 + }; 49 50 50 - const defaultValue = (type?: 'boolean' | 'string' | 'array') => { 51 - if (type === 'string') return ''; 52 - if (type === 'array') return []; 51 + const defaultValue = (type?: "boolean" | "string" | "array") => { 52 + if (type === "string") return ""; 53 + if (type === "array") return []; 53 54 return true; 54 - } 55 + }; 55 56 56 - const coerce = (value: string, type?: 'string' | 'boolean' | "array") => { 57 - if (type === 'string') return value; 58 - if (type === 'boolean') return !!value; 57 + const coerce = (value?: string, type?: "string" | "boolean" | "array") => { 58 + if (type === "string") return value; 59 + if (type === "boolean") return !!value; 59 60 60 61 if (!value) return value; 61 - if (BOOL_RE.test(value)) return value === "true"; 62 - if (NUMBER_RE.test(value)) return Number(value); 63 - if (QUOTED_RE.test(value)) return value.slice(1, -1); 62 + if (value.length > 3 && BOOL_RE.test(value)) return value === "true"; 63 + if (value.length > 2 && QUOTED_RE.test(value)) return value.slice(1, -1); 64 + if (value[0] === '.' && /\d/.test(value[1]) || /\d/.test(value[0])) return Number(value); 64 65 return value; 65 66 }; 66 67 ··· 82 83 TAliasNames extends string = string 83 84 >( 84 85 argv: string[], 85 - { default: defaults, alias: aliases = {}, ...types }: ParseOptions< 86 - TBooleans, 87 - TStrings, 88 - TCollectable, 89 - TDefaults, 90 - TAliases 91 - > = {} 86 + { 87 + default: defaults, 88 + alias: aliases, 89 + ...types 90 + }: ParseOptions<TBooleans, TStrings, TCollectable, TDefaults, TAliases> = {} 92 91 ): Args<TArgs> { 93 92 if (argv.length === 0) return {} as Args<TArgs>; 94 - const str = argv.join(' '); 93 + const obj = { ...defaults, _: [] } as unknown as Args<TArgs>; 94 + 95 + const args = []; 96 + for (let i = 0; i < argv.length; i++) { 97 + const curr = argv[i]; 98 + const next = argv[i + 1]; 95 99 96 - FLAG_RE.lastIndex = 0; 97 - let m; 98 - const obj = { ...defaults, _: [] } as unknown as Args<TArgs>; 99 - while ((m = FLAG_RE.exec(str))) { 100 - let [value, key, arg] = m; 101 - let isAliased = false; 102 - if (!key && !arg) { 103 - (obj as any)._.push(coerce(value)); 104 - continue; 105 - } 106 - if (aliases.hasOwnProperty(key)) { 107 - key = aliases[key as keyof typeof aliases] as string; 108 - isAliased = true; 109 - } 110 - const t = type(key, types as any); 100 + let t: 'string' | 'boolean' | 'array' | undefined; 101 + let key = ''; 102 + let value: string | undefined; 111 103 112 - if (!isAliased && SINGLE_RE.test(value)) { 113 - // Special case! `-a.a1` should be treated as { a: '.a1' } 114 - if (key.includes(".")) { 115 - set(obj, key.split(".")[0], "." + key.split(".").slice(1).join(".")); 116 - FLAG_RE.lastIndex -= arg?.length ?? 0; 104 + if (curr.length > 1 && curr[0] === "-") { 105 + if (curr[1] !== "-" && curr.length > 2 && !curr.includes('=')) { 106 + if (curr.includes('.')) { 107 + key = curr.slice(1, 2); 108 + value = curr.slice(2); 109 + } else { 110 + const keys = curr.slice(1, -1); 111 + for (let key of keys) { 112 + if (aliases && (aliases as Record<string, any>)[key] !== undefined) { 113 + key = aliases[key as keyof typeof aliases] as string; 114 + } 115 + set(obj, key, defaultValue(t), t) 116 + } 117 + key = curr.slice(-1) 118 + if (next && next[0] !== '-') { 119 + value = next; 120 + i++; 121 + } 122 + } 123 + } else if (!curr.includes("=") && next && next[0] !== "-") { 124 + key = curr.replace(/^-{1,2}/, ''); 125 + value = next; 126 + t = type(key, types as any); 127 + i++; 117 128 } else { 118 - for (const k of key.slice(0, -1)) { 119 - set(obj, k, true); 129 + const eq = curr.indexOf('='); 130 + if (eq === -1) { 131 + key = curr.replace(/^-{1,2}/, ''); 132 + } else { 133 + key = curr.slice(0, eq).replace(/^-{1,2}/, ''); 134 + value = curr.slice(eq + 1); 120 135 } 121 - set(obj, key[key.length - 1], coerce(arg, t) ?? true); 136 + t = type(key, types as any); 122 137 } 123 - } else if ((!t || t === 'boolean') && NEGATED_RE.test(key)) { 124 - set(obj, key.slice(3), false); 125 - } else { 126 - set(obj, key, coerce(arg, t) ?? defaultValue(t), t); 138 + 139 + if ((!t || t === "boolean") && key.length > 3 && key.startsWith('no-')) { 140 + set(obj, key.slice(3), false) 141 + } else { 142 + if (aliases && (aliases as Record<string, any>)[key] !== undefined) { 143 + key = aliases[key as keyof typeof aliases] as string; 144 + } 145 + set(obj, key, coerce(value, t) ?? defaultValue(t), t) 146 + } 147 + } else if (curr) { 148 + (obj as any)._.push(coerce(curr)); 149 + continue; 127 150 } 128 151 } 129 152