続・玩式草子 ―戯れせんとや生まれけん―

第42回bashの便利な機能

ここ数年、再利用するかも知れないコードは可読性がいいPythonで書くことが多かったものの、最近「スクリプトを書くほどでもないなぁ」的な作業が頻発し、bashのワンライナーで誤魔化してみたところ、最近のbashは変数展開や置換機能がずいぶん充実していて、かつてはsedやtr、awkを使って書いていた処理が、ほぼbashの機能だけで書けるようになっていました。

そこで改めてbashのマニュアル等を確認したら、⁠へー、こんなこともできるんだ」的な機能があれこれ目について、ちょっとbashに対する認識を改めているところです。今回は、そのようなbashの便利な機能をいくつか紹介してみようと思います。

bashの来歴

Linux系ディストリビューションのデフォルトのシェルであるbashは、GNUプロジェクトが開発したシェルで、"Bourne Again SH"の略になっています。

この"Bourne Again"というのは、"Born Again(再生)"との掛け言葉になっていて、もともとSystemV系UNIXの標準シェルだった"Bourne Shellbsh"を、GNUプロジェクトで作り直した、という意味を込めています。

UNIXの世界では、⁠シェル(shell⁠⁠」は本来の「貝殻」の意のごとく、システムをその内側に隠してユーザと直接やりとりするコマンドを意味し、UNIXというOSが生まれた当初から用意されていました。

最初のシェルは、UNIXの生みの親であるケン・トンプソン(Ken Thompson)自身が書いたshで、1975年に公開されたUNIX Version6のころまで使われていました。その後、ケン・トンプソンらと共にUNIXの開発に従事していたステファン・ボーン(Stephan Bourne)が、トンプソンのshにスクリプト機能などを追加したシェル(Bourne shell:bsh)を開発し、1979年のUNIX Version7から標準のシェルとなりました。

一方このころ、UNIXの可能性に興味を持ったカリフォルニア大学バークレー校のCSRG(Computer Science Research Group)が、UNIXを魔改造(笑)して高速なファイルシステムやネットワーク機能、仮想記憶機能など追加したBSD UNIXを開発しました。このBSD UNIX用にビル・ジョイ(Bill Joy)が開発したのがcshで、コマンドの履歴を遡ったり、過去のコマンドを編集する機能が追加され、対話的な操作がより簡単になると共に、"ctrl-Z"でジョブをサスペンドしてバックグラウンドに移すといったジョブ制御機能が追加されました。また、"if"を"fi"で閉じるなどかなりクセが強いshスクリプトの文法を多少ともC言語風にする等の変更が加えられました。

1980年代になると、自由にコピーや改造ができるフリーソフトウェア(free software)でUNIXの上位互換OSを作ろうとするGNUプロジェクトが始動し、そのプロジェクト用のシェルとして開発されたのがbashです。"bash"は、"Bourne Shell(bsh)"の機能やスクリプトの文法を踏襲しつつ、cshの持つ便利な対話的機能も採用し、⁠bshを新たに蘇えらせる」という思いを込めてBourne Again SHellと名付けられました。

bashの最初のバージョンが公開されたのが1989年、その後も開発は続き、現在の最新版は5.1.16になっています。残念ながら手元で利用しているPlamo Linuxのbashはすこし古い4.4系のままのため、今回紹介する機能も一世代前の4.4系で確認した内容になります。もっとも、今回紹介するような機能はさらに古いバージョンのころに採用されたものなので、最近のbashでは問題なく動作すると思います。

bashの便利機能

さて、前置きがずいぶん長くなりましたが、今回は筆者が最近便利に感じているbashの機能のうち、コマンドラインの反復展開や変数の置換、削除回りを紹介しましょう。

{ } による反復展開

入力文中の中カッコ("{ }")内にコンマ(",")区切りで要素を列挙すれば、コマンドラインの解析時にその要素数だけ反復、展開してくれます。これはブレース(brace)展開と呼ばれる機能です。

$ echo {a,b}.txt
a.txt b.txt

"{…}"内には2つ以上の要素を指定することも可能で、"{…}"が複数あれば組み合わせ総当たり的に展開されます。

$ echo {a,b,c}-{01,02,03}.txt
a-01.txt a-02.txt a-03.txt b-01.txt b-02.txt b-03.txt c-01.txt c-02.txt c-03.txt

コンマ前後に要素を指定しないことも可能で、要素を指定しなければ無いものとして展開するので、バックアップ用にファイル名を変更する場合はこういう指定ができます。

$ ls
log.dat
$ mv -v log.dat{,.old}
名前変更: 'log.dat' -> 'log.dat.old'
$ ls
log.dat.old

もちろん、"{…}"がパス等の文字列中にあっても大丈夫です。

$ echo /{,old}home/kojima
/home/kojima /oldhome/kojima

要素に数字やアルファベットを指定する場合、"〈start〉..〈end〉"の指定で連番と見なされます。

$ echo {1..10}.txt
1.txt 2.txt 3.txt 4.txt 5.txt 6.txt 7.txt 8.txt 9.txt 10.txt
$ echo {a..l}.txt
a.txt b.txt c.txt d.txt e.txt f.txt g.txt h.txt i.txt j.txt k.txt l.txt

さらに"..〈interval〉"を追加すれば、連番の2つ跳びとか5つ跳びも指定できます。

$ echo {1..10..2}.txt
1.txt 3.txt 5.txt 7.txt 9.txt
$ echo {a..z..5}.txt
a.txt f.txt k.txt p.txt u.txt z.txt

指定範囲が連番として解釈できない場合は、展開せずにそのまま表示されるようです。

$ echo {a..9}.txt
{a..9}.txt

従来、この手の処理には外部の"seq"コマンドを利用したものの、ブレース展開を使えばbashだけで間に合いそうです。

変数の置換、一部削除

画像ファイルをまとめて形式変換したりリネームする際、従来はこんな風に"sed"などを使って新しい名前を生成していました。

$ ls
CIMG3424.JPG  CIMG3425.JPG  CIMG3426.JPG 
$ for i in *JPG ; do
  > new=`echo $i | sed "s/JPG/pnm/"`
  > jpegtopnm $i > $new
  > done

一方最近のbashでは変数の置換機能があるので、これだけで済みます。

$ for i in *JPG ; do
  > jpegtopnm $i > ${i/JPG/pnm}
  > done

2行目の${i/JPG/pnm}「$iという変数の中の"JPG"を"pnm"に変更する」という指定です。もう少し汎用的に書くと${parameter/pattern/string}という指定になり、"parameter"中の"pattern"を"string"に変換する、という指定になります。

また、変数の一部を削除するような機能もあります。従来、メールアドレスからログイン名やドメイン名を取り出すには、"cut"や"gawk"を使っていましたが、最近のbashではこう書けます。

$ [email protected]
$ echo $addr
[email protected]
$ echo ${addr%@*}
kojima
$ echo ${addr#*@}
linet.gr.jp

前者の${addr%@*}の指定は、変数$addr(=kojima@linet.gr.jp)から後方一致("%")「@以下の文字を削除する⁠⁠、という指定になります。"@*"はワイルドカードを用いた指定で「@に続く全ての文字」を意味します。一方、後者の${addr#*@}は、前方一致("#")「@より前の文字を削除する」という指定です。"*@"は前者と同様ワイルドカードを用いた指定で「@より前の全ての文字」を意味します。

もちろん、ワイルドカードを用いず、${addr%.jp}(".jp"のみ削除)とか${addr#ko}(先頭の"ko"を削除)のような指定も可能です。

この後方一致(%⁠⁠、前方一致(#)には「同じパターンが繰り返す限り」を意味する最長指定もあって、"%%"とすれば後方一致で最長、"##"とすれば前方一致で最長、を意味します。例えば絶対パスからファイル名やディレクトリ名を取り出す場合、従来は"basename"や"dirname"コマンドを使っていましたが、最近のbashではこういう風に書けます。

$ path=/usr/bin/file
$ echo ${path##*/}
  file
$ echo ${path%/*}
  /usr/bin
$ echo ${path%%/*}

${path##*/}は前方最長一致で、文字列の最初から"*/"(何らかの文字列の最後に"/"が出現するパターン)が続くまでを削除します。${path%/*}は、後方から最初の"/"までを削除します。${path%%/*}は"/*"の最長指定のため、"/usr"までマッチして全ての文字列が削除されてしまいました。

ちなみに削除する文字列の指定に使えるのはワイルドカード("*")のみで、正規表現的なパターンマッチは利用できません。

算術式の評価

bash/bshでは変数は全て文字列なので、変数を直接足し合わせたりすることはできません。

$ a=5
$ b=3
$ echo $a + $b
5 + 3
$ echo "$a + $b"
5 + 3

そのため算術演算したい場合は外部コマンドである"expr"を利用していました。

$ echo `expr $a + $b`
8
$ echo `expr $a * $b `
expr: 構文エラー: 予期しない引数 `file 01.txt.old'
$ echo `expr $a \* $b `
15

なお後者はよくやるミスで、"*"はそのままだとファイル名選択用のワイルドカードと解釈されてしまうため、乗算演算子として"expr"コマンドに渡すには"\"でエスケープする必要があります。

一方、最近のbashでは算術展開(arithmetric expansion)という機能があって、"$(( ))" 内で直接計算式を記述できます。

$ echo $(($a + $b))
8
$ echo $(($a * $b))
15

この機能を使えば"expr"コマンドも必要なくなりそうです。

bshの仕様でシェルスクリプトを学んだ人間にとって中括弧({…})にはそれほど重要な意味はなく、${var}は$varと同じで、誤読しやすい変数名を明示する際に使う、という意識しかありませんでした。一方、bashの開発者たちは、bshとの互換性を維持しつつ新たな機能を追加するために、重視されていなかった中括弧に目を付け、そこに様々な意味を追加していったようです。そのような視点からbashのマニュアル等を読んでいくと「なるほど、その手があったか」と感心することしきりです。


今回、⁠へー、こんなこともできるんだ」と思ったbashの機能をいくつか紹介してみました。見てきたように、これらの機能を使えばbshでは必須だった外部コマンドがほとんど不要になり、bashだけでかなり高機能なスクリプトを書けそうです。

しかしながらPlamo Linuxの場合、パッケージのインストール時に使うのはbshと同等の機能しか持たないbusyboxの"ash"なので、ヘタにbashの拡張機能に慣れてしまうとパッケージ作成時に思わぬミスをしでかしそうです。また、この手の凝った表記はしばらく使わないとすぐに忘れてしまうので、半年前に自分で書いたコードを眺めて首をひねっている、将来の自分の姿が目に浮びます(苦笑⁠⁠。

まぁ、このあたりのバランスは難しいものの、せっかく用意されている機能を使わないのも勿体ないので、老犬も少しずつ新しい芸を覚えていくことにしましょう。

おすすめ記事

記事・ニュース一覧