シェルスクリプトで役立つテキスト文字列のパターンマッチングの基礎

 シェルプログラミングでは、文字列に関する操作が大きな役割を果たすことになる。ここで言う文字列とは、連続した任意のキャラクタの並びで構成されるテキストのことで、具体例としてはインプットされるテキストやコマンドの引数などを考えればいい。そして、プロンプトへの応答入力、必要なファイル名の生成、コマンドの出力結果に対する処理を繰り返し実行させる際に必要となるのが、特定の文字列が特定のパターンと一致しているかという確認作業であり、これこそがパターンマッチングと呼ばれているプロセスである。そしてシェルには、パターンマッチング用の様々な機能が用意されているのだ。

本稿は最近出版された『Beginning Portable Shell Scripting』からの抜粋である。

 パターンマッチングの機能はgrepsedなど、多くのUnixユーティリティの形態で提供されている。通常この種のプログラムでは、正規表現(regular expression)と呼ばれるより強力なパターンマッチング機能が利用できる。正規表現はシェルパターンとは別系統の機能だが、効率的なシェルスクリプティングを行う上で必須の存在と考えて間違いないはずだ。ただし一口に正規表現と言っても、シェル内部にて共通化された正規表現機能が移植性のある形でサポートされている訳ではなく、シェルプログラムが依存する各種の外部ユーティリティが、それぞれ個別に正規表現を実装しているという状況になっている。

シェルパターン

 シェルパターン(shell pattern)は各種の状況にて利用される機能である。その代表例の1つがcaseステートメントであり、例えば下記のサンプルコードではstringpatternという2つのシェル変数を用いて、stringpatternに一致するかを判定している。

case $string in
  $pattern) echo "Match" ;;
  *) echo "No match";;
esac

 このサンプルでは$string$patternと一致した場合、シェルからのエコーとして“Match”が返されてcaseステートメントの分岐処理は終了する。そして一致していなかった場合に行われるのが$string*と一致するかの判定である。シェルパターンにおける*は任意の文字列との一致を意味するので、このサンプルでは$patternとの不一致時にシェルから返される出力は“No match”となる(仮に一致するパターンが複数存在してもcaseステートメントで実行される分岐処理は1つだけである)。

 これから初めてパターンマッチングを学習するユーザの場合、先のサンプルコードを基とした試験用のシェルスクリプトを用意しておくと便利である。例えば、特定パターンに対する一連のマッチング試験を連続的に処理したければ、下記のようなスクリプトを用いればいい。

#!/bin/sh
pattern="$1"
shift
echo "Matching against '$pattern':"
for string
do
  case $string in
  $pattern) echo "$string: Match." ;;
  *) echo "$string: No match." ;;
  esac
done

 実際にこのスクリプトを使用するには、同コードをpatternという名称で保存したファイルを実行可能にしておく必要があるが(chmod a+x pattern)、そうした準備後は下記のような一連の試験を簡単に行えるようになるはずだ。

$ ./pattern '*' 'hello'
Matching against '*':
hello: Match.
$ ./pattern 'hello*' 'hello' 'hello, there' 'well, hello'
Matching against 'hello*': hello: Match.
hello, there: Match.
well, hello: No match.

 この場合の注意点として、個々の引数は一重引用符で囲む必要がある。特にパターン中で使うアスタリスク(*)などの特殊記号が引用符で囲まれていないと、グロビング(globbing)というシェル内部の処理(ファイル名展開とも呼ばれる操作)によって当該パターンに一致する名称を持つ任意のファイルに強制的に置き換えられるため、ここで意図するような試験は行えなくなってしまう。

