シェルスクリプトで文字列の途中に改行を入れたい場合、echo -e "a\nb"と書く方法がある。しかしechoの-eは環境によって解釈が異なるため、移植性を求めるならprintfを使う。
echo -e で改行を出力する
bashの組み込みechoは、-eを付けるとバックスラッシュのエスケープシーケンスを解釈する。
$ echo -e "a\nb"
a
b
-eを付けない場合、\nは文字列としてそのまま出力される。
$ echo "a\nb"
a\nb
echo -e の挙動は環境によって異なる
-eはPOSIXで規定されておらず、シェルやechoの実装ごとに扱いが変わる。
Ubuntuの/bin/shはdashである。dashのechoは-eをオプションとして認識せず、そのまま文字列として出力する。一方でエスケープシーケンスは常に解釈する。
$ sh -c 'echo -e "a\nb"'
-e a
b
macOSの/bin/echoはBSD由来の実装で、-eを文字列として出力し、エスケープシーケンスも解釈しない。
$ /bin/echo -e "a\nb"
-e a\nb
macOSの標準シェルであるzshの組み込みechoは、-eを付けなくてもエスケープシーケンスを解釈する。
$ zsh -c 'echo "a\nb"'
a
b
bashでもxpg_echoが有効な場合は-eなしでエスケープシーケンスを解釈する。
$ bash -c 'shopt -s xpg_echo; echo "a\nb"'
a
b
同じecho "a\nb"が、bashではa\nb、zshでは2行の出力になる。手元の環境で意図どおり動いても、CIやコンテナ内の/bin/shでは結果が変わる。
printf を使う
printfはPOSIXで挙動が規定されており、シェルによらずエスケープシーケンスを解釈する。POSIXもechoの代わりにprintfを使うよう新規のアプリケーションに推奨している。
$ printf "a\nb\n"
a
b
printfはechoと違って末尾に改行を付けない。行末で改行したい場合はフォーマット文字列に\nを明記する。
改行なしで出力したい場合は\nを書かなければよい。echo -nに相当するが、-nと違って移植性の問題がない。
$ printf "abc"; echo "|"
abc|
変数はフォーマット文字列に直接渡さない
printfの第1引数はフォーマット文字列として解釈される。変数を直接渡すと、値に含まれる%が変換指定子として扱われ、出力が壊れる。
$ V="100%"
$ printf "$V\n"
100bash: printf: `\': invalid format character
フォーマット文字列は固定の%s\nとし、変数は引数として渡す。
$ V="100%"
$ printf "%s\n" "$V"
100%
変数の中の\nをエスケープシーケンスとして解釈したい場合は%bを使う。
$ V="a\nb"
$ printf "%b\n" "$V"
a
b
引数が余るとフォーマットが繰り返される
printfは変換指定子より引数が多い場合、フォーマット文字列を先頭から繰り返し適用する。1行1要素の出力に利用できる。
$ printf "%s\n" a b c
a
b
c
引数が足りない部分は空文字として扱われる。
$ printf "%s-%s\n" a b c
a-b
c-
配列を1行ずつ出力する用途にも使える。
$ ARR=(a b c)
$ printf "%s\n" "${ARR[@]}"
a
b
c
使い分け
| 用途 | 書き方 |
|---|---|
| 単純な1行出力 | echo "text" |
| エスケープシーケンスを含む出力 | printf "a\nb\n" |
| 末尾の改行を付けない出力 | printf "%s" "$V" |
| 変数の内容をそのまま出力 | printf "%s\n" "$V" |
| 配列やリストを1行ずつ出力 | printf "%s\n" "${ARR[@]}" |
エスケープシーケンスを含まない単純な文字列であればechoで問題ない。改行やタブを制御する場合、および変数の値を出力する場合はprintfを使うと環境差の影響を避けられる。
