シェルスクリプトのオプションパーサに柔軟性を求めて

シェルスクリプトを書いていると、テキトーなオプション指定をテキトーに解釈し、厳密な指定には厳密に答えてくれるオプションパーサが書きたくなる。


もちろん、この「テキトー」というのはわりと高度な人間的処理のことだから難しいとは決まってるんだけど、そういう要求はずーっと長いこと求められていたはずで、だからそのようなユーティリティが発達してるはず。


いつもいつも、自分でちょろっと書くのは馬鹿馬鹿しいに決まってると思いつつ、調べる隙がないような気分で、ちょっと手を出してしまったりする。そして本体の処理よりずっと長くなる。


いまもコマンドラインから実行してOSX の Notification Center に通知をくれる単純なポモドーロタイマーを書いてるんだけど(名前はpomotimer)、主要部はものすごく簡単なのに、オプションパーサが本体の5倍くらいになってしまった。


パーサでやりたいことを箇条書きにすると、

1. pomotimer 5 60
みたいに2つの数字を取ったら、小さい方を休憩時間、長い方を全体の時間として実行。

2. pomotimer 5
みたいに短い数字だけ取ったら休憩時間と解釈し、全体の時間はデフォルト時間(60分。ソースの最初の宣言部で指定可)で実行。

3. pomotimer 45
みたいに長い数字だけ取ったら全体の時間と解釈し、休憩時間はデフォルト時間(10分。ソースの最初の宣言部で指定可)で実行。

4. pomotimer -r 10
または
pomotimer -r10
みたいな形で-r(レシオ)を指定されたら、デフォルト全体時間に対する休憩時間のパーセンテージと解釈して実行。

5. pomotimer -r 10 45
みたいに-rと1つの数字を指定されたら、数字の方を全体時間として実行。

6. 余計な情報を指定されたら捨てる。
これは普通の設計思想としては間違ってて明示的にエラーを出すべきだけど、今回のようにすぐ解釈結果の見えるプログラムで(notificationに「*時*分まで*分集中してください」と出る)、エラーを気にせず仕事に集中したい場合、直すプレッシャーがかかるより適当に動いてくれる方がありがたい。

7. オプション指定の順序は問わないこと。

という感じ。


んで、これを単純にif文とかで実装していくよりは、既存のツールを使うほうがいいに決まってると思ったので、bash付属版とGNUのコマンド版のgetoptを試してみた。

bashのgetopts
  • -r 10 や -r10 の指定はできるが 5 30 などの数値のみの引数を解釈してくれない
  • オプション以外が出たら解釈やめちゃう
  • -rなどのオプションを解釈した後も、解釈済みの引数を除去した引数リストを返してくれないから、数値のみの引数を自前で解釈しようとするならオプション部分を捨てる処理を書く必要がある

という感じで、手間はぜんぜん減らない。

gnu getoptコマンド
  • -r 10 や -r10 の指定はできるが 5 30 などの数値のみの引数を解釈してくれない
  • 間違った引数があればその後は全部捨てちゃう

という感じ。やっぱり全然手間は減らない。


どっちにしても手間は減らないし解釈の柔軟性もない。間違ってたら拒否、という態度は普通の設計思想では正しいんだけど、要求6や7を満たすことはできない。


オレがついつい上のようなオプションパーサを求めるのは、Pythonのoptperseやargparseがそういったことをだいたいカバーしているから。shellならもっと昔からあるし、別の形の優れたアプローチになってるんじゃないか、と思ったけど、なかなかそうもいかないみたい…。オプションを解釈した後で残りの引数リストをくれないってのは致命的な面倒だと思うんだけど。


結論としては、オプション以外の引数の解釈はif文を並べるのが結局楽でシンプル。
そこにオプションを混ぜるならbash正規表現(if [[ $1 =~ ^[0-9]* ]] みたいに書ける)で自分で場合分けする。
ツールに頼らない方法は面倒な気がするけど、引数の少ないシェルスクリプトなら現実的な長さに収まるので、だからツールが存在しないのかもしれない。


…ていうか、Pythonで書いた方が楽な気がしてきた。


参考:
bash によるオプション解析 - Qiita

      • -

2014/04/20追記
http://shellscript.sunone.me/parameter.html
の「shiftコマンドでオプション部分の切り捨て」を見ると、オプションをgetoptsで処理した後にOPTIND(オプションのインデックス番号。処理するとインクリメント)から1を減じた数だけshiftしてる。なるほど、オプションは先に、それ以外の引数は後に書くという文化がやっぱり一般的なわけね。