パターンマッチングの基礎

 パターンに用いる通常キャラクタの大部分は、自分自身と同じ文字とのみ一致すると判定され、他の文字と一致すると見なされることはない。例えばhelloというパターンに対して一致する文字列はhelloだけである。また目的とする文字列の一部だけに一致するパターンがあっても、それは当該文字列に一致しているとは見なされず、先の例で言うと、「hello, world」というテキストに対しhelloは一致していないと判定される。つまりあるパターンが特定の文字列に一致するには、下記の2つの条件を満たしていなくてはならない。

  • パターンの構成キャラクタが、文字列側のものと一致している
  • 文字列の構成キャラクタが、パターン側のものと一致している

 ただしパターンの有す機能がこれだけであれば、それは単純な文字列比較を別の単語で言い換えただけに過ぎず、この章の残りの部分も「空白文字を挟んで連続した非空白文字のブロックは……と呼ばれ」といった埋め草的な解説でお茶を濁すか、あるいは美味しいクッキーの焼き方というクッキングレシピでも書き込んでおけばいいはずだ。残念ながら実際にはそうなってはおらず、パターン中では自分自身以外のキャラクタとも一致する特殊な記号が使えるのであり、こうしたパターン中にて特殊な意味を有すキャラクタ群はワイルドカード(wildcard)あるいはメタキャラクタ(metacharacter)と呼ばれている。なおワイルドカードという表現については、特殊記号の中でも任意のキャラクタと一致するものだけに限定すべきだという意見も一部から出されているが、シェル内にて特殊な機能を果たすキャラクタ群との混同を避けるため、ここで行うパターンについての解説ではそうした制限を設けることなく、この種の記号すべてをワイルドカードと呼称しておく。いずれにせよワイルドカードの存在は、先に提示した2つの基本ルールを複雑なものとしてしまう。なぜならパターン中の単一キャラクタが長大な文字列と一致することもあれば、逆にパターン中に配された一連のキャラクタ群がわずか1文字分のキャラクタだけに一致したり、まったく一致しなかったりする結果となるかもしれないからだ。重要なのは、対象となる文字列を取りこぼすようなミスマッチ部が存在しない対応関係となれるかという点である。

 ワイルドカードのうち最も多用される記号は、任意の1キャラクタに一致する疑問符(?)および、空白文字列も含めた任意長の文字列に一致するアスタリスク(*)である。

 実際のパターンにおける?の使用法は簡単で、その位置には1文字分のキャラクタが入ることは分かっているが具体的にどのキャラクタかは不明、という箇所に用いればいい。例えば、ユーザによっては挨拶の言葉にhallohulloといった単語を使う者もいるが、h?lloというパターンを使えばこうした表記にも対応できるはずである。ただしこうした指定法には2つの問題が潜んでいる。1つ目の問題として、例えば先のようなタイプのユーザを扱う場合は、hello, therehello little computerないしhello how do i send emailといった冗長な表現が使われる傾向が見られるはずだ。つまりこれらの文字列が挨拶関連の文面であるかを判定するのであれば、文末が特定の単語ないしそうした単語プラスアルファで終わるか、という指定をする必要が生じてくる。

 そうした処理に利用できるのが*である。*は任意長の文字列と一致するので、hello*というパターンを用いれば、hello単体だけでなくhello以降に何らかの文字列が続くテキストとのマッチングが行える。ただしこのパターンもwell, helloという文字列には一致しない。それはhelloの前にキャラクタがあるテキストに対するマッチングの指定がないためである。そのため、どのような位置であれ特定の単語が出ているテキストを検出したい、という場合はアスタリスクを前後両方に配したパターンを使うのが常套手段であり、ここの例で言うと*hello*としておけば広範な挨拶文にマッチングするはずだ。

 これら2つのワイルドカードを組み合わせると、文字列の長さその他の要件がより不確かなマッチングをさせることも可能である。またhello ?*というパターンはhello worldには一致するが単独のhelloだけには一致しない。ただしこのパターンについては、また別の問題が生じることになる。それは半角の空白文字を示すキャラクタは、パターンでは特殊な意味を有さないがシェルでは特殊な意味を有しているためだ。つまりこのパターンを引用符で囲んでおかなかった場合、シェルにより複数の単語に分割されることで意図したマッチングが行えなくなってしまうのだが、逆に引用符で囲んだ場合シェルはワイルドカードを無視してしまうというジレンマが生じるのである。この問題に関しては、空白文字を引用符で囲んでおくか、ワイルドカードは引用符で囲まないようにするという2つの対策が存在し、この場合の指定についてはhello" "?*または"hello "?*と記述しておけばいい。

 シェルがパターンマッチングを実行するコンテクスト(caseステートメントなど)については変数置換の分割が行われないので、変数置換にまつわる空白文字を気にする必要はない(ただしzshの場合shエミュレーションモードを使用しない状況での結果は異なってくる)。

