Closed46

deno用のちょっとしたargparseの代替

podhmopodhmo

実装に関する事柄

podhmopodhmo

とりあえず型パズルは置いておいてヘルプメッセージの作成とかをやっていくか

podhmopodhmo

aliasの対応要らなくない?
booleanとか配列だけを受け取るようにすれば良くない?

podhmopodhmo

冷静に型の挙動を整理すると

‐booleansのundefinedは存在しない
‐stringsはdefault,requiredが設定されていたときにundefinedは存在しない
‐stringsはcollectが指定されたとき配列になる
‐aliasは対応したくない

そして
‐booleansのdefaultがなければ埋める(未設定を用意しなくて良いでしょ)
‐negatableのときdefaultをtrueにする
‐unknownも設定がなかったら埋める

podhmopodhmo

ヘルプメッセージは上書きで良いんじゃない?

podhmopodhmo

とりあえずは完成した。
とりあえずjsrに挙げて使えるようにしたい。

podhmopodhmo

あと型チェックのテストどうしようかな?と思ったりもしてたのだけど、これは単にdeno checkの結果を吐き出させてゴールデンデータと確認するだけで良さそう

(エラーメッセージまで確認せずファイル名と行番号だけあれば十分そう)

podhmopodhmo

型パズルの知識

不足している知識のメモ

podhmopodhmo

interfaceとかtypeでgenericsを使うとそれをわざわざ渡す必要が出てきてしまう。

interface Opt<K extends string> {
    boolean: K[]
    negatable?: K[]
}
const opt: Opt<"X" | "Y"> = {
    boolean: ["X"],
    negatable: ["X", "Y"]
}
  • Optにわざわざunion typesを渡さないとだめ
  • booleanに含まれない値をnegatableに渡せてしまう

🤔たぶん、string[]をunion typesに変換するような機能が必要そう

podhmopodhmo

一応、as constでそれ限定のものにはなるらしい。これにtype[number]とかを指定して上げれば取れるかもしれない。

// [deno-ts] Type '["x", "y"]' is not assignable to type 'never'.
const xs: never = (["x", "y"] as const)

こんなユーティリティ型を作る事はできる。

// Utility type to convert an array of strings to a union of string literals
type ArrayToUnion<T extends readonly string[]> = T[number];

こんな感じで使えるんだけれど、as constとかreadonlyが必須なんだろうか?

// Utility type to convert an array of strings to a union of string literals
type ArrayToUnion<T extends readonly string[]> = T[number];

// Example usage
const stringArray = ["a", "b", "c"] as const;

// Infer the type of the array elements as a union of string literals
type StringUnion = ArrayToUnion<typeof stringArray>;

// StringUnion is now "a" | "b" | "c"
const example: StringUnion = "a"; // This is valid
// const exampleInvalid: StringUnion = "d"; // This would cause a TypeScript error
podhmopodhmo

今まではkeyofでunion typeに変換していたのだけれどこれを配列にする必要がある。こういう問題が書けると良い。

const options = {
    boolean: ["x", "y"]
    negatable: ["x"]
};
  • booleanに渡された値を型の範囲にする
  • negatableはbooleanに渡された値以外利用できない
podhmopodhmo

問題 (prompt)

typescriptを書いている。以下のような型Optionsを定義したい

  • boolean, negatableの2つのフィールドを持つ。これらは文字列の配列型のサブセット
  • booleanに渡された値のみをnegatableに渡すことができる
{boolean: ["x", "y"], negatable: ["x"]} // ok
{boolean: ["x"], negatable: ["x","y"]} // fail
{boolean: ["x"], negatable: ["z"]} // fail

それができたら、このOptionsの型に渡す型パラメーターを値から導出する関数newを定義したい。

