Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 16746] ファイルの概要

このコミットは、Goプロジェクトのビルドおよびテストスクリプトである src/run.bash におけるエラーハンドリングの改善を目的としています。特に、Cgo関連のテストやその他のサブシェル内で実行されるコマンドが失敗した場合に、スクリプトが適切に停止しない問題を修正しています。

コミット

commit 3a8845b5259e9b4fa80a43444643ea74f1078286
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jul 11 23:24:57 2013 -0400

    run.bash: actually stop on cgo failures
    
    I hate bash.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/11200043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3a8845b5259e9b4fa80a43444643ea74f1078286

元コミット内容

run.bash: actually stop on cgo failures

このコミットは、run.bash スクリプトがCgo関連のテスト失敗時に実際に停止するように修正します。コミットメッセージには「I hate bash.」という一文があり、Bashスクリプトの複雑さやエラーハンドリングの難しさに対する作者のフラストレーションが伺えます。

変更の背景

run.bash はGoプロジェクトの主要なビルドおよびテストスクリプトであり、様々なサブプロジェクトやテストケースを実行します。Bashスクリプトでは、通常 set -e を使用して、コマンドが非ゼロの終了ステータスを返した場合にスクリプトの実行を即座に停止させることができます。しかし、このコミットの背景には、set -e がサブシェル内や || (ORリスト) 演算子と組み合わされた場合に期待通りに動作しないというBashの挙動の落とし穴がありました。

具体的には、Cgo関連のテストが失敗した場合、その失敗がサブシェル内で発生したり、|| exit $? のような構造と組み合わされたりすると、set -e が有効であってもスクリプト全体が停止せずに続行してしまう問題がありました。これは、テストの失敗を見逃し、ビルドプロセスが不健全な状態のまま続行される可能性があるため、非常に危険な挙動です。

このコミットは、このようなBashの挙動の癖に対処し、スクリプトの堅牢性を高めることを目的としています。

前提知識の解説

Bashの set -e

set -e はBashのオプションの一つで、スクリプト内で実行されるコマンドが非ゼロの終了ステータス(エラーを示す)を返した場合に、スクリプトの実行を即座に終了させるように設定します。これにより、エラーが発生した際にスクリプトが予期せぬ動作を続けることを防ぎ、早期に問題を検出できます。

例:

#!/bin/bash
set -e
echo "Start"
false # このコマンドは非ゼロの終了ステータスを返す
echo "End" # この行は実行されない

サブシェルと set -e の挙動

Bashでは、括弧 () で囲まれたコマンドはサブシェルで実行されます。サブシェルは親シェルとは独立した環境を持つため、set -e の挙動が複雑になることがあります。

このコミットのコメントで説明されているように、set -e はサブシェル内では機能しますが、|| (ORリスト) 演算子と組み合わせると、その挙動が期待通りにならない場合があります。

例(コミットコメントより):

$ set -e; (set -e; false; echo still here); echo subshell exit status $?
subshell exit status 1
# サブシェルは早期に停止し、終了ステータスを設定したが、外側の set -e は停止しなかった。

$ set -e; (set -e; false; echo still here) || echo stopped
still here
# なぜか '|| echo stopped' が内側の set -e を壊した。

この例が示すように、サブシェル内で false コマンドが実行されても、|| 演算子が存在すると、set -e がスクリプトを停止させずに、echo still here が実行されてしまいます。これは、|| 演算子が左側のコマンドの失敗を「処理」しようとするため、set -e がエラーとして認識しないためです。

|| exit 1 の必要性

上記の set -e の制限を回避するために、サブシェル内で実行される各コマンドの後に || exit 1 を追加するというプラクティスが推奨されます。これは、コマンドが失敗した場合(非ゼロの終了ステータスを返した場合)に、そのサブシェルを明示的に終了させるためのものです。これにより、サブシェルが非ゼロの終了ステータスで終了し、外側の set -e がその失敗を捕捉してスクリプト全体を停止させることができます。

技術的詳細

このコミットの技術的詳細は、Bashスクリプトにおける堅牢なエラーハンドリングの実装にあります。特に、Goプロジェクトのビルドおよびテストプロセスにおいて、Cgo関連のテストが非常に重要であるため、これらのテストの失敗が確実にビルドプロセス全体を停止させることが求められます。

