🦕

Deno製CLIツールでオプションの任意引数を判定する

2021/11/22に公開

本稿はかなりピンポイントな悩みに対処したもので、すぐに使えるものではないと思われます。
「こんな解説があったな」程度に覚えておいていただけると、なにかの機会に見直して役立つことがあるかもしれません。
というか自分でもわからなくなると思うので、未来の自分が見返せるように書きました。

オプションの任意引数には不確定性がある

以下のコマンドをご覧ください。

deno run --allow-read cat.ts dog.ts

deno runコマンドにオプションと引数が渡されています。
--allow-readは単体で渡されるとすべてのリソースの読み込みを許可しますが、読み込み対象を限定するため、引数としてパスを渡すこともできます。

すると、上記のコマンドは次の2通りの解釈ができる気がします。

  1. cat.ts--allow-readの引数、dog.tsdeno runの引数
  2. cat.tsdeno runの引数、dog.tscat.tsの引数

ここでの正解は後者です。manualのcatの例もそうなっています。

https://deno.land/manual@v1.16.2/examples/unix_cat

この見分け方はdeno run --helpで確認できます。

https://twitter.com/KawarimiDoll/status/1462629912804790273

オプションと引数の間に=が入っていないのが必須引数、入っているのが任意引数です。

引数指定が必須のオプションは、直後の値は確実に引数です。したがって、=の有無で解釈は変わりません。

# same
deno run --config deno.json main.ts
deno run --config=deno.json main.ts

しかし引数指定が任意の場合は、直後の値がオプションの引数なのかdeno runの引数なのか判定できません。
したがって、=を明示する必要があります。

# NOT same
deno run --allow-read cat.ts dog.ts
deno run --allow-read=cat.ts dog.ts

Denoでオプションの任意引数を判定する

ここからは、引数がオプションの引数なのかスクリプトの引数なのかをDenoスクリプト内で見分ける方法について説明します。

目標は、以下のような結果を返すことです。
※ キー_はスクリプト自体の引数

❯ deno run parse.ts --allow-read cat.ts dog.ts
{ _: [ "cat.ts", "dog.ts" ], "allow-read": true }

❯ deno run parse.ts --allow-read=cat.ts dog.ts
{ _: [ "dog.ts" ], "allow-read": [ "cat.ts" ] }

std/flagsの処理の確認

DenoのCLIの引数は、標準ライブラリのflagsで処理できますが、ここで提供されている機能だけでは所望の結果を得ることができません。

https://deno.land/std@0.115.1/flags

このライブラリのparseを使って実験してみます。

parse1.ts
import { parse } from "https://deno.land/std@0.115.1/flags/mod.ts";

console.log(parse(Deno.args));

以下のように、=の有無で結果は変わりません。

❯ deno run parse1.ts --allow-read cat.ts dog.ts
{ _: [ "dog.ts" ], "allow-read": "cat.ts" }

❯ deno run parse1.ts --allow-read=cat.ts dog.ts
{ _: [ "dog.ts" ], "allow-read": "cat.ts" }

stringオプションをつけても上記と同様の結果になります。

booleanオプションを使ってみるとどうでしょうか。

parse2.ts
import { parse } from "https://deno.land/std@0.115.1/flags/mod.ts";

console.log(parse(Deno.args, { boolean: ["allow-read"] }));

この場合、=をつけても引数が無視されてしまいます。

❯ deno run parse2.ts --allow-read cat.ts dog.ts
{ _: [ "cat.ts", "dog.ts" ], "allow-read": true }

❯ deno run parse2.ts --allow-read=cat.ts dog.ts
{ _: [ "dog.ts" ], "allow-read": true }

対処:自作関数で--を挿入

以上の不確定性の問題に対し、なるべくstd/flagsparseを使って対処しようと考え、「適切なポイントに--を挿入する」というのを思いつきました。
--を検出すると、その後の値はすべて_に含める仕様となっているためです。

先程のparse1.tsをそのまま使うと、このようになります。

❯ deno run parse1.ts --allow-read -- cat.ts dog.ts
{ _: [ "cat.ts", "dog.ts" ], "allow-read": true }
  • --allow-read=cat.tsのように=付きで指定されていたら引数とみなし変更しない
  • --allow-read cat.tsのように=がない場合は区切りとみなし間に--を挿入する

これを実装したのが以下のensureOptsArgsです。

parse.ts
import { parse } from "https://deno.land/std@0.115.1/flags/mod.ts";

function ensureOptsArgs(args: string[], needEquals: string[]) {
  needEquals.forEach((option) => {
    if (!/^--[a-z]/.test(option)) {
      throw new Error(
        "Item in needEquals must be start with '--', but got " + option,
      );
    }
  });

  for (let i = 0; i < args.length - 1; i++) {
    if (!needEquals.includes(args[i])) {
      continue;
    }
    if (args[i + 1].startsWith("-")) {
      continue;
    }

    return [...args.slice(0, i + 1), "--", ...args.slice(i + 1)];
  }

  return args;
}

const NEED_EQUALS = [
  "--allow-env",
  "--allow-ffi",
  "--allow-net",
  "--allow-read",
  "--allow-run",
  "--allow-write",
];

console.log(parse(ensureOptsArgs(Deno.args, NEED_EQUALS)));

ensureOptsArgsはパースする引数の配列と=の必要なオプションの配列を受け取ります。
内部では引数の配列についてループし、「与えられた=の必要なオプションに該当」「直後が別のオプションではない」ものを見つけたら、そこに--を挟んで返却します。
needEqualsの配列は、ユースケースにより調整が必要です。

これで目標としていた挙動を作り出すことができました。

❯ deno run parse.ts --allow-read cat.ts dog.ts
{ _: [ "cat.ts", "dog.ts" ], "allow-read": true }

❯ deno run parse.ts --allow-read=cat.ts dog.ts
{ _: [ "dog.ts" ], "allow-read": [ "cat.ts" ] }

❯ deno run run.ts --allow-read --allow-write cat.ts dog.ts
{ _: [ "cat.ts", "dog.ts" ], "allow-read": true, "allow-write": true }

❯ deno run run.ts --allow-read --allow-write=cat.ts dog.ts 
{ _: [ "dog.ts" ], "allow-read": true, "allow-write": "cat.ts" }

❯ deno run run.ts --allow-read cat.ts --allow-write dog.ts    
{ _: [ "cat.ts", "--allow-write", "dog.ts" ], "allow-read": true }

おわりに

DenoのCLI引数の扱いに関する考察でした。
オプションの任意引数を使いたい場合は役に立つかもしれません。

Discussion