new({boolean: ["x", "y"], negatable: ["x]}) // OK

回答

こんな感じ?

type Options<B extends string[], N extends string[]> = {
    boolean: B;
    negatable: N extends B[number][] ? N : never; // NがBのサブセットでなければnever
};

function createOptions<B extends string[], N extends B[number][]>(
    options: Options<B, N>
): Options<B, N> {
    return options;
}

// OK: NはBのサブセット
// [deno-ts] Type 'Options<string[], "x"[]>' is not assignable to type 'never'.
const opt1: never = createOptions({ boolean: ["x", "y"], negatable: ["x"] });

// エラー: negatableに"y"が含まれているがbooleanには含まれていない
// [deno-ts] Type 'Options<string[], ("x" | "y")[]>' is not assignable to type 'never'.
const opt2: never = createOptions({ boolean: ["x"], negatable: ["x", "y"] });

// エラー: negatableに"z"が含まれているがbooleanには含まれていない
// [deno-ts] Type 'Options<string[], "z"[]>' is not assignable to type 'never'.
const opt3: never = createOptions({ boolean: ["x"], negatable: ["z"] });
podhmopodhmo

↑で受け取った値をkeyに展開できないとだめなのか。

podhmopodhmo

うーん、as constが必要になるのか。。

function F<K extends readonly string[]>(xs: K): { [P in K[number]]: boolean } {
    const ob: Record<string, boolean> = {}
    xs.forEach((x) => ob[x] = true)
    return ob as { [P in K[number]]: boolean }
}


// [deno-ts] Type '{ x: boolean; y: boolean; z: boolean; }' is not assignable to type 'never'.
const ob: never = F(["x", "y", "z"] as const)
podhmopodhmo

こういう感じにstringの配列ではなく直接絞られた型の配列を持つようにするとうまくいく

function F<T extends string>(xs: { boolean: T[] }): { [P in T]: boolean } {
    const ob: Record<string, boolean> = {}
    xs.boolean.forEach((x) => ob[x] = true)
    return ob as { [P in T]: boolean }
}


// [deno-ts] Type '{ x: boolean; y: boolean; z: boolean; }' is not assignable to type 'never'.
const ob: never = F({ boolean: ["x", "y", "z"] })

最初の方にやれていたのは何がだめだったんだろ?

podhmopodhmo

こんな感じで両方持てる?

function F<B extends string, S extends string>(ob: { boolean: B[], string: S[] }): { [P in B]: boolean } & { [P in S]: string } {
    const r: Record<string, boolean | string> = {}
    ob.boolean.forEach((x) => r[x] = true)
    ob.string.forEach((x) => r[x] = "")
    return r as { [P in B]: boolean } & { [P in S]: string }
}


// [deno-ts] Type '{ x: boolean; y: boolean; z: boolean; } & { i: string; j: string; k: string; }' is not assignable to type 'never'.
const ob: never = F({ boolean: ["x", "y", "z"], string: ["i", "j", "k"] })
podhmopodhmo

collectの対応とかも欲しくなった。

function F<B extends string, S extends string, C extends S>(ob: { boolean: B[], negatable?: B[], string: S[], collect: C[] }): { [P in B]: boolean } & { [P in Exclude<S, C>]: string } & { [P in C]: string[] } {
    const r: Record<string, boolean | string | string[]> = {};

    ob.boolean.forEach((x) => r[x] = !(ob.negatable || []).includes(x));
    ob.string.forEach((x) => {
        if (ob.collect.includes(x as C)) {
            r[x] = [];
        } else {
            r[x] = "";
        }
    });

    return r as { [P in B]: boolean } & { [P in Exclude<S, C>]: string } & { [P in C]: string[] };
}

// [deno-ts] Type '{ x: boolean; y: boolean; z: boolean; } & { i: string; k: string; } & { j: string[]; }' is not assignable to type 'never'.
const ob: never = F({ boolean: ["x", "y", "z"], negatable: ["x"], string: ["i", "j", "k"], collect: ["j"] });

console.log(ob); // { x: true, y: true, z: true, i: "", j: [], k: "" }
podhmopodhmo

これはstring周りが怪しい

type RequiredKeys<T extends string[], U extends string[]> = Exclude<T[number], U[number]>;

function F<B extends string, S extends string, C extends S, R extends (B | S)>(ob: { boolean: B[], negatable?: B[], string: S[], collect: C[], required: R[] }): { [P in Extract<B, R>]: boolean } & { [P in Extract<B, RequiredKeys<B[], R[]>>]?: boolean } & { [P in Extract<S, Exclude<S, C>>]: string } & { [P in Extract<S, Exclude<S, C>>]?: string } & { [P in Extract<C, R>]: string[] } & { [P in Extract<C, RequiredKeys<C[], R[]>>]?: string[] } {
    const r: Record<string, boolean | string | string[] | undefined> = {};

    ob.boolean.forEach((x) => r[x] = !(ob.negatable || []).includes(x));
    ob.string.forEach((x) => {
        if (ob.collect.includes(x as C)) {
            r[x] = [];
        } else {
            r[x] = "";
        }
    });

    return r as { [P in Extract<B, R>]: boolean } & { [P in Extract<B, RequiredKeys<B[], R[]>>]?: boolean } & { [P in Extract<S, Exclude<S, C>>]: string } & { [P in Extract<S, Exclude<S, C>>]?: string } & { [P in Extract<C, R>]: string[] } & { [P in Extract<C, RequiredKeys<C[], R[]>>]?: string[] };
}

// Example usage
// [deno-ts] Type '{ x: boolean; } & { y?: boolean | undefined; z?: boolean | undefined; } & { i: string; k: string; } & { i?: string | undefined; k?: string | undefined; } & { j: string[]; } & {}' is not assignable to type 'never'.
const ob: never = F({ boolean: ["x", "y", "z"], negatable: ["x"], string: ["i", "j", "k"], collect: ["j"], required: ["x", "i", "j"] });
console.log(ob); // { x: true, y: undefined, z: undefined, i: "", j: [], k: undefined }

podhmopodhmo

倒せない型パズル

オリジナルのparseArgs()を呼ぶ部分は仕方がないとしてTDefaultsにbooleanを渡す部分の型エラーを倒せないのだった。

podhmopodhmo

まだまだいろいろあった

  • ok 型のテストを書く
  • 型のおかしなところを直す
    • ok collectのオプションが無いときになぜかstring[]になる
    • ok なぜか"help"とかしか渡せなくなる
    • ok なぜかdefaultsでエラーが出る -> booleansがundefinedだからか。
    • ok stringがundefinedのときとstringが[]のときで、collectのあたいのチェックが異なる
    • ok booleanがundefinedのときのチェックが働かない
    • ok どうせならstring,booleanがundefinedだった場合にはneverが良い

基本的にundefinedのときにneverではなくstringにになったりするのが悪かった

podhmopodhmo

最終的にdefaultを諦めてrequiredとcollectだけを見るようにした

podhmopodhmo

jsrに

まだ挙げてない

https://jsr.io/docs/publishing-packages

podhmopodhmo

モジュールのコメントはmod.tsに書くべきかも?

あ取るコードの例をparse-argsにするかは悩む

podhmopodhmo

あと壊れてないことを保証しきれてないかも?

parseArgsの方はテストがない
(型チェックのテストはどうやるんだろ?)

podhmopodhmo

motivationというかusecaseというか

podhmopodhmo

ヘルプメッセージの生成付きのCLIパーサーが欲しくなるタイミングは、少数の作品を愛でるときではなくたくさんの作品未満を嫌々触るときに必要になる。

作りが{丁寧,雑}なコマンドを{使いたい,使いたくない}の四象限で考えたときにの雑なものを嫌々使うとき(一ヶ月前の自分は他人)

そうなると1文字フラグに否定的な気持ちになるのも説明がつく。

podhmopodhmo

他のひとはどうしているか?

たとえば、fresh等を覗いてみる
直接ヘルプメッセージの文字列を記述している。

https://github.com/denoland/fresh/blob/96e520a352009661f4d17cc8c4a4cf4142739d48/init/src/init.ts#L37

個人的な気質

💭自分はインデントとかスペースの調節が嫌いなのかも。フリーハンドが苦手。

あとはドキュメントを読まずにとりあえずコマンドを実行してみたい
コピペでコマンドを入力せずシェルから手打ちで入力したいとかの気質も関係してそう

ちなみに細々とした調整がしたくなったら直接文字列の方が良いし、変に利用方法を覚えるより楽。

podhmopodhmo

ヘルプメッセージ生成の悩み

問題は実行してみないと組み立てられるヘルプメッセージが分からなくなっていくほど複雑になっていった場合にどこかに結果のログを貼りたくなってしまう所

まぁなのでこのパッケージで対応できるのはオプションと精々数個程度のものそう。

podhmopodhmo

what is command line options parser?

なんとなくCLIパーサーで型の変換までを含めるとやりすぎだと思うのはいわゆるJSONを作るまでを責務にするのがちょうど良いと思うからなのかも。

そこからの変換器は得意なものが色々あるでしょみたいな感じ。
そうなるとnumberくらいは欲しいかも…?

ただ、それこそconfigファイルを触っているときにはほとんど文字列と真偽値程度なきもするし、意外と数値関係はコード側に定数として持つことが多い気がする(paginationのときのlimit,offsetみたいなものはnumberのほうが嬉しい。findのときのmmin,mtimeとかもそうか)。

ネストした構造が必要になるか?というとそうでもないような気がしていてそれこそeslintなどでもflat configの対応がどうこうとか言われていた。

podhmopodhmo

環境変数の読み込み

環境変数が読み込めると嬉しい。

環境変数とフラグが衝突したときは環境変数が勝つということで良いか?たぶんそちらのほうが使いやすい。

リテラルタイプで取得できるのは引数として渡してるオブジェクトの中だけだから嬉しい筈。

podhmopodhmo

例えばこんな感じで使う。

import { parseArgs } from "../src/parse-args.ts";

const args = parseArgs(Deno.args, {
    string: ["name"],
    required: ["name"],
    envvar: {
        name: "OVERRIDE_NAME",
    },
} as const)
podhmopodhmo

⚠️collect optionがついていても環境変数だと1つしか渡せない
あとbooleanのものはnegatableでも無視して環境変数で設定する

このスクラップは3日前にクローズされました