理系学生日記

おまえはいつまで学生気分なのか

BashのGlobは積極的に利用しましょう

bash には glob というものがあります。glob ってなによっていう人も ls *.sh とかを展開する bash の機能ですよっていうと分かるかと思います。 この glob の機能って多用されますがあまりマニュアルとか読んだ人もいないと思うので、簡単にまとめてみます。

1. Pattern Matching

Glob のパターンマッチングに使用できる文字のパターンっていうのは、通常、次の文字です。

  • *: 何にでもマッチする
  • ?: 任意の一文字にマッチする
  • [...]: [] の間に記述された任意の文字にマッチする。これはちょっとややこしいので、もうちょっとまとめます。

[...]

[...]... には通常として文字の集合を指定できますが、多少表現力のあるものも指定できます。 次のように、ハイフン- で指定された範囲のみにマッチさせたりできますし、

$ ls test[1-4].txt
test1.txt  test2.txt  test3.txt  test4.txt

[ の直後に ! あるいは ^ を指定することで、文字集合を除外させることもできます。

$ ls test[!1-4].txt
test5.txt  test6.txt  test7.txt  test8.txt  test9.txt
$ ls test[^1-4].txt
test5.txt  test6.txt  test7.txt  test8.txt  test9.txt

[...] の中には、文字クラス名を [:class:] の形で指定できます。[:alnum:] とか [:punct:] とかですね。

$ ls test[[:digit:]].txt

2. Glob の拡張表現

だいたい *.txt とかで用が足りるときは多いのですが、「このファイルとはマッチさせたくない」(not 条件) とか、正規表現を使いたいとか、そういう要望はデフォルトの Pattern Matching 用の文字では満たせません。このままでは、毎日歯軋りをしながら眠りにつくしかない。 Bash にはこの Pattern Matching の機能を、以下のようにして拡張することができます。

$ setopt -s extglob

これにより、正規表現の表現力や not 表現が使えるようになります。

  • ?(pattern-list): pattern-list に記述されたパターン 0 回または 1 回の出現とマッチ。
  • *(pattern-list): pattern-list に記述されたパターンの 0 回以上の出現とマッチ。
  • *(pattern-list): pattern-list に記述されたパターンの 1 回以上の出現とマッチ。
  • @(pattern-list): pattern-list に記述されたパターンの 1 回の出現とマッチ。
  • !(pattern-list): pattern-list に記述されたパターンのいずれでもないものとマッチ。
$ shopt -s extglob
$ ls test+([[:digit:]]).txt
test1.txt  test10.txt  test2.txt  test3.txt  test4.txt  test5.txt  test6.txt  test7.txt  test8.txt  test9.txt

$ ls test!(1|2|5|8|9).txt
test10.txt  test3.txt  test4.txt  test6.txt  test7.txt

特に、not 表現が記述できるようになるのは大きい。 例えば、"カレントディレクトリの hoge.sh 以外のスクリプト" は次のようにして取得できます。

$ ls !(hoge).sh

3. ls | grep パターンには気をつけよう

シェルスクリプトの中に ls | grep パターンがあるときは、glob に置き換えられないかを検討する価値があります。 ls | grep パターンが良くないケースとしては、ファイル名にスペース等が入っているような場合が挙げられます。

たとえば、hello world.txt hello-world.txt が存在するディレクトリがあるとしましょう。

$ ls
'hello world.txt'  hello-world.txt

このディレクトリに対して、以下のスクリプトを実行してみます。

#!/bin/bash
for f in $(ls | grep \\.txt$); do
    echo $f
done

結果はこちらで、"hello world.txt" が 2 行に分割されてしまっていることがわかります。

hello
world.txt
hello-world.txt

ls | grep の結果がスペースで分割されてしまってるわけですね。 このような問題は、Glob にすることで解決します。

for f in *.txt; do
    echo $f
done

# 実行すると:
hello world.txt
hello-world.txt

hoge.txt 以外を処理したいのであれば、拡張 Glob を使用して !(hoge).txt にすれば良いですし、Glob の使いどころは非常に多いと思います。