キャラクタクラス

 h?lloというパターンに潜んでいる2つ目の問題は、その一致範囲が広すぎるというものだ。先に触れたような、正規の英語に比して多少変則的な綴りのスペルも受け付けるようにしておくという配慮はその種のユーザから歓迎されるだろうが、hzlloh!llohXlloなどは明らかに除外すべきであろう。そうしたより一致範囲の狭いマッチングをシェルにて実行するために用意されているのが、キャラクタクラス(character class)という機構だ。1つのキャラクタクラスに一致するのは指定範囲内にあるキャラクタの1つだけであり、これは?に一定の制限を設けた機構だと考えてもいいだろう。キャラクタクラスの指定は角カッコ([])で囲むことで行え、具体的には[characters]といった記述をする。例えばここでの例として用いた挨拶文に対するマッチングについてはh[aeu]lloという1つのキャラクタクラスにまとめることができるのだ。なおキャラクタクラスのマッチングにて一致するのは、指定したキャラクタ群のいずれか1つだけであり、これらキャラクタの複数個に一致するという判定はされない。

 キャラクタクラスの一般的な用途は、特定範囲内のキャラクタを指定するという使い方である。実際、任意の数値を対象としたマッチングには[0-9]という指定がよく用いられる。このように2つのキャラクタをハイフンでつないだものは、これらを両端とするキャラクタセット全体を意味する指定だと解釈されるので、その用途としては順番的に連続した並びのアルファベットや数値を示す目的で使われるのが一般的だ。ただしこのパターンでは大文字と小文字が区別されるので、ASCIIの標準アルファベットすべてを指定するのであれば[a-zA-Z]という指定をしなくてはならない。また範囲を特定する2つのキャラクタを前後逆に指定すると、実際にどのようなマッチングが実行されるか予測不能となるので、そうした指定を行うべきではない。

 また“何を含めるか”ではなく“何を除外するか”が分かっている場合は、キャラクタクラスを反転させる記号として、その先頭キャラクタに感嘆符(!)を付ければいい。例えば[!0-9]というキャラクタクラスは、数字でない任意のキャラクタに一致する。ただしキャラクタクラスの反転は、通常使われるアルファベット以外の記号類にも一致範囲を広げてしまう点に注意が必要で、仮に子音だけに一致させるつもりで[!aeiou]という指定をすると、句読点や制御記号なども含めることになってしまう。

 またキャラクタクラス中でワイルドカードを利用することはできず、例えば[?*]という指定は、疑問符かアスタリスクのいずれかに一致するだけになる。

 こうしたキャラクタクラスは、シェルで用いるパターンマッチングのうち最も複雑な部類に属すもので、特に左右の角カッコ([])、ハイフン(-)、感嘆符(!)は、キャラクタクラスにて意味をなす特殊記号である。また通常の文字としてのハイフン記号そのものをクラスに含めたい場合は、キャラクタクラスの末尾に位置するキャラクタとして指定すればよく(その後には何のキャラクタも続けない)、同じく感嘆符についても、キャラクタクラスの先頭以外であれば任意の場所に配置すればいい(2番目以降に置くべきキャラクタが存在しない場合は、そもそもキャラクタクラスを使う必要性がないので、目的となるキャラクタを単独で指定するべきである)。同様の規則は左側の角カッコの場合も当てはまり、これは任意の場所に配置しておけばいいが、右側の角カッコ(])については、リストの先頭キャラクタとするか、あるいはクラスを反転させる!の直後にのみ置くことだけが可能であり、それ以外の場所に右角カッコが配置されていると、それをシェルはキャラクタクラスの終了部を示す記号だと判断するかもしれない。またこうした機能セットに潜む単純なバグとして、シェルによってはキャラクタクラスにおける右角カッコの判定が正常に行われないこともあるので、そのような現象に遭遇した場合は可能な限り使用を避けるべきである。

 具体例として“他のキャラクタはすべて除き、左右の角カッコ、ハイフン、感嘆符のいずれかのみに一致させたい”という場合は下記のように指定すればいい。

