ShellScript

POSIX shの変数展開による文字列置換の限界を探る

More than 1 year has passed since last update.

シェルスクリプト中で文字列置換をしたくなったら普通はsedやawkを呼び出すのが正解でしょう。ただ、世の中にはプロセスのforkが異常に遅い環境というのがあるので(CygwinとかMSYSとか)、シェルスクリプト内で頑張りたくなることがあります。

そんなときに使えるのがシェルの変数展開の機能です。特に下記の4つの書き方はPOSIX shがサポートしているのでポータビリティの観点でも安心して使うことができます。

表現
説明

${parameter%パターン}

$parameterの末尾からパターンにマッチする文字列を取り除いた値を返す
(最短一致)

${parameter%%パターン}

$parameterの末尾からパターンにマッチする文字列を取り除いた値を返す
(最長一致)

${parameter#パターン}

$parameterの先頭からパターンにマッチする文字列を取り除いた値を返す
(最短一致)

${parameter##パターン}

$parameterの先頭からパターンにマッチする文字列を取り除いた値を返す
(最長一致)

これらを使ってどこまで頑張れるのかについて調べてみました。


パターンの書き方

上記のパターンの部分はいわゆるワイルドカードマッチで、次のような表現が使えます。

表現
説明

?
任意の1文字にマッチ

*
任意の0文字以上にマッチ

[abc]
文字a,b,cいずれか1文字にマッチ

[!abc]
文字a,b,c以外の1文字にマッチ

[a-z]
aからzまでの1文字にマッチ

[!a-z]
aからz以外の1文字にマッチ

上の表を見てわかる通り、パターンマッチの能力はかなり貧弱です。特に、直前の表現のn回繰り返し(正規表現で言う*)が使えないのは非常に痛いですね。


POSIX shでできること一覧

わかりやすさのため、変数展開で出来ることを正規表現と並べて記述してみました。


先頭または末尾の固定文字列の削除

正規表現
POSIX shで対応する変数展開

README.mdに対して適用した結果

s/\.md$//
${str%.md}
README

s/^README//
${str#README}
.md

元の文字列と比較することでマッチングの成功失敗も判定できますから、次のようにifで分岐すれば文字列置換を行うことも可能です(もちろん、こんな書き方をするくらいならsedなどを使うべきだと思いますが)。

tmp=${path%.gif}

if [ "$path" != "$tmp" ]; then
tmp="${tmp}.jpg"
fi


先頭から特定の文字列までの削除・特定の文字列から末尾までの削除

abc=123=foo=barから=より前(abc)や=より後(bar)を取り出したいような場合も簡単に記述可能です。

正規表現
POSIX shで対応する変数展開

abc=123=foo=barに対して適用した結果

s/=.*$//
${str%%=*}
abc

s/=[^=]*$//
${str%=*}
abc=123=foo

s/^.*=//
${str##*=}
bar

s/^[^=]*=//
${str#*=}
123=foo=bar

これと同じ書き方で、basenameコマンドを${path##*/}で、dirnameコマンドを${path%/*}で代用する、などというのが定番の使い方だと言えます。


文字列先頭から特定の文字群が連続する限り取り出す

Abc123-foo:barから、先頭や最後でアルファベットが連続している部分(Abcbar)を取り出したいような場合もPOSIX shで記述可能です。

正規表現
POSIX shで対応する変数展開

Abc123-foo:barに対して適用した結果

s/^([A-Za-z]*).*$/$1/
${str%%[!A-Za-z]*}
Abc

s/^.*?([A-Za-z]*)$/$1/
${str##*[!A-Za-z]}
bar

逆にアルファベット連続だけを削除する方法はありません。不便ですね。


まとめ

今回調べてみて、POSIX shでもそれなりに文字列操作ができるんだな、という感想を持ちました。一方で、このような記法は保守性が非常に低いと個人的には感じます。実際に利用する際は補足コメントを書いておいた方が良いでしょう。

これより複雑な処理を書きたい場合はsedなどの外部コマンドを呼び出すか、ポータビリティはあきらめてbashやzshに依存したスクリプトを書くのが良さそうです。bash/zshならPOSIX正規表現(zshならPCREも使える)によるマッチングおよびキャプチャができるので、もう少し頑張れるはずです。


参考URL