optex であらゆる UNIX コマンドのオプションをカスタマイズする


# Getopt::EX

greple コマンドからオプションハンドリングの部分を切り出して、Getopt::EX という Perl モジュールとしてリリースした。

- [greple](https://github.com/kaz-utashiro/greple)
- [Getopt::EX](https://github.com/kaz-utashiro/Getopt-EX)

これは、コマンドの実行前にオプションを処理して、新しいオプションや機能を実現するためのものだ。greple を本格的に使おうとすると、オプションの指定がかなり複雑になり、端末から入力するのは現実的ではない。シェルスクリプトにするという手はあるが、管理がしにくい。Getopt::EX を使うと、rc ファイルとモジュールという形でまとめることができる。

# optex コマンド

optex は、この Getopt::EX の機能をすべてのコマンドで使えるようにしようというものだ。`getoptex` だと長いので `optex` にした。読み方には特にこだわっていないが、何となく自分では「オプテック」と呼んでいる。

シェルのエイリアス機能でも近いことはできるが、コマンドそのものを拡張したように見えるので、シェルスクリプトや他のコマンドから間接的に実行される場合にも有効になる点が決定的に違う。

使い方は、optex の引数としてコマンドを指定する方法と：

    $ optex date

シンボリックリンクを使う方法がある：

    $ ls -l ~/.optex.d/bin/date
    ~/.optex.d/bin/date -> ~/perl5/bin/optex*
    $ date

リンクの置き場所はどこでも構わないが、デフォルトで `~/.optex.d/bin` というディレクトリを想定して、リンクを管理する機能も用意してある。`--ln` オプションでリンクを作成し、`--rm` オプションで削除することができる。

    $ optex --ln date
    $ optex --ls -l date
    date -> ~/perl5/bin/optex
    $ optex --rm date
    ~/.optex.d/bin/date removed.
    $ optex --ls -l date
    date: not exist

設定ファイルは ~/.optex.d/ というディレクトリに置く。`date` コマンドであれば `~/.optex.d/date.rc` だ。

設定ファイルの中では、次のようなことができる。

## option

例えば macOS の `date` コマンドには `-I[TIMESPEC]` オプションが実装されていないが、次のような設定を `~/.optex.d/date.rc` ファイルに入れておくことで実現できる。

```:~/.optex.d/date.rc
option -I        -Idate
option -Idate    +%F
option -Iseconds +%FT%T%z
option -Iminutes +%FT%H:%M%z
option -Ihours   +%FT%H%z

option --iso-8601         -I
option --iso-8601=date    -Idate
option --iso-8601=seconds -Iseconds
option --iso-8601=minutes -Iminutes
option --iso-8601=hours   -Ihours
```

こうすると、次のように実行できる。

    % optex date -Iseconds

シンボリックリンクがあれば、普通のコマンドと同じように実行できる。

    % date -Iseconds

## define

複雑なオプションを定義するためにマクロを使うこともできる。ファイル `~/.optex.d/awk.rc` に次の内容を入れておくと、awk で --vowels というオプションが使えるようになる。

```:~/.optex.d/awk.rc
define __delete__ /[bcdfgkmnpsrtvwyz]e( |$)/
define __match__  /ey|y[aeiou]*|[aeiou]+/
define __count_vowels__ <<EOS
{
    s = tolower($0);
    gsub(__delete__, " ", s);
    for (count=0; match(s, __match__); count++) {
        s=substr(s, RSTART + RLENGTH);
    }
    print count " " $0;
}
EOS
option --vowels __count_vowels__
```

これは、英単語の音節を調べようとしているもので、こんな風に使う。

    % awk --vowels /usr/share/dict/words

これは、この記事で使ったスクリプトだ。
https://qiita.com/kaz-utashiro/items/9027eba0c1347d441bd9

## expand

複雑なオプションを定義するためには expand を使う。expand は option と同じように使えるが、そのファイルの中でだけ有効になる。find コマンドのオプションは指定するのが面倒なので、こんな風にまとめておけると便利だ。

    expand repository   ( -name .git -o -name .svn -o -name RCS )
    expand no_dots      ! -name .*
    expand no_version   ! -name *,v
    expand no_backup    ! -name *~
    expand no_image     ! -iname *.jpg  ! -iname *.jpeg \
                        ! -iname *.gif  ! -iname *.png
    expand no_archive   ! -iname *.tar  ! -iname *.tbz  ! -iname *.tgz
    expand no_pdf       ! -iname *.pdf

    option --clean \
            repository -prune -o \
            -type f \
            no_dots \
            no_version no_backup \
            no_image \
            no_archive \
            no_pdf

    % find . --clean -print

# モジュール

**optex** はモジュールで拡張することができる。`date` コマンドの場合、モジュールは `~/.optex.d/date/` からロードする。ここに `~/.optex.d/date/default.pm` というモジュールがあると自動的に実行される。

モジュールファイルには rc ファイルと同じ内容も書くことができて、この場合別に Perl のコードを書く必要はない。先の awk の例なら `~/.optex.d/awk/vowels.pm` というファイルに以下の内容を入れておくと `awk -Mvowels` で `--vowels` オプションが有効になる。最初の3行のおまじないは必要だが、その後は rc ファイルと同じだ。

```:~/.optex.d/awk/vowels.pm
package vowels;
1;
__DATA__

define __delete__ /[bcdfgkmnpsrtvwyz]e( |$)/
define __match__  /ey|y[aeiou]*|[aeiou]+/
define __count_vowels__ <<EOS
{
    s = tolower($0);
    gsub(__delete__, " ", s);
    for (count=0; match(s, __match__); count++) {
	s=substr(s, RSTART + RLENGTH);
    }
    print count " " $0;
}
EOS

option --vowels __count_vowels__
help   --vowels count vowels
```

これは、普通の Perl モジュールなので、何でも書くことができる。
例えば、

    package default;
    $ENV{LANG} = 'C';
    1;

こんな default モジュールを用意して置くと、`date` コマンドを実行する前に、環境変数 `LANG` を `C` に設定する。なので、こんな風になる。

    % /bin/date
    2017年 10月22日 日曜日 18時00分00秒 JST

    % date
    Sun Oct 22 18:00:00 JST 2017

その他のモジュールは `-M` オプションで指定する。

`~/.optex.d/es.pm` というモジュールは `-Mes` オプションで指定する。

    package es;
    $ENV{LANG} = 'es_ES';
    1;

    % date -Mes
    domingo, 22 de octubre de 2017, 18:00:00 JST

# サンプル

サンプルのモジュールがいくつか同梱してある。

## -Mutil::filter

util::filter モジュールは、入出力フィルタを集めたモジュールだ。

### &timestamp

macOS の ping コマンドには ``--apple-time`` というオプションがあるのをご存知だろうか。これを指定すると、RTT だけではなく、パケットが到着した時刻を表示してくれる。

<img width="762" alt="スクリーンショット 2018-11-18 9.50.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/36551/e22c8519-f3da-90cd-c16e-3bf5d96459b7.png">

util::filter モジュールの `timestamp` 関数を使うと、コマンド出力の各行の先頭にタイムスタンプを挿入する。
ping コマンドに使うとこんな感じだ。

<img width="762" alt="スクリーンショット 2018-11-18 9.46.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/36551/9f04f19f-3faf-de61-d140-d912d9a54fe2.png">

`~/.optex.d/ping.rc` には、こんな風に書いてある。

```:~/.optex.d/ping.rc
option --time -Mutil::filter --of &timestamp
```

`util::filter` モジュールをロードして、出力フィルタとして timestamp 関数を指定している。

試しに、両方指定してみるとこんな風になった。誤差は1ミリ秒以下だ。

<img width="842" alt="スクリーンショット 2019-12-24 17.56.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36551/4fd65168-27d5-40ae-3006-b07fd377a2fd.png">

`timestamp` 関数の引数で `format` を指定することもできる。デフォルトは `%T.%f` で `%f` はマイクロ秒、`%L` はミリ秒を表す。`%dN` の形式で精度を指定することができて `%f` と `%L` は、それぞれ `%6N`、`%3N` と同じだ。他にも `strftime` がサポートする形式を指定することができる。

`App::sdif` に含まれる `watchdiff` コマンドと組み合わせてこんな風に使うこともできる。

```
optex -Mutil::filter --osub timestamp=format='%F %T.%L' watchdiff -pr0 uptime
```

### --if

`--if` オプションにはコマンドを指定することができるので、こんな設定を `~/.optex.d/default.rc` に置いておくと、すべてのコマンドの標準入力で圧縮データを入られるようになる。

```:~/.optex.d/default.rc
option --gz -Mutil::filter --if 'gunzip -c'
```

これだと圧縮ファイルは指定できないが、次の process substitution などと組み合わせれば実現可能だ。

### --io-color

標準出力とエラー出力が混ざって見にくいと感じたことはないだろうか。``util::filter`` モジュールで定義されている ``--io-color`` オプションを使うと、標準エラー出力の色を変えて出力してくれる。自分の ``~/.optex.d/default.rc`` には次のように指定してあるので optex を通じて実行すると、すべてのコマンドのエラー出力が着色されるようになっている。

```~/.optex.d/default.rc
autoload -Mutil::filter	--io-color --set-io-color
option default --io-color
```

<img width="762" alt="スクリーンショット 2018-11-18 9.40.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/36551/2f39ab01-f3c6-bfdf-7002-8ef4fe90edee.png">

デフォルトでは ``555/201;E`` という色を指定してあるので、気に入らなければこれを変えてもいいし ``--set-io-color`` オプションを使って好きな色を設定してもいい。色の指定方法については ``perldoc Getopt::EX::Colormap`` を参照して欲しい。ここで使っているのは RGB 6x6x6 216色の色指定で、555 は白で 000 が黒だ。``E`` は実はマイブームの機能で、行削除のシークエンスを出力する。この時、背景色が指定されていると、その色で行末までを塗りつぶすので、上の画像のような結果になる。

```util/filter.pm
option --set-io-color &io_color($<shift>)
option --io-color --set-io-color STDERR=555/201;E
```

## -Mutil::argv

util::argv モジュールは、コマンド引数を何とかするモジュール。

たとえば、times 関数を使うと、引数を増やすことができる。

    $ optex echo -Mutil::argv::times=count=3 a b c
    a a a b b b c c c

まあ、こんなのはあまり役には立たない。

自分はずっと tcsh を使っていて、process substitution の機能を使いたい時だけ bash を使っていたのだが、最近ついに30年以上使い続けた tcsh からログインシェルを bash に移行した。

これも、util::argv::proc 関数を使うと実現できる。

<img width="762" alt="スクリーンショット 2018-11-18 10.21.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/36551/e1d37c6c-a56e-6a94-6bc9-6cfc0f1379a9.png">

# インストール

とりあえず、

    $ cpanm App::optex

でインストールできるはず。

cpanm がない場合は、cpanminus で検索してインストールしてください。

----
# 追記: コンフィグレーションファイル

`~/.optex.d/config.toml` で設定情報を指定できるようにした。

## alias

今までのままでも、別の名前でリンクを作って設定ファイルを作れば alias 的な使い方はできたのだが、設定ファイルの中で指定できるようにした。次のように、文字列か配列で指定できる。

```~/.optex.d/config.toml
[alias]
    pgrep = [ "greple", "-Mperl", "--code" ]
    hello = "echo -n 'hello world!'"
```

## no-module

元々 ``Getopt::EX`` を使っているコマンドは ``-M`` オプションを解釈するので、``optex`` から呼ぶと混乱する。そのため ``no-module`` と指定してあるコマンドについてはモジュールオプションを解釈しないようにした。

```~/.optex.d/config.toml
no-module = [
    "greple",
    "pgrep",
]
```