Skip to content

Commit 07f23c7

Browse files
committed
Optimize performance for v0.1.0
1 parent 581f12f commit 07f23c7

File tree

6 files changed

+108
-80
lines changed

6 files changed

+108
-80
lines changed

.changeset/violet-taxis-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ultraflag": minor
3+
---
4+
5+
Optimize performance by using a single loop for parsing

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# `ultraflag`
22

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.
44

55
### Features
66

@@ -37,7 +37,8 @@ const args = parse(argv, {
3737
## Benchmarks
3838

3939
```
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)
4344
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"globby": "^13.1.2",
5555
"gzip-size": "^7.0.0",
5656
"minimist": "^1.2.7",
57+
"mri": "^1.2.0",
5758
"npm-run-all": "^4.1.5",
5859
"prettier": "^2.5.1",
5960
"pretty-bytes": "^6.0.0",

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/bench.js

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,15 @@ import benchmark from "benchmark";
22
import { parse as ultraflag } from "../dist/index.js";
33
import minimist from 'minimist';
44
import yargs from 'yargs-parser';
5+
import mri from 'mri';
56

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'];
89

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();

src/index.ts

Lines changed: 80 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,12 @@ import type {
66
BooleanType,
77
StringType,
88
Collectable,
9-
109
Aliases,
1110
} from "./types.js";
1211
export { ParseOptions, Args } from "./types.js";
1312

14-
const FLAG_RE = /(?:--?([^\s=]+))(?:\s+|=|$)("[^"]+"|'[^']+'|[^-\s]+)?|\S+/gm;
1513
const BOOL_RE = /^(true|false)$/;
16-
const NUMBER_RE = /^(\.?\d)/;
1714
const QUOTED_RE = /^('|").*\1$/;
18-
const NEGATED_RE = /^no-/;
19-
const SINGLE_RE = /^-[^-]/;
2015

2116
const set = (obj: NestedMapping, key: string, value: any, type?: string) => {
2217
if (key.includes(".")) {
@@ -29,38 +24,44 @@ const set = (obj: NestedMapping, key: string, value: any, type?: string) => {
2924
}
3025
key = parts[parts.length - 1];
3126
}
32-
if (type === 'array' && obj[key] !== undefined) {
27+
if (type === "array" && obj[key] !== undefined) {
3328
if (Array.isArray(obj[key])) {
3429
(obj[key] as any[]).push(value);
3530
} else {
3631
obj[key] = [obj[key], value];
3732
}
3833
} else {
39-
obj[key] = type === 'array' ? [value] : value;
34+
obj[key] = type === "array" ? [value] : value;
4035
}
4136
};
4237

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";
4748
return;
48-
}
49+
};
4950

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 [];
5354
return true;
54-
}
55+
};
5556

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;
5960

6061
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);
6465
return value;
6566
};
6667

@@ -82,48 +83,70 @@ export function parse<
8283
TAliasNames extends string = string
8384
>(
8485
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> = {}
9291
): Args<TArgs> {
9392
if (argv.length === 0) return {} as Args<TArgs>;
94-
const str = argv.join(' ');
95-
96-
FLAG_RE.lastIndex = 0;
97-
let m;
9893
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);
11194

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;
95+
const args = [];
96+
for (let i = 0; i < argv.length; i++) {
97+
const curr = argv[i];
98+
const next = argv[i + 1];
99+
100+
let t: 'string' | 'boolean' | 'array' | undefined;
101+
let key = '';
102+
let value: string | undefined;
103+
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++;
117128
} 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);
120135
}
121-
set(obj, key[key.length - 1], coerce(arg, t) ?? true);
136+
t = type(key, types as any);
122137
}
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;
127150
}
128151
}
129152

0 commit comments

Comments
 (0)