変更の中心は、src/run.bash 内で実行される多くのコマンド、特に go rungo testmake./test.bashgo build などの後に || exit 1 を追加することです。これにより、これらのコマンドのいずれかが非ゼロの終了ステータスを返した場合、即座に現在のサブシェルが終了し、その終了ステータスが親シェルに伝播されます。親シェルで set -e が有効であれば、この非ゼロの終了ステータスを捕捉してスクリプト全体の実行を停止させることができます。

コミットメッセージのコメントでRuss Coxが説明しているように、これはBashの set -e の既知の「バグ」または「癖」に対するワークアラウンドです。set -e は非常に便利ですが、サブシェルや || 演算子のような複雑な制御フローと組み合わせると、その挙動が直感的でなくなることがあります。このため、スクリプトの信頼性を確保するためには、各コマンドの成功を明示的に確認し、失敗時には exit 1 を呼び出すという防御的なプログラミングスタイルが必要になります。

この変更は、Goのビルドシステムがテストの失敗を確実に検出し、CI/CDパイプラインの健全性を維持するために不可欠です。

コアとなるコードの変更箇所

変更は src/run.bash ファイルに集中しており、主に以下のパターンで || exit 1 が追加されています。

--- a/src/run.bash
+++ b/src/run.bash
@@ -68,72 +68,88 @@ esac
 xcd() {
 	echo
 	echo '#' $1
-	builtin cd "$GOROOT"/src/$1
+	builtin cd "$GOROOT"/src/$1 || exit 1
 }
 
+# NOTE: "set -e" cannot help us in subshells. It works until you test it with ||.
+#
+#
+#	$ bash --version
+#	GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin12)
+#	Copyright (C) 2007 Free Software Foundation, Inc.
+#
+#
+#	$ set -e; (set -e; false; echo still here); echo subshell exit status $?
+#	subshell exit status 1
+#	# subshell stopped early, set exit status, but outer set -e didn't stop.
+#
+#
+#	$ set -e; (set -e; false; echo still here) || echo stopped
+#	still here
+#	# somehow the '|| echo stopped' broke the inner set -e.
+#	
+# To avoid this bug, every command in a subshell should have '|| exit 1' on it.
+# Strictly speaking, the test may be unnecessary on the final command of
+# the subshell, but it aids later editing and may avoid future bash bugs.
+
 [ "$CGO_ENABLED" != 1 ] ||
 [ "$GOHOSTOS" == windows ] ||
 (xcd ../misc/cgo/stdio
-go run $GOROOT/test/run.go - .
+go run $GOROOT/test/run.go - . || exit 1
 ) || exit $?
 
 [ "$CGO_ENABLED" != 1 ] ||
 (xcd ../misc/cgo/life
-go run $GOROOT/test/run.go - .
+go run $GOROOT/test/run.go - . || exit 1
 ) || exit $?
 
 [ "$CGO_ENABLED" != 1 ] ||
 (xcd ../misc/cgo/test
-set -e
-go test -ldflags '-linkmode=auto'
-go test -ldflags '-linkmode=internal'
+go test -ldflags '-linkmode=auto' || exit 1
+go test -ldflags '-linkmode=internal' || exit 1
 case "$GOHOSTOS-$GOARCH" in
 openbsd-386 | openbsd-amd64)
 	# test linkmode=external, but __thread not supported, so skip testtls.
-\tgo test -ldflags '-linkmode=external'
+\tgo test -ldflags '-linkmode=external' || exit 1
 	;;
 darwin-386 | darwin-amd64)
 	# linkmode=external fails on OS X 10.6 and earlier == Darwin
 	# 10.8 and earlier.
 	case $(uname -r) in
 	[0-9].* | 10.*) ;;
-\t*) go test -ldflags '-linkmode=external' ;;\n+\t*) go test -ldflags '-linkmode=external'  || exit 1;;\n
+\t*) go test -ldflags '-linkmode=external' || exit 1;;\n
 	esac
 	;;
 freebsd-386 | freebsd-amd64 | linux-386 | linux-amd64 | netbsd-386 | netbsd-amd64)
