文字列やコマンドをインデントしながら出力したい
POSIX sh
zsh
やbash
などのシェルで、変数に代入したコマンドの実行結果をecho
する際にインデントしたい。
bash
なら以下のようなことが、sh
でもしたい。
indent() { sed 's/^/ /'; }
echo "hoge fuga"
echo "foo bar" | indent
# Output:
# hoge fuga
# foo bar
気付けば簡単なことだったのですが、Qiita 記事に絞って「bash
文字列
インデント
」でググってもドンピシャの方法が出てこなかったので、未来の自分のための外付け記憶の神殿として。
-
複数行を同じ行(1行内)に表示させたい場合はこちら:
-
単純に POSIX でタブを出力したい(
echo -e
が使えない)場合はこちら:printfでタブなどのエスケープ文字が使えるprintf 'foo\tbar\n'
TL; DR
【ポイント】
IFS=
の設定と-r
オプション
echo 'カレント・ディレクトリのファイル一覧'
indent=' '
ls -lah . |
while IFS= read -r line; do
echo "${indent}${line}"
done
echo
- オンラインで動作をみる @ paiza.IO
TS; DR
自作のセットアップスクリプトなどで、apt install
といった外部コマンドを実行する際に、その出力結果をインデントさせたかったのです。
ほら、CI や Docker のビルド時に「あれ?これは俺様スクリプトの出力?それとも apt
の方?」とか悩んだ時にインデントされていると目視しやすいじゃないですか。あれです。
頑張るも失敗した
最初は以下のように echo
時、パイプ渡しで sed
を使って行頭にタブやスペースを挿入する方法を最初考えていました。
echo "${RESULT}" | sed "s/^/\t/g" # ^ にマッチしたら "\t" に置き換える
echo "${RESULT}" | sed "s/^/ /g" # ^ にマッチしたら " " に置き換える
以下のような感じで、install_dependencies
の後にパッケージを指定すると小綺麗に出力させていました。
#!/usr/bin/env bash
function echo_indent () {
MSG="${1}"
echo -e "${MSG}\n" | sed s/^/$'\t'/g
}
function install_dependencies () {
NAME_PACKAGE=$1
echo -n "Installing ${NAME_PACKAGE} ... "
# 実行結果を変数に代入
RESULT=`sudo apt -y install ${NAME_PACKAGE} 2>&1`
if [ $? -gt 0 ]; then
echo 'NG'
echo "Error occured while installing ${NAME_PACKAGE}."
echo "Called from line:${BASH_LINENO[0]}"
# 取得したエラー出力をインデントする
echo_indent "${RESULT}"
return ${BASH_LINENO[0]}
fi
echo 'OK'
return 0
}
install_dependencies libusb-1.0-0
install_dependencies xlibusb-1.0-0 #わざと間違えてみる
$ ./sample.sh
Installing libusb-1.0-0 ... OK
Installing xlibusb-1.0-0 ... NG
Error occured while installing xlibusb-1.0-0. (Called from line:22)
WARNING: apt does not have a stable CLI interface yet. Use with caution in scripts.
パッケージリストを読み込んでいます...
依存関係ツリーを作成しています...
状態情報を読み取っています...
E: パッケージ xlibusb-1.0-0 が見つかりません
E: 正規表現 'xlibusb-1.0-0' ではパッケージは見つかりませんでした
- オンラインで動作確認 @ paiza.IO
- GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)
シンプルで力強く
しかし、自由度も足りなかったので、むしろ出力を while
read
で各行読み込み、インデントを各行の頭に加える方法がメンテナンス性やカスタム性が高まりました。
#!/bin/sh
# 関数化
indentStdin() {
indent=' '
while read line; do
echo "${indent}${line}"
done
echo
}
バックスラッシュ対策
シェルの read
コマンドは、デフォルトでバックスラッシュを解釈(展開)してしまいます。つまり、"\/hoge
" は "/hoge
" と変換されて渡されてしまうのです。「そのまま」渡したい場合には -r
オプションを指定します。
- SC2162 "read without -r will mangle backslashes" | ShellCheck @ GitHub
#!/bin/sh
# 関数化
indentStdin() {
indent=' '
while read -r line; do
echo "${indent}${line}"
done
echo
}
行頭のスペースやタブ文字の消滅対策
渡された文字の頭にタブやスペースが入っているとトリムされてしまう(消える)現象が発生しました。
ずいぶん悩んだのですが、以下の記事が大変参考になりました。
-
インデントされたファイルを
while read
コマンドでループさせるとインデントが消えてしまう問題に対処 @ ゲンゾウ用ポストイット
これは read
コマンドが「スペース」「タブ」「改行」を1行の区切りの文字として認識してしまうことが原因でした。
つまり、read
コマンドは区切り文字を検知すると、それ以降の区切り文字までを1行データとして渡します。そのため、行頭のスペースやタブも「区切り」的な扱いになってしまうので消えてしまうのです。
そして、それらの区切り文字は環境変数の IFS
に定義されています。
$ # よくわからない。空に見える。
$ echo -n "$IFS"
$ # 3文字セットされているみたい
$ echo -n "$IFS" | wc -c
3
$ # od でバイナリをチェックしてみる
$ echo -n "$IFS" | od -t x1
0000000 20 09 0a
0000003
$ # ASCII名で見てみる
$ echo -n "$IFS" | od -t a
0000000 sp ht nl
0000003
$ # キャラクターセットでみてみる
$ echo -n "$IFS" | od -t c
0000000 \t \n
0000003
回避策としては read
コマンドの直前に IFS
変数を空で定義(IFS=
)して環境変数の値を一時的に乗っ取るのです。具体的には IFS= read
とします。
#!/bin/sh
# 関数化
indentStdin() {
indent=' '
while IFS= read -r line; do
echo "${indent}${line}"
done
echo
}
# サンプル
echo 'カレント・ディレクトリのファイル一覧'
ls -lah | indentStdin
- オンラインで動作をみる @ paiza.IO
- GNU bash, バージョン 4.3.30(1)-release (arm-unknown-linux-gnueabihf)、Raspbian GNU/Linux 8 (jessie)