minimist は JavaScript でCLI引数をパースするときに便利なパッケージです
CLIオプションはlongオプションの場合 --foo-bar
のように指定しますが、interfaceのpropertyで使用する際には fooBar
とcamelCaseになっていたほうが気持ちがいいです。
それをするためのラッパー処理を書きました
import minimist from "minimist";
type Kebab<T extends string, A extends string = ""> = T extends `${infer F}${infer R}`
? Kebab<R, `${A}${F extends Lowercase<F> ? "" : "-"}${Lowercase<F>}`>
: A;
type KebabKeys<T> = Partial<{ [K in keyof T as K extends string ? Kebab<K> : K]: T[K] }>;
type Alias<T> = Partial<{ [K in keyof T as K extends string ? Kebab<K> : K]: string | string[] }>;
type IsRequired<T> = Partial<{ [K in keyof T as K extends string ? Kebab<K> : K]: boolean }>;
const toCamel = (str: string) => str.replace(/([-_][a-z])/gi, ($1) => $1.toUpperCase().replace("-", "").replace("_", ""));
interface Opts<T> {
alias?: Alias<T>;
default?: KebabKeys<T>;
required?: IsRequired<T>;
}
/**
* kebab-caseで指定されたCLIオプションをcamelCaseのフィールドに変換する
* e.g. `--base-path /work` は `basePath: "/work"` になる
*/
export function parseArgs<T extends minimist.ParsedArgs>(args: string[], opts?: Opts<T>): T {
const al: { [key: string]: string | string[] } = {};
if (opts?.alias != null) {
for (const [k, v] of Object.entries(opts.alias)) {
al[k] = v;
}
}
const argv = minimist<T>(args, {
alias: al,
default: opts?.default,
}) as T;
if (opts?.required != null) {
for (const k of Object.keys(opts.required) as (keyof KebabKeys<T>)[]) {
const isRequired = opts.required[k];
if (isRequired && argv[k] == null) {
throw new Error(`required parameter [${String(k)}] is missing`);
}
}
}
let t: any = {};
Object.keys(argv).forEach((k) => {
t[toCamel(k)] = argv[k];
});
return t;
}
使い方
interface Options {
_: string[];
workingDir: string;
foo: string;
no: number;
}
const args = parseArgs<Options>(process.argv.slice(2), {
alias: {
foo: ["f"],
"working-dir": "w",
},
default: {
foo: "bar",
no: 1,
},
required: {
"working-dir": true,
},
});
console.log(JSON.stringify(args));