-\tgo test -ldflags '-linkmode=external'
-\tgo test -ldflags '-linkmode=auto' ../testtls
-\tgo test -ldflags '-linkmode=external' ../testtls
+\tgo test -ldflags '-linkmode=external' || exit 1
+\tgo test -ldflags '-linkmode=auto' ../testtls || exit 1
+\tgo test -ldflags '-linkmode=external' ../testtls || exit 1
 esac
 ) || exit $?
 
 [ "$CGO_ENABLED" != 1 ] ||
 [ "$GOHOSTOS" == windows ] ||
 (xcd ../misc/cgo/testso
-./test.bash
+./test.bash || exit 1
 ) || exit $?
 
 [ "$CGO_ENABLED" != 1 ] ||
 [ "$GOHOSTOS-$GOARCH" != linux-amd64 ] ||
 (xcd ../misc/cgo/testasan
-go run main.go
+go run main.go || exit 1
 ) || exit $?
 
 (xcd ../doc/progs
-time ./run
+time ./run || exit 1
 ) || exit $?
 
 [ "$GOARCH" == arm ] ||  # uses network, fails under QEMU
 (xcd ../doc/articles/wiki
-make clean
-./test.bash
+make clean || exit 1
+./test.bash || exit 1
 ) || exit $?
 
 (xcd ../doc/codewalk
 # TODO: test these too.
-set -e
-go build pig.go
-go build urlpoll.go
+go build pig.go || exit 1
+go build urlpoll.go || exit 1
 rm -f pig urlpoll
 ) || exit $?
 
@@ -143,19 +159,19 @@ go build ../misc/dashboard/builder ../misc/goplay
 
 [ "$GOARCH" == arm ] ||
 (xcd ../test/bench/shootout
-./timing.sh -test
+./timing.sh -test || exit 1
 ) || exit $?
 
 [ "$GOOS" == openbsd ] || # golang.org/issue/5057
 (
 echo
 echo '#' ../test/bench/go1
-go test ../test/bench/go1
+go test ../test/bench/go1 || exit 1
 ) || exit $?
 
 (xcd ../test
 unset GOMAXPROCS
-time go run run.go
+time go run run.go || exit 1
 ) || exit $?
 
 echo

コアとなるコードの解説

このコミットの核心は、src/run.bash スクリプト内の各コマンドの実行結果をより確実にチェックし、エラーが発生した場合にはスクリプトの実行を停止させるための変更です。

  1. xcd() 関数の変更: builtin cd "$GOROOT"/src/$1 || exit 1 xcd 関数はディレクトリを移動するためのヘルパー関数です。この変更により、cd コマンドが失敗した場合(例: 指定されたディレクトリが存在しない場合)に、即座にスクリプトが終了するようになります。

  2. サブシェル内のコマンドへの || exit 1 の追加: スクリプトの大部分で、go rungo testmake clean./test.bashgo build などのコマンドがサブシェル () 内で実行されています。これらのコマンドの後に || exit 1 が追加されています。 例:

    go run $GOROOT/test/run.go - . || exit 1
    go test -ldflags '-linkmode=auto' || exit 1
    make clean || exit 1
    ./test.bash || exit 1
    go build pig.go || exit 1
    

    これは、前述の「前提知識の解説」で説明したBashの set -e の挙動の癖に対処するためのものです。サブシェル内で実行されるコマンドが失敗した場合、|| exit 1 がそのサブシェルを強制的に終了させ、その非ゼロの終了ステータスが親シェルに伝播されます。これにより、親シェルで有効な set -e がそのエラーを捕捉し、スクリプト全体の実行を停止させることができます。

  3. set -e の削除: 以前のコードでは、一部のサブシェル内で set -e が明示的に設定されていましたが、このコミットではそれらが削除されています。これは、|| exit 1 を各コマンドに追加することで、サブシェル内での set -e の挙動の不確実性を回避し、より確実なエラーハンドリングを実現するためです。

これらの変更により、run.bash スクリプトは、Goのビルドやテスト中に発生する可能性のあるエラーに対して、より堅牢で予測可能な挙動を示すようになります。特にCgo関連のテストは、外部のCコンパイラやリンカに依存するため、失敗する可能性があり、その失敗を確実に捕捉することが重要です。

関連リンク

参考にした情報源リンク

これらの情報源は、Bashスクリプトのエラーハンドリングのベストプラクティス、特に set -e の挙動と || exit 1 の必要性について理解を深めるのに役立ちます。また、Cgoに関する情報は、このコミットが対処している特定のテストの文脈を理解する上で重要です。