トラック
/
Bash
Bash
/
シラバス
/
クォートの重要性
クォ

クォートの重要性 の Bash

{other: "%{count}個の演習"}

クォートの重要性について

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文字にマッチします。

    Advanced
    文字集合には細かい話がたくさんあります。 ここをクリックすると表示されます。
    • [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:]_]から成ります。
        • アンダースコア(8進数の137)は[[: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文字以上にマッチする
    • 正規表現:.*
    • グロブ:*
      • グロブパターン.*は、「ファイル名の1文字目が文字通りのドットで、そのあとに任意の文字が0個以上続くものにマッチする」という意味です。
  • 1文字以上にマッチする
    • 正規表現:.+
    • グロブ:?*
  • 正規表現の文字集合とグロブの文字集合は同じように動作します。
Advanced

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:]

Caution

なぜそれが問題なのでしょうか。 クォートされていないグロブパターンは、マッチするファイルの一覧に展開されてしまいます。 スクリプトを使っている人のカレントディレクトリに、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 -- マッチするファイルがないとき、パターンを何もないものに置き換えます。 デフォルトの動作では、パターンは文字列としてそのまま残ります。
GitHubで編集 リンクは新しいウィンドウまたはタブで開きます