シェルスクリプトで文字列の途中に改行を入れたい場合、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

printfechoと違って末尾に改行を付けない。行末で改行したい場合はフォーマット文字列に\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を使うと環境差の影響を避けられる。

参考