fishのargparseで未定義オプションを透過させるラッパー関数の作成
モチベーション
最近は Deno がお気に入りで、特に deno run
コマンドを使えば TypeScript をすぐさま実行できて、リンターもデフォルトで色々やってくれるので、TypeScript の学習環境としてとても良いと感じています。
ただ、Deno CLI の deno run
コマンドを実行する際に、TypeScript や JavaScript のデバッグとして console.log
の出力をリダイレクションを使って適当なファイルに出力すると、ANSI escape code という制御コードが一緒に出力されて結果がまともに見られなくなる場合があります。
この ANSI escape code を取り除く処理をラッパー関数内に噛ませることで、きれいに出力できるようにしたい思います。
というわけで、Deno CLI のコマンドである deno run
のラッパーとなる fish function を作成しながら、argparse
ビルトインコマンドの --igonre-unknown
オプションによってラップ元にそのコマンド自体のオプションを渡す方法について解説していきます。
やりたいこと
やりたいこととしてはシンプルに次の三点です。
- (1) ラップ元の
deno run
のオプションを使用できるようにする - (2) ラップ元の
deno run
コマンドの補完(completion)の引き継ぎ - (3)
deno run
で出力される結果から ANSI escape code を取り除くような処理をかませる
(1)については alias
ビルトインコマンドを使えば早いのですが、(2)ラップ元の補完を継承させて、(3)の処理をオプションで噛ませたいので、通常の function
と argparse
ビルトインコマンドを使用してラッパーを作成します(fisher
や git
を使ってプラグイン的に開発した方が管理もしやすいです)。
基本構造
通常のプラグイン作成と同じようにつくります(fish-plugin-template
を使用してプロジェクト内に次の3つのディレクトリとテンプレートを展開しても OK)。
fish-plugin-template
については以前の記事で紹介したので参照してください。
関数の名前は deno-run-out
ということにしておきます。あとは、deno run
で走らせるテスト用の TypeScript ファイルを tests
ディレクトリに作成しておきます。
├── completions
│ └── deno-run-out.fish
├── conf.d
│ └── deno-run-out.fish
├── functions
│ └── deno-run-out.fish
└── tests
├── console_test.ts
└── read_write_test.ts
deno-run-out.fish
は次のような感じで、このテンプレートを改造していきます。
function deno-run-out -d 'DISCRIPTION'
argparse \
-x 'v,h' \
'v/version' 'h/help' -- $argv
or return 1
set --local version_deno_run_out 'v0.0.1'
if set -q _flag_version
echo "deno-run-out: " $version_deno_run_out
else if set -q _flag_help
__deno-run-out_help
else
# main body
end
end
function __deno-run-out_help
echo 'USAGE:'
echo ' deno-run-out [OPTION]'
echo 'OPTIONS:'
echo ' -v, --version Show version info'
echo ' -h, --help Show help'
end
名前については alias
を使って短縮したものを適当に conf.d
に定義しておきます。今回は derun
という短縮名で使用できるようにします。
alias derun="deno-run-out"
read_write_test.ts
は次のように --allow-read
と --allow-write
パーミッションが必要なように適当なコードを書いておきます。
import * as fs from "https://deno.land/std@0.126.0/fs/mod.ts";
// docs: https://deno.land/std@0.126.0/fs
const dir_name = "created_dir";
const file_name = "created_file.md"
await fs.ensureDir(dir_name);
// ディレクトリが存在することを保証する、存在しなければ作成する(mkdir -p と同等)
console.log(`directory ${dir_name} is created by ensureDir()!`);
await fs.ensureFile(`${dir_name}/${file_name}`);
// ファイルが存在することを保証する、存在しなければ作成する
console.log(`file ${file_name} is created by ensureFile()!`)
console_test.ts
にはリダイレクションによって ANSI escape code が入る出力用のテストコードを書いておきます。
const array= [1, 2, 3, 4, 5];
const new_item = array.push(6, 7, 8);
console.log({ array });
console.log({ new_item });
const removed_item = array.pop();
console.log({ removed_item });
console.log({ array });
const unshifted_length = array.unshift(-1, 0);
console.log({ unshifted_length });
console.log({ array });
const shifted_item = array.shift();
console.log({ shifted_item });
console.log({ array });
これを実際にリダイレクションしてみます。
❯ deno run tests/console_test.ts > tests/console_test.log
[33m
のような制御コードが紛れ込んでしまっています。
{ array: [
[33m1[39m, [33m2[39m, [33m3[39m, [33m4[39m,
[33m5[39m, [33m6[39m, [33m7[39m, [33m8[39m
] }
{ new_item: [33m8[39m }
{ removed_item: [33m8[39m }
{ array: [
[33m1[39m, [33m2[39m, [33m3[39m, [33m4[39m,
[33m5[39m, [33m6[39m, [33m7[39m
] }
{ unshifted_length: [33m9[39m }
{ array: [
[33m-1[39m, [33m0[39m, [33m1[39m, [33m2[39m, [33m3[39m,
[33m4[39m, [33m5[39m, [33m6[39m, [33m7[39m
] }
{ shifted_item: [33m-1[39m }
{ array: [
[33m0[39m, [33m1[39m, [33m2[39m, [33m3[39m,
[33m4[39m, [33m5[39m, [33m6[39m, [33m7[39m
] }
ラップ元の補完の引き継ぎ
補完の引き継ぎは、completions
ディレクトリにある関数名と同一名のファイル deno-run-out.fish
というファイルに以下を書けば終了です。これだけで、deno run
の補完が引き継がれます。
# complete -c 新規コマンド -w 継承元のコマンド
# deno run の補完の設定を deno-run-out に引き継ぐ
complete -c deno-run-out -w "deno run"
これによって、例えば derun --
とコマンドラインに入力すると deno run
のオプション補完が表示されるようになります。
あとは、ラッパーコマンド自体のオプションの補完を追加しておきます。
complete -c deno-run-out -s v -l version -f -d "Show version info"
complete -c deno-run-out -s h -l help -f -d "Show help"
complete -c deno-run-out -s s -l stdout -f -d "Strip ANSI escape code for stdout"
補完スクリプトのつくり方については以下の記事が網羅的かつ分かりやすくまとまっています。
ラップ元のオプションを使用できるようにする
次のように alias
を定義するだけなら、当たり前ですがラップ元のオプションをそのまま使用できます。
$ alias deno-run="deno run"
$ deno-run --allow-read --allow-write tests/read_write_test.ts
directory created_dir is created by ensureDir()!
file created_file.md is created by ensureFile()!
また、argparse
を使わなければエラーも無いので簡単にラップ元にオプションを渡せますが、今回はラッパー自体に独自オプションを定義したいので argparse
ビルトインコマンドとそのオプションである --ignore-unknown
を使用して関数作成します(argparse
を使えば色々楽ですしね)。
--ignore-unknown
は未定義オプションを無視して $argv
にそのままフラグを残すというオプションです。これによって、ラップ元のオプションについて argparse
がエラーを吐かずに関数内部へ通過させることができます。
逆に --ignore-unknown
を使わない場合、例えば以下のように argparse
を使って引数のパースをするような関数 bad-pattern
を定義したとします。
function bad-pattern
argparse \
-x 'v,h' \
'v/version' 'h/help' -- $argv
or return 1
set --local version_bad_pattern "v0.0.1"
echo "argv:" $argv
if set -q _flag_version
echo "bad-pattern: " $version_bad_pattern
else if set -q _flag_help
echo "help message"
else
command deno run $argv
end
end
この場合、argparse
のオプション処理対象として定義している -v, --version
と -h, --help
についてはうまく処理しますが、未定義のオプション(例えば deno run
の --allow-read
オプションなど)を入力したときにエラーを吐き出します。
❯ bad-pattern test.ts -v
argv: test.ts
bad-pattern: v0.0.1
❯ bad-pattern --allow-read --allow-write read_write_test.ts
bad-pattern: Unknown option '--allow-read'
--allow-read
のように argparse
で処理を定義していないオプションをそのまま deno run
コマンドにわたすために --ignore-unknown
オプションを使用してあげます。
function good-pattern
argparse --ignore-unknown \
-x 'v,h' \
'v/version' 'h/help' -- $argv
or return 1
set --local version_good_pattern "v0.0.1"
echo "argv:" $argv
if set -q _flag_version
echo "good-pattern: " $version_good_pattern
else if set -q _flag_help
echo "help message"
else
command deno run $argv
end
end
これで、未定義のオプションを argparse
に無視(通過)させて deno run
にわたすことができるようになりました。内部的には定義されてある -v, --version
と -h, --help
フラグについてはそれが引数として渡されるとフラグ変数 _flag_version
と _flag_hel
などをローカルスコープで生成しますが、未定義オプション(--allow-read
など)は定義済みオプション以外のすべての引数を含む $argv
に保存されます。
# 定義済みオプション -v は $argv に格納されず、内部変数 _flag_version に保存される
❯ good-pattern -v
argv:
good-pattern: v0.0.1
# 未定義オプション --allow-read は $argv にそのまま格納される
❯ good-pattern -v --allow-read
argv: --allow-read
good-pattern: v0.0.1
❯ good-pattern --allow-read --allow-write read_write_test.ts
argv: --allow-read --allow-write read_write_test.ts
directory created_dir is created by ensureDir()!
file created_file.md is created by ensureFile()!
ANSI escape codeを取り除く
自分としては、これが本来的にやりたかったことなんですが、TypeScript のデバッグに console.log
の出力をリダイレクションを使って適当なファイルに出力すると、ANSI escape code という制御コードが一緒に出力されてしまって見づらかったので、これを取り除く処理をラッパー内に噛ませます。関数内部で利用する外部コマンドは GNU 実装の sed
で macOS の場合は brew install gnu-sed
でインストールすると gsed
としてインストールされます。
gsed
と正規表現 s/\x1b\[[0-9;]*m//g
を組みわせると ANSI escape code が取り除けます。次のようにパイプでフィルターします。
command deno run $argv | command gsed 's/\x1b\[[0-9;]*m//g'
正規表現の参考(&解説):
これによってリダイレクションしたときに制御コードが紛れずに済みます。これをラッパーのオプションとして -s, --stdout
というフラグを併用して実行することで使用できるようにします。
あとは uname -s
と test
で OS 判定させて Linux と macOS において sed と gsed を切り替えられるようにしておきます。
# -s, --stdout オプションフラグが渡されたときの処理
if set -q _flag_stdout
set --local sed_version
# OS 判定して変数 sed_version に gsed or sed を格納
if test (uname -s) = "Darwin"
set sed_version "gsed"
# gsed がインストールされているかを確認
if not type --query gsed
echo "Plase install gnu-sed"
return 1
end
else
set sed_version "sed"
end
# 実行時に $sed_version は sed or gsed に展開される
command deno run $argv | command $sed_version 's/\x1b\[[0-9;]*m//g'
else
command deno run $argv
end
全体は次のようになります。
functions/deno-run-out.fish
function deno-run-out -d "deno run wrapper"
# ignore unknown option flags to pass them to deno run command (use -i option in argparse)
argparse --ignore-unknown \
-x 'v,h,s' \
'v/version' 'h/help' 's/stdout' -- $argv
or return 1
set --local version_deno_run_out 'v0.1.1'
if set -q _flag_version
echo "deno-run-out: " $version_deno_run_out
else if set -q _flag_help
__deno-run-out_help
else if not test (count $argv) -eq 0
if set -q _flag_stdout
set --local sed_version
if test (uname -s) = "Darwin"
set sed_version "gsed"
if not type --query gsed
echo "Plase install gnu-sed"
return 1
end
else
set sed_version "sed"
end
command deno run $argv | command $sed_version 's/\x1b\[[0-9;]*m//g'
else
command deno run $argv
end
else
echo "Pass a file"
return 1
end
end
# helper function
function __deno-run-out_help
printf '%s\n' \
'ALIAS:' \
' derun' \
'USAGE:' \
' deno-run-out [-v|-h]' \
' deno-run-out [-s] [deno-run-OPTIONS...] TARGETFILE' \
'OPTIONS:' \
' -v, --version Show version info' \
' -h, --help Show help' \
' -s, --stdout Strip ANSI escape code for stdout'
end
これで、リダイレクションしてもうまくログを残せるようになりました。(たぶん、もっといい方法があるというか console
のメソッドそのものの使い方で制御コードが入らないようにできる気がします)
プロジェクトフォルダのトップレベルで fishser
を使ってローカルインストールしたらコマンドとして使用できるようになります。
$ fisher install $PWD
実際に -s
オプションを使ってリダイレクションしてみます。
❯ derun -s ./tests/console_test.ts > ./tests/new_console_test.log
中身を見ると、ANSI escape code が取り除かれています。
{ array: [
1, 2, 3, 4,
5, 6, 7, 8
] }
{ new_item: 8 }
{ removed_item: 8 }
{ array: [
1, 2, 3, 4,
5, 6, 7
] }
{ unshifted_length: 9 }
{ array: [
-1, 0, 1, 2, 3,
4, 5, 6, 7
] }
{ shifted_item: -1 }
{ array: [
0, 1, 2, 3,
4, 5, 6, 7
] }
もちろん、--allow-read --allow-write
パーミッションフラグとも併用できます。
❯ derun -s --allow-read --allow-write ./tests/read_write_test.ts > ./tests/read_write_test.log
あとは、同じ方法で色々微調整したりします。
余談ですが、deno doc --builtin
で API のドキュメントを出力して grep
などをするときに正規表現でうまく検索できず、この gsed
のパターンを組みわせるとちゃんと検索できました。そのようなケースや、結果をリダイレクションをしたい場合などには gsed
を活用してみてください。
❯ deno doc --builtin | gsed 's/\x1b\[[0-9;]*m//g' | grep -e "^\s*interface"
# interface が行頭にある行だけ出力
gsed 's/\x1b\[[0-9;]*m//g'
自体になにかエイリアスを定義して使うとかが楽そうですね。
追記
別の解決方法が見つかったのでそちらについて記事を書きました。
Discussion