[][!-]

 このうち1番目の左角カッコはキャラクタクラスの開始を示す記号だが、同じく1番目の右角カッコはキャラクタクラスの終了を示す記号ではなく、対応する角カッコのペア間に何も置かれていないため、通常の文字としての角カッコと見なされる。そして2番目の左角カッコと感嘆符も、その置かれた位置は任意キャラクタを配置する場所であるため、特殊な意味を有す記号とは見なされない。その次にあるハイフンも通常の文字としてのハイフンと見なされるが、それは末尾に位置する2番目の右角カッコは当該キャラクタクラスの終了を示す記号と判定されるため、このハイフンは他の2つのキャラクタを挟む位置に置かれていないからである。

 なおシェル内で使用するキャラクタクラスに関しては、キャレット(^)を!の代わりに使用するユーザも多い。これはすべてのシェルにて採用された移植性のある表記法ではないが、正規表現で使われる記号と共通しているため、多くのシェルが1つの拡張としてこの記号の使用を許可している。いずれにせよこうした使用法もあることを知っておかないと、通常の文字としてのキャレットをキャラクタクラスでマッチングさせようとする場合に、想定外の結果が返されるかもしれない。

 表2-1には、キャラクタクラスで使われる特殊記号および、それらを通常文字として認識させる記述法がまとめてある。

表2-1 キャラクタクラスで使われる特殊記号

キャラクタ 意味 移植性 通常文字としての記述法
] キャラクタクラスの終了 汎用 キャラクタクラスの先頭に置く(あるいは反転指定記号の直後)
[ キャラクタクラスの開始 汎用 キャラクタクラス中の任意の位置に置く
^ 反転の指定 一般的 他のキャラクタの次に置く
! 反転の指定 汎用 他のキャラクタの次に置く
範囲の指定 汎用 キャラクタクラスの先頭または末尾に置く

 キャラクタクラスの範囲指定に関しては、主として英語圏の人間が見逃し勝ちな問題も隠されている。それは[a-z]という指定がアルファベットの小文字すべてに一致するとは限らないというもので、より厳密に言うと小文字以外にもマッチする可能性があるのだ。その本質的な原因は、この種の指定をする際の一般的な傾向として、アクセント付きのアルファベットを取り込んでいないASCIIのキャラクタセットを前提とすることにある。ASCIIの定義では、大文字および小文字のアルファベット群はどちらも連続した並びで配置されている(ただし両者の間には若干の記号類が置かれているため[A-z]というマッチングはそうした記号にも一致してしまう)。ところがUnix系システムの場合、これらとは異なる配置が採用されている可能性があるのだ。いずれにせよ[a-z]という指定をすれば26あるアルファベットの小文字に一致するというのは汎用性のある一般的な想定と見なせるはずだが、アクセント付きのアルファベットまで考えた場合、このパターン指定にてマッチさせることはできない。更にこの種の特殊なアルファベットに関しては、汎用性のあるマッチング指定ができないどころか、その確認法すら存在しないのである。この種の問題の対処法としては、スクリプトを実行する個々の環境に応じてキャラクタセットの指定も変更するしかないと考えておくのが無難だろう。

 現状ではkshpdkshを除く)やbashなどに限定される話だが、一部のシェルはPOSIXにて導入されたその他のキャラクタクラスも追加サポートしている。それは[[:class:]]という指定法で、この中のclass部にはdigitalphapunctなどのキーワードを指定でき、これにより対応するCのisclass()関数にてtrueが返される任意のキャラクタに対するマッチングが行えるのだ。例えば[[:digit:]]という指定は[0-9]に相当する。またこれらのクラスは他の通常キャラクタと組み合わせることも可能で、例えば任意の数字とアルファベットに加えてアンダースコア記号(_)にも一致させたければ[[:digit:][:alpha:]_]と指定すればいい。これとよく似たものに、特殊なコレーティング(collating)シンボルとのマッチングを行う[.name.](地域や言語によっては、chといった特定アルファベットの組み合わせとその他のchが並ぶ場合とではマッチングとソーティングに関する規則が異なるといった場合がある)、および特定の小文字とそこから派生したアクセント付き記号といった等価クラスに対するマッチングを行う[=name=]という規則がある。これらの規則は多国語対応のスクリプトを記述する際に便利なはずだが、現状では実際の移植性を確保できるほどの普及はしていない。いずれにせよこうした表記法が存在する以上、キャラクタクラス内部に置いた左角カッコの直後には、ピリオド(.)、等号(=)、コロン(:)を使わないように配慮しておくべきである。ただしこの制限が適用されるのはキャラクタクラスの開始を示す左角カッコではなく、その内部に置かれた左角カッコについてであり、例えば[.]はピリオドという記号そのものに対するマッチングとなる(特に正規表現の場合、こうした表記をしないピリオドは特殊記号の1つとして解釈される)。

 ここまで解説したように、シェルのパターンマッチングの中でも特に複雑な規則が適用されるのがキャラクタクラスである。

 シェルパターンは強力な機能である反面、キャラクタクラスに対する反復指定はできず、任意桁数の数値に対するシェルのパターンマッチングも行えないというように、様々な欠点も有している。またパターンの一部をオプション化する指定も不可能で、任意選択要素の指定法としてアスタリスク以上の機能は用意されていない。

 一般的にパターンを用いたマッチングでは、最長マッチと呼ばれる一致判定が行われ、可能な限り広範な文字列とのマッチングが試みられる。しかしながら、アスタリスクのマッチ部に必要範囲以上のキャラクタが取り込まれた結果、当該パターン中の他のコンポーネントによるマッチングが妨げられるというように、アスタリスクによる最長マッチに起因して目的とする文字列全体とのマッチングが行えなくなる可能性も存在するのだ。例えばパターンb*を用いた文字列bananaに対するマッチングでは*部のコンポーネントがananaというテキストに一致するが、このパターンをb*naに変更すると*部が一致するテキストはanaとなるというように、*はマッチングが可能な範囲内にて最大限のキャラクタを取り込もうとするのである。またキャラクタクラスなどその他のパターンコンポーネントについては、通常の文字であるリテラルキャラクタおよび特殊記号の1つである疑問符のマッチングが最優先され、その残りの部分に対してアスタリスクによるマッチングが行われるようになっている。

 こうしたシェルパターンに付随する制限の一部は、ユーザ側の工夫によって回避することも可能であり、例えば複数のアイテムを並べたリストをシェル内にて格納したければ、対象となるアイテム群をa,b,cのように区切り文字を挟んでつなげればいい。下記のサンプルコードは、こうしたリストの具体的な使用例である(このcaseステートメントでは、指定した文字列とパターンが一致した場合にのみ出力用のコードを実行する)。

