bashの初学者が何度も耳にするアドバイスがあります。
変数はクォートしましょう!
なぜでしょうか。
変数($nameなど)を展開するとき、ほとんどの場合、その変数の_値_を1つの文字列(または数値)として扱いたいはずです。
値が意図せず2つ以上の別々の文字列として扱われてほしくはありません。
クォートは、あるテキストを1つの単語として扱うための主要な仕組みです。 クォートには主に2つの種類があります。
ダブルクォートの中では、パラメーター展開、算術展開、コマンド置換が使えます。
echo "This is bash version $BASH_VERSION"
ダブルクォートで囲んだ文字列の中にダブルクォートそのものを含めたいときは、その直前にバックスラッシュを付けます。
echo "She said \"Let me teach you about bash.\""
シングルクォートの中では、どんな展開も_行われません_。
echo 'This is bash version $BASH_VERSION'
# ...^..................................^
# prints exactly: This is bash version $BASH_VERSION
シングルクォートで囲んだ文字列の中に、シングルクォートそのものを埋め込むことは_できません_。
echo $nameの、いったい何が問題なのでしょうか。
シェルが1行のコードを解釈するとき、いくつかの種類の展開を順番に実行します。 最後の方に行われる展開のうち2つが、_単語分割_と_ファイル名展開_です。
パラメーター展開、コマンド置換、算術展開の結果のうち、_ダブルクォートの中で起きなかったもの_は、「単語分割」の対象になります。
シェルは展開された結果を、$IFSの値に含まれる文字の並びで分割します。
デフォルトでは、スペース、タブ、改行です。
シェルのループを使うと、実際の動きを確認できます。
sentence="The quick brown fox jumps."
i=0
for word in "$sentence"; do echo "$((++i)) $word"; done
# ..........^.........^
出力:
1 The quick brown fox jumps.
一方、クォートしなかった場合は次のようになります。
i=0
for word in $sentence; do echo "$((++i)) $word"; done
# .........^.........^
出力:
1 The
2 quick
3 brown
4 fox
5 jumps.
$IFSに別の値を設定した場合も見てみましょう。
csv="first,second,third"
IFS=","
for word in $csv; do echo "$word"; done
出力:
first
second
third
単語分割を無効にする方法の1つは、$IFSを空文字列に設定することです。
sentence="The quick brown fox jumps."
IFS= # or explicitly, IFS=""
i=0
for word in $sentence; do echo "$((++i)) $word"; done
# .........^.........^
出力:
1 The quick brown fox jumps.
ただし、ファイル名展開も無効にしない限り、変数をクォートせずに放置することはおすすめしません。 その方法は後ほど見ていきます。
「グロブ」パターンは、一連のファイルにマッチする簡潔なパターンを表すために使われます。
たとえば、ディレクトリ内のすべてのMarkdownファイルを*.mdでマッチできます。
グロブパターンは、Unixが生まれたときから存在しています。
グロブのワイルドカード文字は次のとおりです。
*は、任意の文字が0個以上続くものにマッチします。
?は、任意の文字1個にマッチします。
[...]は、角括弧の中に並べた文字の集合から、ちょうど1文字にマッチします。
[abc]は、a、b、cのいずれか1文字にマッチします。
^の後)に置きます。
[][]は、閉じ角括弧か開き角括弧のどちらかにマッチする文字集合です。[[\]]
[x-y]は、xからyまでの_範囲_にある1文字にマッチします。
^の後)か、閉じ角括弧の直前のどちらかに置く必要があります。
[-xy]と[xy-]はどちらも、-、x、yのいずれか1文字にマッチします。[x\-y]
[0-9A-Fa-f]は16進数の数字にマッチします。[^abc]は、a、b、cのいずれでもない1文字にマッチします。[^x-y]は、xからyまでの範囲にない1文字にマッチします。[[:character_class:]]は、指定した「character_class」に属する1文字にマッチします。
lower -- 小文字upper -- 大文字alpha -- 英字digit -- 数字alnum -- 英字と数字space -- 空白文字blank -- 水平方向の空白文字(スペースとタブ)xdigit -- 16進数の数字word -- 識別子に使える文字(英字、数字、アンダースコア)punct -- 句読点cntrl -- 制御文字graph -- 「表示可能な」文字print -- 表示可能な文字とスペース[[:alpha:]]クラスは[[:lower:][:upper:]]から成ります。[[:alnum:]]クラスは[[:alpha:][:digit:]]から成ります。[[:word:]]クラスは[[:alnum:]_]から成ります。[[:word:]]と[[:punct:]]の両方です。[[:graph:]]クラスは[[:alnum:][:punct:]]から成ります。[[:print:]]クラスは[[:graph:] ]から成ります。つまりスペース(8進数の040)だけで、他の空白文字は含みません。forループを使うと、ファイルの一覧を繰り返し処理できます。
for file in *.csv; do
do_something_with "$file"
done
変数"$file"がクォートされている点に注目してください。
グロブパターンは、名前にスペースを含むファイルを返すことがあります。
do_something_withコマンドが引数をちょうど1つ、つまりファイル名だけを受け取れるように、変数をクォートする必要があります。
グロブパターンは正規表現ではありません。
.
?
.*
*
.*は、「ファイル名の1文字目が文字通りのドットで、そのあとに任意の文字が0個以上続くものにマッチする」という意味です。.+
?*
Bashには、正規表現により近い拡張パターンが用意されています。
ファイル名展開を抑えたい場面もあります。 ユーザー入力を扱うときがその一例です。 次のコードを見てみましょう。
read -p "Enter something: " user_input
echo $user_input
ユーザーがアスタリスク(*)を入力すると、何が出力されるでしょうか。
カレントディレクトリにあるファイルの一覧です。
これは、おそらくやりたいことではないはずです。
もう1つの例は、パターンを期待するプログラムに、パターンを引数として渡す場合です。
trプログラムは、テキストを操作するのによく使われます。
変数の値を小文字にするには、次のようにします。
var="My Puppet"
echo "$var" | tr '[:upper:]' '[:lower:]'
出力:
my puppet
よくある間違いは、trの引数をクォートし忘れることです:tr [:upper:] [:lower:]
なぜそれが問題なのでしょうか。
クォートされていないグロブパターンは、マッチするファイルの一覧に展開されてしまいます。
スクリプトを使っている人のカレントディレクトリに、p、u、wという名前のファイルがあるとします。
touch p u w
var="My Puppet"
echo "$var" | tr [:upper:] [:lower:]
これはエラーになります。
クォートされていない[:upper:]パターンは、:、u、p、e、rのいずれかにマッチします。
クォートされていない[:lower:]パターンは、:、l、o、w、e、rのいずれかにマッチします。
Bashは、最初のパターンでファイルpとuを、2番目のパターンでファイルwをマッチさせ、trは次のように呼び出されます。
echo "$var" | tr p u w
そして、これはtrにとって間違った数の引数です。
setコマンドを使うと、ファイル名展開を無効にできます。
echo * # prints a list of files
set -f # disable filename expansion
echo * # prints a literal asterisk
set +f # enable filename expansion
組み込みのshoptコマンドには、ファイル名展開の動作を制御する設定がいくつかあります。
興味深いものをいくつか挙げます。
shopt -s nocaseglob -- 大文字と小文字を区別せずにマッチしますshopt -s extglob -- 拡張パターンを有効にしますshopt -s nullglob -- マッチするファイルがないとき、パターンを何もないものに置き換えます。
デフォルトの動作では、パターンは文字列としてそのまま残ります。