拡張case文 - Bash Advent Calendar - Day 10

今日も本当に1行もスクリプトも書いてないし、なんの検証もしてない状態でこのブログを書き始めました。途中で破綻しないといいんですけどね。

そもそも、今日のお題はどちらかというと extglob なので、"c" の順番じゃないんじゃないかと思うんですが、所詮は個人でやってる Advent Calendar、細かいことは気にしないでください。

もし気にする方は、代わりにお題をください。いや、気にしない方もこれ読んだらお題をください。

globってなんぞや

今日の本当のお題である extglob という単語は拡張 extended glob の略です、多分。じゃ glob って何って言ったら、そりゃ Wikipedia でも参照してください。Wikipedia の当該エントリ曰く、"Global command" の略だそうです。

で、世間一般では、よく言われるのはワイルドカードってやつですね。POSIX だと、単に「パターンマッチング」とか呼ばれてるアレです。man 7 glob でもいけるかもしれません。あの、
  • *任意の0文字以上マッチ
  • ?任意の1文字にマッチ
  • [c...]ブラケット内のいずれか1文字([a-z]のような範囲指定も可)にマッチ
という、単純なやつです。で、こんなの拡張するぐらいだったら正規表現使えばいいじゃんと、世界中から総ツッコミが入りそうなところですが、シェルスクリプトでは "echo ファイル*" みたいなパス名マッチングだけでなく
${parameter%pattern}
case ... in pattern) ...
と、それ以外にも Bash では
[[ str == pattern ]]
とか pattern が使われるところがあるので、これを拡張すると他のLLは嫌いだけどシェルスクリプトの最新機能は好きとかいう偏食家に大いに受けるわけです。ちなみに最後のは、複雑なことをしたければ [[ str =~ regex ]] 使ってください、と言いたいところだけど例のごとく Bash は遅いので、sed とか awk, grep を使ったほうが無難です。

case文

簡単に。書式としては
case WORD in
  pattern) expression;;
  pattern | pattern ...) expression;;
esac
で、WORD が pattern にマッチしたら expression が実行されます。複数のパターンにパッチする場合、最初にマッチしたものが使われます。

このとき pattern は glob パターンとして解釈されるので
case foo in
  *) echo "wildcard";;
esac
と実行すると、"wildcard" と表示されます。Bash の場合は [[ WORD == pattern ]] というマッチングがありますが、そうじゃないシェルでも case 文ではパターンマッチングが使えるので、case による場合分けがなくてもパターンマッチングのためによく使われます。 ちなみに、ワンライナーの場合は
$ case foo in *) echo bar;; esac
とそのままくっつけちゃって大丈夫です。

shopt

少し寄り道を。Bash の動作を操作する方法として、set コマンドというのが良く使われると思います。たとえば
  • set -xでデバッグ
  • set +Hでヒストリー展開を無効にする
  • set -uで typo 探し
と、皆さん馴染み深いと思います。が、色々と(変な)機能が満載の Bash さん、a-z A-Z の 52 文字じゃ全然足りません。というわけで "shopt -s OPTION" という形で、色々な動作変更ができます。暇な人は man bash で compat31 で検索してみてください。Bash の黒歴史がかいま見えます。

で、そうしたオプションの中に extglob というオプションがあって、これを設定することで拡張 glob パターンが使えるようになります。ちなみに shopt の使い方は
  • shopt -s OPTION
    OPTION を有効にする
  • shopt -u OPTION
    OPTION を無効にする
  • shopt -q OPTION
    OPTION が有効なら終了ステータス 0, そうでなければ 1 を返す
となっております。shopt はシェルの動作仕様を変えることができるので、shopt -q で現在の設定を取得、色々処理した後に、必要であればもとに戻すというのがお行儀よいと(私の心の中でだけ)言われております。

extglobで使えるパターン

やっと本題に近づいて参りました。なんかもう飽きてきた。みんな本当にここ読んでる?
さて、extglob でどんなことができるかというと
  • pattern|pattern
    パターンのいずれかにマッチ(前述のとおり、case 文で | が使えるのはそれが case の文法の一部だからであって、デフォルトの glob パターンには | はありません)。以下の (...) 内でのみ使えます。
  • ?(pattern-list)
    パターンに0回または1回マッチ
  • *(pattern-list)
    パターンの0回または1回以上の繰り返しにマッチ
  • +(pattern-list)パターンの1回以上の繰り返しにマッチ
  • @(pattern-list)パターンのいずれかにマッチ
  • !(pattern-list)パターンのいずれにもマッチしない任意の文字列にマッチ
以上のとおりです。@(...)以外は(その記法は置いておくとして)なんとなく分かるとおもいます。@(...) については、既存の bash の文法に無理矢理追加する上で何らかの記号が必要であった事情をお察しください。

ここまで長かったですが、では、ちょっとここまで説明してきた機能を試してみましょう。
$ foo="foobarbuzz"
$ case "${foo}" in "foo"*) echo 0;; *) echo 1;; esac
0
$ case "${foo}" in "FOO"*) echo 0;; *) echo 1;; esac
1
ここまでは普通。
$ shopt -s extglob
で extglob が有効にして試してみましょう。
$ case "${foo}" in @(FOO|foo)*) echo 0;; *) echo 1;; esac
0
となりました。これだけだと本当は
$ case "${foo}" in FOO* | foo*) echo 0;; *) echo 1;; esac 0
と一緒で大したことないんですが
case ${method_name} in @(set|get)_@(foo|bar)) echo buzz ;; qux) ... ;; esac
みたいな感じで、便利に使える場合があります。

今日のネタ

さて、全然ネタっぽいところがなくてつまらないですね。ただただ便利なだけ。しかし、もう一度途中で出てきたコマンドを extglob を無効にしてから実行してみましょう。
$ shopt -s extglob
$ case "${foo}" in @(FOO|foo)*) echo 0;; *) echo 1;; esac
0
$ shopt -u extglob
$ case "${foo}" in @(FOO|foo)*) echo 0;; *) echo 1;; esac
bash: 予期しないトークン `(' 周辺に構文エラーがあります
わーい。というわけで、なんと extglob の有無で言語の文法が変わってしまうという、実は超強力機能なんです。 

で、これはコマンドラインで実行してるから可愛いものなのですが、たとえば .bashrc で
shopt -s extglob
case $HOSTNAME in)
  @(foo|bar).example.com)
    PS1="SERVER $PS1"
   ;;
esac
とか書いておくと、場合によっては bash が起動時に激おこになります。なぜなら、 .bashrc 読み込み時点で extglob が有効になってない場合、shopt -s extglob を実行する以前にパースエラーになってしまって何も実行されないからです。

というのを知らずに、みんなの .bashrc から読み込まれるファイルに上記のようなコードを仕込んで、数百人に迷惑をかけたことがあるので、みなさんも気をつけましょうというお話でした。
コメントを投稿

このブログの人気の投稿

シェル操作課題への回答

連想配列 - Bash Advent Calendar - Day 5

今、永福町のピザ戦争が熱い!