list=orange,apple,banana
case $list in
*apple*)  echo "How do you like them apples?";;
esac

How do you like them apples?

 ただしこのサンプルスクリプトには、一致のチェックが厳密でないことに起因したバグが潜んでいる。具体的な問題点は、下記のような若干の変更を加えることで明確化するはずだ。

list=orange,crabapple,banana
case $list in
*apple*)  echo "How do you like them apples?";;
esac

How do you like them apples?

 これは先に触れたアスタリスクによる最長マッチに起因する問題であり、その対策としては、最長マッチの影響範囲と区切り記号のカンマの位置関係なども考えなくてはならない。一見すると、下記のようにパターン本体に区切り記号を追加すると上手く行きそうだが、今度はリストの両端に位置するアイテムには一致できなくなってしまう。

list=orange,apple,banana
case $list in
*,orange,*)  echo "The only fruit for which there is no Cockney slang.";;
esac

[no output]

 この問題の最終的な対処法としては、下記のようにリストの展開時に意図的に区切り記号を追加しておくことが考えられる。

list=orange,apple,banana
case ,$list, in
*,orange,*)  echo "The only fruit for which there is no Cockney slang.";;
esac

The only fruit for which there is no Cockney slang.

 こうして得られる$listの展開結果はカンマが両端に置かれた文字列となり、当該リストのアイテムすべてがカンマに挟まれた状態となるので、先のパターンによるマッチングは問題なく実行可能となる。

Linux.com 原文(2008年12月26日)