Bashのデバッグ
(wizardzines.com)- Bashスクリプトが想定と違う動きをするときは、実行されるコマンドをそのまま見る方法だけでも原因を追跡しやすくなる
set -xは各行を変数展開後に出力し、スクリプトが実際にどのコマンドを実行しているか確認できるようにする- コマンドラインで
bash -x script.shとして実行すると、script.shの先頭にset -xを入れたのと同じ効果がある DEBUGトラップとreadを併用すると、各行の実行前に停止して、ファイル名、行番号、次のコマンドを確認できるdie() { echo $1 >&2; exit 1; }関数は、失敗したコマンドの後に付けて、標準エラーにメッセージを出力して終了する流れをシンプルにする
実行フローを目で確認する
set -xはスクリプトが実行する行を出力し、変数は展開後の値で表示する- スクリプトの先頭に
set -xを入れて使える - 同じ動作はコマンドラインからも実行できる
$ bash -x script.sh- これは
script.shの先頭にset -xを入れるのと同じ
行ごとに止めて確認する
DEBUGトラップは各コード行が実行される前に動作する- スクリプトの冒頭に以下のコードを入れると、次のコマンドを実行する前にEnter入力を待つ
trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUGread -pはメッセージを出力し、Enter入力を待つ$BASH_SOURCEはスクリプトファイル名$LINENOは行番号$BASH_COMMANDは次に実行されるコマンド
失敗時にメッセージを残して終了する
die関数は、コマンド失敗時にメッセージを出力してプログラムを終了するために使えるdie() { echo $1 >&2; exit 1; }- 失敗する可能性のあるコマンドの後に
some_command || die "oh no!"のように付ければよい
- この関数はメッセージを標準エラーへ送り、
exit 1で終了する
1件のコメント
Hacker News の意見
コードのあちこちに
zdebugロギング関数が入っている: https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...デバッグロギングを有効にした状態でメインメニューから Ctrl-T を押すと、このような画面が出る: https://i.imgur.com/Ge75zkP.png
さらに https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme... で有効にできるフレームグラフ・プロファイリングもあり、シリアルポートにダンプしたデータを再構成すると https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master... のようなグラフを作れる
Bash は意外と柔軟
set -xを使うときは、PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: 'がとても便利こうするとファイル名、関数名、行番号が表示されるので、大きな Bash スクリプトをデバッグするときにかなり役立つ
shellcheckもおすすめ。問題を直接見つけられなくても、潜在的な問題を教えてくれるそれから、スクリプトを別の言語で書き直すこともおすすめする。会社では Bash スクリプトを Rust に置き換えているが、導入コストは高いものの、結果としてできるコードはずっと保守しやすく信頼性も高い
Bash は手早いスクリプトには今でも向いているが、100行くらいを超えたら、より強い保証を与えてくれる言語を使う価値がある
die()はエラーメッセージを標準エラーに出力してから、指定したエラーコードで終了するヘルパー関数さらに多くのシェルスクリプト終了コードとヘルパー関数はここにある: https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
ただ、その
dieの考え方には少し物足りなさがある。die関数は基本的に失敗したコマンドの終了コードを伝え、そのコマンドのエラー出力も隠すべきではない大きなスクリプトでコマンド失敗に自分で意味を持たせたいなら、もっと特化した
dieを別に使うだろう。自分のdieはおおよそ__errex "$?" "${LINENO}" "$0"の形で、致命的エラー、行番号、スクリプト名、メッセージを出力し、その終了コードで終わる実装例のひとつはここにある: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
some-command || fail "message"のように使うと、some-commandが 0 以外の終了ステータスを返したときにスタックトレースを作成してシェルを終了する関数内でスタックトレースを作成して返したいなら、
some-command || softfail "message" || return $?のように書ける必要なことはできるが不格好で、構文もひどい。スクリプトがある程度の大きさや複雑さに達すると、まともな言語へ移るよう強制してくるので、もしかすると意図された設計なのかもしれないと思う
現代のディストリビューションなら、たいていかなり新しい Bash が入っているし、配列のようなものを使おうとしているのでなければ、バージョンもそれほど気にしなくてよい
Bash の魅力は、他の言語やツールの間で占めている位置にある。他のツールをつなぎ合わせるのに理想的で、OS に十分近いので便利だし、Python のようにライブラリのインストールを要求することもない
より複雑なスクリプティングは Python のような言語に移せとよく言われるが、長期的には助けにならないかもしれない複雑さの層が追加される。20年前に書いた Bash スクリプトは今でも問題なく動くが、20年前の Python プログラムはバージョン問題に遭遇している可能性が高い
Plan 9 の
rcはもう少しきれいだが、「似ているけれどよりきれいなもの」だけでは誰も乗り換えない。今すぐ https://pkgsrc.se/shells から似ているがより良いものをインストールできるのに使っていないし、他人にとっても実行方法は変わらない定着した技術を置き換えるには、核心的な面で何倍も優れていなければならない。Plan 9 も UNIX 系より優れているが、置き換えるほど十分に優れてはいなかった
Bourne シェルスクリプティングのニッチを置き換えるほど十分に良いものを作るのは難しい。そこまで良くなる前に、すでに Perl、Python、Ruby のような本物のスクリプト言語の生態的地位や問題領域へ移ってしまう
狭い問題領域では局所最適解が空気をすべて吸い尽くし、理論上の大域最適解に近い競争相手が出てきにくくなる
最近追加された機能は
sh/Bash の上にうまく載ってはいるものの、結局シェルスクリプティングは目的のための手段であり、一般的なプログラミング言語よりはるかにゆっくり進化すべきものBash/
shの核心的な特徴は、反エントロピー的である点にある。開発や進化がほとんどないため、依存関係や新機能で面倒が起きる可能性が低く、20年前に使えたものが今も基本ツールとして残り続けているその設計上、変化に抵抗するシステムになっており、限界にぶつかると人々をその外へ出ていかせる誘因が生まれる
FreeBSD のスクリプトの大半は
sh用に書かれており、shは POSIX 標準の一部なので、はるかに広くサポートされていると感じる。Bash は単に人気がある方だと思うただし Bash があまりにひどいので、Groovy スクリプトを使うために名前空間を短くしたユーティリティを大量に作った。IDE で開発でき、ライブラリシステムも安全で、Groovy が Java の不便さをほぼ全部ならしてくれるので、ずっと良かった
いくつか制限はあるが、一般的に役立つかもしれない: https://github.com/ketancmaheshwari/pd
die()テクニックは良いが、Bash には腹立たしい特性がある。サブシェル内でexitしようとすると、サブシェルだけが終了し、残りのスクリプトは実行され続ける例えば
cat myfile | while read line; do ... die "Found match" ... doneのようなパイプライン内でdieを呼び出すと、後続のecho "I don't want this line"が依然として出力されるサブシェルを避けられる場合は多く、この例では
shellcheckが UUOC を指摘するのは正しく、それを直せばサブシェル内のdie問題も解決するしかし時にはサブシェルを避けられなかったり、避けようとするとスクリプトが複雑になりすぎたりする。そういう場合は、スクリプトの開始部分で
MYPID=$$と PID を保持しておき、die() { echo "$1" >&2; kill -9 $MYPID; exit 1; }のように kill できるもちろん、これにもトレードオフがある。この kill の仕方はかなり乱暴で、理由は分からないが完全に信頼できるものでもなかった
set -eを追加するだけで、サブシェルが 0 以外のエラーコードで終了したときにスクリプトも一緒に終了するどんなシェルスクリプトでも
set -eを外すべき理由はあまり思い浮かばないset -euxo pipefailを入れている条件テストは少し難しくなるが、特に
pipefailだけでも何度も元を取ったset +xで無効にすることもできるずっと有効にしておくと、かなり退屈になる
ただし
-xは、本当にあらゆる汚いデバッグ出力を見る必要があるときまで取っておく