はじめに
プログラミング言語は、言語によって記号の意味が異なることがよくあります。クォーテーションマークはその一つです。C 言語ではシングルクォートは文字リテラル(一文字)を意味し文字列はダブルクォートします。JavaScript や Python ではシングルクォートとダブルクォートの意味は同じでエスケープシーケンスが使えます。Ruby ではシングルクォートはただの文字列('と\ を使う時だけエスケープが必要)ですがダブルクォートはバックスラッシュ記法や式展開が使えます。クォートは多くの言語で文字列を表現する時に使いますが微妙な違いがあります。シェルスクリプトも他の言語とは違う意味があります。それを正しく理解していないと大きなミスを引き起こしかねません。
前提知識 シェルスクリプトのクォートの一般的な話
特別な意味を持つ文字がなければクォートしなくてよい
シェルスクリプトの他の言語との大きな違いの一つは文字列のクォートが必須ではないという点です。これはシェル上で快適に単語を入力をできるように操作性の観点から設計した結果です。もしクォートが必須だと入力がとても面倒になってしまいます。
find /var/log -type f -name "*.log" -exec ls -l {} \;
# もしクォートが必須だったら?
find "/var/log" "-type" "f" "-name" "*.log" "-exec" "ls" "-l" "{}" ";"
# おまけ もしカッコとカンマも必須だったら?
find("/var/log", "-type", "f", "-name", "*.log", "-exec", "ls", "-l", "{}", ";")
# おまけ2 cp *.txt files の glob 展開と文字列の区別をどう表現しますか?
cp(glob("*.txt"), "files") # glob データ型の導入?
余談ですがシェルスクリプトの代わりに別の言語を使おうとか新しいライブラリが登場したりしますが、シェル(スクリプト)はこのように無駄を極限まで削ぎ落とした優れた文法を実現しています。この点において他の言語はシェルに敵いません。この文法の差に気づいてそれを超えない限り他の言語がシェルスクリプトの代わりとして置き換わることはまずないでしょう。汎用プログラミング言語用の REPL をシェルとして使う人はいないということです。
なお、クォートせずに特別な意味がある文字を使いたい場合はバックスラッシュでエスケープすると使うことができます。
echo \"\<\>\' # => "<>'
特別な意味を持つ文字があればクォートする
シェルスクリプトの文法として特別な意味を持つ文字(&、 |、 <, >, * 等)をただの文字として使う場合にはクォートが必要です。その時シングルクォートとダブルクォートで文字列中の記号の意味が変わるものがあります。これに関しては他の言語でもよくあることなのでここでつまづく人は少ないと思います。
echo "Hello World" # スペースを使いたい場合はクォートする
echo "Hello <World>" # シェルのリダイレクト記号として解釈されなようにクォートする
echo "Hello $USER" # ダブルクォートの中では変数($ は特別な意味を持つ)が使える
echo 'Hello $USER' # シングルクォートの中では $ は特別な意味を持たない
シングルクォートの場合はその中ですべての文字が特別な意味を持たなくなります。唯一の例外は文字列を終了させるシングルクォーテーションでシングルクォートの中にシングルクォーテーションを含めることはできません。もしそういうことをしたければ次のように書きます。(シェルスクリプトでは文字列と文字列をつなげる + のようなものはなく、つなげて書くことができます。)
# What’s New
echo 'What'"’"'s New' # ダブルクォーテーションで括っている ('What' "’" 's New')
echo 'What'\’'s New' # バックスペースでエスケープしている ('What' \’ 's New')
ダブルクォートでも多くの文字が特別な意味を持たなくなり $ と ` と \ と文字列終了の " だけが特別な意味を持ちます。
echo "$VAR" # 変数展開 の $
echo "$(uname -a)" # コマンド置換の $
echo "`uname -a`" # コマンド置換の `
echo " \$ \` \" \\ " # 特殊な文字を埋め込む場合のエスケープ文字の \
たまに勘違されますがダブルクォートの中で \ でエスケープできる文字は $、`、\、" の四文字だけです(それに加えて行末で \ を用いると続く改行文字が無かったことになるので改行文字もエスケープできるとみなすことができます)。それ以外の文字、つまりダブルクォートの中の \t や \n はタブや改行の意味にはなりません。あれ?でも \n で改行されてるよ?って思った方、それは echo コマンドが解釈しているだけです。
echo "foo\nbar" # zsh の echo は \n という文字列で改行する
echo 'foo\nbar' # だからシングルクォートしていても改行する
printf '%s' "foo\nbar" # foo\nbar と表示される
なお、シェルスクリプトにはシングルクォート、ダブルクォートの他に、$'...' (Dollar-Single Quotes - ドルシングルクォートってカタカナで書いてる人いなさそうだけど・・・ダラーのほうが良い?)も多くのシェルで利用可能です(POSIX への追加が検討されています)。これを使うと \t や \n などのエスケープシーケンスが使えるようになります。ただしドルシングルクォートの中で変数などを使うことはできません。バックスラッシュが使えるシングルクォートという扱いです。
printf '%s' $'foo\nbar' # \n で 改行される
printf '%s' $'foo\nbar'"$VAR" # 変数を使いたければ後ろにつなげればいいだけ
つまりシェルスクリプトには三種類のクォートがあるということです。
シングルクォート('...')
ダブルクォート("...")
ドルシングルクォート($'...') 一部のシェルを除く
ダブルクォートの有無による違い
さてここからが本題なのですが変数をクォートせずにそのまま書いた場合とダブルクォートの中に書いた場合で(特別な意味を持つ文字以外の)違いがあります。
単語分割(フィールド分割)
1つ目は単語分割(フィールド分割)です。
# 注意 zsh の場合は単語分割がデフォルトで無効になってるので
# 同じ動作にするには setopt shwordsplit で有効にする必要がある
args_length() {
echo $# # 引数の長さを出力
}
VAR=""
args_length $VAR # => 0
args_length "$VAR" # => 1
VAR="foo bar baz"
args_length $VAR # => 3
args_length "$VAR" # => 1
このような動作になる理由はシェルはコマンド・シェル関数(この例では args_length 関数)を呼び出す前に変数の中身を展開するからという説明で理解できると思います。
VAR=""
args_length # 引数がない
args_length "" # 引数がある
VAR="foo bar baz"
args_length foo bar baz # 3 つの引数に分割される
args_length "foo bar baz" # 1 つの引数
単語分割というのは、変数をダブルクォートしない場合に(特定のルールで)分割してから変数の中身を展開する機能です。この特定のルールというのは IFS 変数に含まれてる文字のいずれかで分割するというルールです。デフォルトでは IFS 変数にはスペース、タブ、改行(、zshの場合は追加で \0)が設定されているのでこれらの文字で分割して展開されます。
IFS 変数を変更することで別の文字で区切ることができます。
IFS=":"
VAR="foo:bar:baz"
args_length foo bar baz # : で分割してから変数の中身が展開される
この機能をうまく利用すると、カンマ区切りデータを分割したりすることができます。
IFS=","
VAR="foo,bar,baz"
set -- $VAR # 位置パラーメータへ分割して設定
# set -- foo bar baz
echo "1:$1 2:$2 3:$3" # => 1:foo 2:bar 3:baz
ただ単語分割と IFS 変数 を使った分割はいろいろと罠があるので注意が必要です。詳しくは「シェルスクリプトの単語分割 (IFS) は罠だらけ」を参照してください。
パス名展開
もう一つ行われる処理がパス名展開です。これは正確には「変数のダブルクォート」とは直接関係なくダブルクォートされてない特殊な文字列(glob パターン、いわゆるワイルドカード)がある場合に行われる展開で、単語分割の後のタイミングで(単語分割自体がなくても)行われる処理です。
echo * # カレントディレクトリのファイルに展開される(ドットで始まるのを除く)
echo .* # カレントディレクトリのファイルに展開される(ドットで始まるファイルのみ)
echo *.no-such-extension # => *.no-such-extension
# パターンにマッチするファイルが見つからなければ、パターン文字列そのものになるので注意
# ただし zsh のデフォルト設定や一部のシェルでは設定でエラーにすることもできる
GLOB="*"
echo $GLOB # echo * と解釈されカレントディレクトリのファイルに展開される
echo "$GLOB" # echo "*" と解釈されるので "*" が出力されるだけ
# 単語分割が行われる = ダブルクォートしてないから、必然的にパス名展開も行われる
GLOB="foo bar *"
echo $GLOB # echo foo bar * と解釈される
なおパス名展開は set -f (set -o noglob) で無効にすることができます。
set -f
GLOB="foo bar *"
echo $GLOB # => foo bar * という文字列が出力される
変数の単語分割やパス名展開を使うことはほぼない
変数に対して単語分割やパス名展開が必要になる場合はほとんどありません。カンマ区切りデータを分割したい時などまったくないわけではありませんが、必要になるケースの方が圧倒的に少なく特別な理由がなければ変数は常にダブルクォートをしておくべきです。そうしなければ次のような問題が発生してしまいます。
file="data 2021-08-06.txt" # スペースが含まれるファイル名
rm $file # rm data 2021-08-06.txt
# data と 2021-08-06.txt の 2 つのファイルを削除しようとしてしまう
# 正しくは rm "$file" => rm "data 2021-08-06.txt" であるべき
file="data[2021-12].txt" # 日付を[]で追加したファイル名
rm $file # rm data[2021-12].txt
# data1.txt というファイルにマッチするので、それが削除されてしまう
# 正しくは rm "$file" => rm "data[2021-12].txt" であるべき
コマンド置換にもダブルクォートは必要!
話を単純にするために、ここまでは変数だけに絞って話をしましたが、コマンド置換でもダブルクォートは必要です。
args_length() {
echo $# # 引数の長さを出力
}
args_length $(echo "foo bar baz") # => 3 単語分割されている
args_length "$(echo "foo bar baz")" # => 1
echo $(echo "*") # echo * が実行される
echo "$(echo "*")" # echo "*" が実行される
コマンド置換も基本的にはダブルクォートしていればよいのですが、よく見かけるダブルクォートしてはいけない例としてこのようなものがあります。以下のコードを実行すると単語分割が行われないためにループは一回しか実行されません。
for i in "$(seq 3)"; do
echo "@ $i"
done
# 実行結果(改行も含めて一つとして扱われる)
@ 1
2
3
以下のようにダブルクォーテーションを外せばよいのですが、実行するコマンドによっては意図しない単語分割やパス名展開が行われないように十分気をつける必要があります。
for i in $(seq 3); do
echo "@ $i"
done
# 実行結果
@ 1
@ 2
@ 3
ちなみに一般的には for の in でコマンド置換を使うのは避けパイプを使ったほうが良いでしょう。
seq 3 | while IFS= read -r line; do
echo "@ $line"
done
また別件ですがコマンド置換は変数に入れないとエラー処理ができないという問題があります。それについては「シェルスクリプトのコマンド置換 $(...) の出力は変数に入れないとエラー処理ができないという話」を参照してください。
位置パラメータと配列
"$@" と "${arr[@]}" の話
ここまでの話で「ダブルクォートした方が良いのは分かった。ダブルクォートしたら引数が分割されることなく一つになるんだな」と思ってしまった人、ちょっと待ってください。例外があります。それは全位置パラメータ($@)と配列の全要素(${arr[@]})です。
位置パラメータと配列もダブルクォートしなければいけないものです(ダブルクォートなしで使う例はまったくないとは言いませんが、変数の場合よりも使うことはないでしょう)。しかしこれらは一つの引数になるわけではありません。ダブルクォートしても引数の数は変わりません。
args() {
echo "length: $#"
printf '%s\n' "$@" # 引数を一行ごとに表示する
}
set -- # 位置パラメータをなしにする
args "$@" # args として実行される
# length: 0
#
set -- "foo 1" "bar 2" "baz 3"
args "$@" # args "foo 1" "bar 2" "baz 3" として実行される
# length: 3
# foo 1
# bar 2
# baz 3
最初の例の set -- と args "$@" に注意してください。args の引数はあるように書いているのに位置パラメータがなにもない場合は引数は 0 個です。また二番目の例では args "$@" の引数の一つに見えるのに、実際にシェル関数に渡される引数の数は 3 個です。このようにシェルスクリプトのダブルクォートは他の言語のように単一の文字列を作るものではなく変数の展開のルールを変えるのものなのです。
さてここで少し面白い実験をしてみましょう。$@ の前後に文字を付けて args "1$@2" と呼び出したらどうなるでしょうか?答えは以下のようになります。
set -- foo bar baz
args "1$@2" # args "1foo" "bar" "baz2" として実行される
# length: 3
# 1foo
# bar
# baz2
引数の数は変わらず $@ の前につけた文字列は最初の引数の前に $@ の後ろにつけた文字列は最後の引数の後に結合されるのです。この仕様が役に立つことは少ないのですが知っていると意外な場面で使えるかもしれません。
ここまで位置パラメータ($@)の話しかしてきませんでしたが、配列でも同じことが当てはまります。こちらも要素数は変わりません。
arr=("foo 1" "bar 2" "baz 3")
args "${arr[@]}"
# length: 3
# foo 1
# bar 2
# baz 3
arr2=("${arr[@]}") # 別の配列にコピーする際もダブルクォートは必要
ところで配列の文法をよく見ると位置パラメータは無名配列のように見えてきませんか?("${arr[@]}" → "${[@]}" → "${@}" → "$@") この関係性に気づくと配列と位置パラメータは同じような使い方ができるように設計されているということにも気づくと思います。
"$*" と "${arr[*]}" の話
"$@" と "${arr[@]}" の話をしたならば "$*" と "${arr[*]}" の話 もしなければいけないでしょう。これらも同じくダブルクォートしなければいけないものです。ダブルクォートなしで使うことが正しいという例は正直思いつきません。さて、こちらですが "$@" と違って一つの引数にするためのものです。
args() {
echo "length: $#"
printf '%s\n' "$@" # 引数を一行ごとに表示する
}
set -- # 位置パラメータをなしにする
args "$*" # args "" として実行される
# length: 1
#
set -- "foo 1" "bar 2" "baz 3"
args "$*" # args "foo 1 bar 2 baz 3" として実行される
# length: 1
# foo 1 bar 2 baz 3
この時、位置パラメータのすべては IFS 変数の最初の一文字(デフォルトではスペース)を区切り記号として結合されます。もし IFS 変数が空文字であれば区切り記号なしで結合されます。単語分割のほぼ逆の処理が行われると思って良いでしょう(単語分割では区切り文字を複数指定できるので厳密には異なる)。説明が同じになるだけなので省略しますが配列の場合("${arr[*]}")も同じように動作します。
"$@" と "$*" の使い分け
"$@"("${arr[@]}")と "$*"("${arr[*]}")の使い分けですが、複数の引数(要素)として扱いたい場合は前者(こちらが一般的)を使い、一つの引数(要素)に結合したいなら後者を使います。またどちらもダブルクォートしなければいけません。もししなければ両方とも単語分割とパス名展開が行われてしまいます(結果として $@ も $* も同じ意味になります)。位置パラメータまたは配列で単語分割やパス名展開をしたいということはあまり考えられないのでダブルクォートしない使い方は忘れていいと思います。
ダブルクォートしなくても良い箇所
実はダブルクォートしなくても単語分割やパス名展開が行われない箇所がいくつかあります。基本的に変数はどんな場合でもダブルクォートしていればよいのでこの項目の内容は雑学程度に思ってください。(と言いつつ個人的には省けるものは省きたいスタイルなので私はこれらの箇所でダブルクォートしていません。)
var=$VAR # 変数代入
var=$(echo "foo bar") # コマンド置換結果の変数代入も OK
# case の対象は問題なし
case $var in
...
esac
# [[ ]] は OK。ただし [ ] はだめ
# この違いは [[ ]] はコマンドではなくシェルの予約語だから
if [[ $var = "test" ]]; then
...
fi
上記の変数代入と似たような形で export var="$VAR" 等のコマンドを使った変数代入があるのですが、こちらに関しては(現状は)ダブルクォートしなければいけません。なぜ "現状は" と書いたかというと、次期 POSIX の Issue 8 ではこのケースで単語分割やパス名展開が行われないと規定されるからです(詳細は「次期POSIXシェル仕様の「宣言ユーティリティ」とシェルスクリプトの互換性問題」参照し)。とは言っても特定のシェル限定で開発するならともなく、汎用的なシェルスクリプトの場合は古いシェルへの対応をすぐに打ち切ることはできないと思うのでしばらくはダブルクォートしなければいけません。
ところで [ ] ではなく [[ ]] を使うべき理由の一つに「ダブルクォートしなくていいから」というのがあるようなんですが、ほとんどの場所でダブルクォートが必要なのだから逆に常に使っていた方が間違わなくて良いと思うのは私だけでしょうか?[[ ]]の変数をダブルクォートしない人は、変数代入や case でもダブルクォートしないんですよね?(私と同じですね!w)
ShellCheck を使おう!
さて変数は必要ない限りダブルクォートするという方針にするとして、ミスしないように気を張り詰めて書くというのは無駄な労力です。コンピュータがやってくれることはコンピューターに任せましょう。ということで ShellCheck を導入してください。ダブルクォートが必要な場所を教えてくれますし、スペースが必要な場所や必要ない場所も教えてくれます。この件に限らずシェルスクリプト以外でも同じですが生産性とコードの品質を考えてるなら lint ツールを使わないなんてありえません。
まとめ
シェルスクリプトはただの文字列はクォートしなくても良い
変数を使う場合は単語分割やパス名展開が行われないように必ずダブルクォートする
もし本当に単語分割やパス名展開が必要なら理解してからダブルクォートを外す
そんなことよりさっさと ShellCheck を導入しよう!
↧