shとbashでの変数内の文字列置換など

シェルスクリプトで文字列を置換したい際、sedを使う手法が紹介されることが多い。が、実はsedなどの外部コマンドを使わなくても、以下のように変数展開をすることでシェル内部で文字列置換をすることができる。

${変数名#パターン} → 前方一致でのマッチ部分削除(最短マッチ)
${変数名##パターン} → 前方一致でのマッチ部分削除(最長マッチ)
${変数名%パターン} → 後方一致でのマッチ部分削除(最短マッチ)
${変数名%%パターン} → 後方一致でのマッチ部分削除(最長マッチ)
${変数名/置換前文字列/置換後文字列} → 文字列置換(最初にマッチしたもののみ)
${変数名//置換前文字列/置換後文字列} → 文字列置換(マッチしたものすべて)

この機能は記号で書かれるため非常にググりにくいことと、素のshでできること・bashでしかできないことが混在して書かれた記述も多いため、あまりネット上にこれという情報が無いようだ。

というわけで、一度ここで整理してみることにする。

${変数名#パターン} 前方一致でのマッチ部分削除

これは素の/bin/shでも使える書き方。#以降に記述したパターンを用いて変数内の文字列を前方一致でマッチし、マッチした部分を削除した文字列を返す。マッチは最短マッチとなるため、最長マッチ(欲張りマッチ)したい場合はシャープを2個書いて ${変数名##パターン} とすれば良い。

と、こうして文章で書いても意味分からんと思うので、以下の例を見た方が早いだろう。

#!/bin/sh

var="/my/path/dir/test.dat"
echo ${var#*/}
echo ${var##*/}

こういうシェルスクリプトを書いて実行すると、結果は以下のようになる。先ほど書いた通りこれはbashでなくてshの機能なので、FreeBSD/bin/shでも動くことを確認している(もちろんbashでも動くけど)。

% ./var.sh
my/path/dir/test.dat
test.dat

この例では、パターンマッチとして */ を指定している。これはファイルのフルパスを与えた際に、ファイル名だけを取り出すのによく使われる手法である。つまり、「任意の文字列の最後がスラッシュで終わるもの」を最長マッチ(##と、シャープを2コ)して削除すれば、ファイル名だけが残るという仕組みだ。

この場合は、ひとつめの ${var#*/} の最短マッチでは一番最初のスラッシュだけ除かれてしまうので、このタイプでの実用性は無いだろう。参考に書いておいただけである。また、既にお気づきだろうが、この用途ならば現代のLinuxならばbasenameコマンドを使えば同じようなことはできる。

${変数名%パターン} 後方一致でのマッチ部分削除

シャープ(#)をパーセント(%)に置き換えると、マッチを後方一致でおこなうことができる。シャープ(#)と同様、パーセント(%)を二つ並べて%%にすると最長マッチ(欲張りマッチ)する。

これも文章で書くとよく分からんだろうから、さっさと例を出そう。

#!/bin/sh

var="/my/path/dir/test.20130930.dat"
echo ${var%.*}
echo ${var%%.*}

これを実行すると以下のようになる。

$ ./var.sh
/my/path/dir/test.20130930
/my/path/dir/test

この手法は、ファイル名から任意の拡張子を除くために使われることがある。例えばCentOSなら、/etc/init.d/netwowk内にこの書き方を見ることができる(ので、お手元のLinuxを参照してみるといいだろう)。

なお拡張子を除く例では、ドットが複数あることを想定して、最短マッチする%で書くことが多い。

なおこの例も、現代のLinuxならばdirnameコマンドを使えば同じ結果を得ることができる。ただ、外部コマンドを使いたくないシステム寄りのシェルスクリプトは、今もこの手法で書かれることが多いので、知識として知っておいた方がいいだろう。

${変数名/置換前文字列/置換後文字列} 置換する

この記法はbashでしか使えないので、FreeBSD/bin/shではエラーになる。sedと同じ感じで、先に置換したいパターンを書き、スラッシュの後に置換後文字列を書くことで置換できる。以下のような例になるだろう。

#!/bin/bash

var="abcdef abcdef xyz"
echo ${var/abc/XXX}
echo ${var//abc/XXX}

これを実行すると以下のようになる。

$ ./var.sh
XXXdef abcdef xyz
XXXdef XXXdef xyz

置換する際、変数直後のスラッシュを一つだけにすると最初に一致したものしか置換しない。sedで指定するところの/gオプション、つまりマッチしたもの全部を置換するにはスラッシュを2つ書いて ${変数名//置換前文字列/置換後文字列} とすればよい。