LoginSignup
10
4

シェルスクリプトは「0が真」ではありません! 真偽値と終了ステータスは違うものです

Last updated at Posted at 2022-04-19

はじめに

シェルスクリプトの言語は他の言語と反対で 0 が真で 1(0以外)が偽であるという「間違った説明」をよく目にします。シェル言語も他の言語と同じで 0 は偽で 0 以外が真です。他の言語と真偽値の意味が「逆ではない」ので混同しないようにしてください。

間違ったシェルスクリプトの説明を JavaScript に置き換えるとこのようになります。

JavaScript の例(これは間違った説明)
if (exit_status == 0) {
  // exit_status が 0 ならこちらが出力されるから JavaScript では 0 が真である
  console.log("true"); 
} else {
  console.log("false")
}

JavaScript の例を見れば、おそらく誰もが「何言ってるんだ?」となるはずなのですが、困ったことに、シェルスクリプトだと 0 が真だと勘違いする人が多いようです。

シェルスクリプトの例(これも上記と同じで間違った説明をしている)
if [ "$exit_status" -eq 0 ]; then
  # exit_status が 0 ならこちらが出力されるからシェルスクリプトでは 0 が真である
  echo "true"
else
  echo "false"
fi

論より証拠

シェルスクリプトでも他の言語と同じく真となる式は 1 に展開されます。

$ echo $((123 > 100))
1

$ perl -l -e 'print 123 > 100'
1
$ python -c 'print int(123 > 100)'
1
$ node -e 'console.log(Number(123 > 100))'
1

シェルスクリプトでも他の言語と同じく 1 は真、0 は偽として扱われます。

$ bash -c 'if ((1)); then echo "true"; else echo "false"; fi
true
$ bash -c 'if ((0)); then echo "true"; else echo "false"; fi'
false

$ bash -c 'if ((2 > 1)); then echo "true"; else echo "false"; fi'
true
$ bash -c 'if ((0 > 1)); then echo "true"; else echo "false"; fi'
false

((算術式)) という書き方は ksh, bash, zsh 等の拡張機能ですが、Bourne Shell または純粋な POSIX シェル用に expr コマンドを使った場合でも結果は同じです。

$ dash -c 'if expr 1; then echo "true"; else echo "false"; fi'
1
true
$ dash -c 'if expr 0; then echo "true"; else echo "false"; fi'
0
false

$ dash -c 'if expr 2 ">" 1; then echo "true"; else echo "false"; fi'
1
true
$ dash -c 'if expr 0 ">" 1; then echo "true"; else echo "false"; fi'
0
false

シェル言語には真偽値型はありません

まずシェル言語には真偽値型というものはありません。シェル言語は文字列型しか型がない言語です。数値も文字列として扱われ、文字列として扱うか数値として扱うかはその時にどんな命令を使うかで決まります。

value1=123
value2=456

# 文字列として結合する場合(文字列を結合する演算子のようなものはありません)
echo "${value1}${value2}" # => 123456

# 数値として計算する場合
echo $((value1 + value2)) # => 579

シェル言語には真偽値型はありませんが、数値として比較したときに代入される値が真偽値相当の意味を持っています。他の言語でもだいたい似たような仕様です。

ret1=$((10 > 2))
echo "$ret1" # => 1 (真相当の意味を持つ数値)

ret2=$((10 < 2))
echo "$ret2" # => 0 (偽相当の意味を持つ数値)

この結果からわかるように、シェル言語も他の言語と同様に、真は 1 、偽は 0 です。なお 0 以外のすべての数値は真として扱われます。これも他の言語と同様です。

if ((1)); then
  echo "true" # 出力されるのはこちら
else
  echo "false"
fi

シェル言語が他の言語と違うのは if の仕様

シェル言語でも 0 が偽で、0 以外が真です。他の言語と違うのは真偽値の意味ではなく if の仕様です。

$? は終了ステータスです

0 を真と勘違いしてしまう原因の一つは特殊パラメータ $? に格納されている値を真偽値だと考えてしまうことです。$? は真偽値ではありません。これは終了ステータスと呼ばれる数値です。

man bash より
特殊パラメータ
    ?      最後に実行されたフォアグラウンドのパイプラインの 終了ステータスに展開されます。

以下のコードの意味を正しく説明するとこのようになります。

my_command
if [ $? -eq 0 ]; then # 訳 もし my_command の終了ステータスが正常終了(0)なら
  ...
fi

間違った説明では「もし my_command の終了ステータスが真 (0) なら」となっています。終了ステータスは真偽値ではないのに真偽値として扱っているからおかしなことになります。

補足 上記は良くない書き方(下記参照)

おまけ $? を比較するのはやめよう

そもそも必要がないのであれば $?0 と比較する必要はありません。この話については関連記事 シェルスクリプトの if は3分で簡単にマスターできるで詳しく説明しています。

# 適切な書き方はよりシンプル
if my_command; then # 訳 もし my_command が成功なら
  ...
fi

# $? を参照するのは終了ステータスの値によって異なる処理に分岐したい時だけ
case $? in 
  1) ... ;;
  2) ... ;;
  3) ... ;;
esac

if はコマンドの実行結果で分岐する命令

他の多くの言語では if は評価された値(真偽値)で分岐する命令ですが、シェルスクリプトの if は真偽値で分岐するのではなくコマンドの実行結果の成否で分岐する命令です。

man bash より
if list; then list; [ elif list; then list; ] ... [ else list; ] fi
       最初に if list が実行されます。list の終了ステータスが 0 ならば、then list  が実行
       されます。 (以下略)

if 自体に数値を比較する機能はありません。数値を比較しているのは test コマンド(または同等の [ コマンド)であって、if 自体はコマンドの実行結果で分岐しているだけです。

if test 1 -gt 0; then # 訳 もし test コマンドの結果が成功なら
  ...
fi

if [ 1 -gt 0 ]; then # 訳 もし [ コマンドの結果が成功なら
  ...
fi

if の文法は次のようなものです。if に「条件」という概念はありません。

if 任意のコマンド; then
  ...
fi

どの言語でも成功時の終了ステータスは 0

「終了ステータス」というのはシェル言語特有の概念ではありません。例えば C 言語で作ったコマンドは正常終了であれば終了ステータスとして 0 を返します。

#include <stdio.h>

int main() 
{
  printf("Hello, World!\n");
  return 0;
}

&&|| はちょっと注意

foo && bar

これも foo の終了ステータスが 0、つまり foo が成功なら bar を実行すると読みます。ただし ((... && ...))$((... && ...))[[ ... && ... ]] の中の && は、左側が「真」ならと読みます。

(( 123 > 100 && 1 ))
echo $(( 123 > 100 && 1 )) # => 1
[[ 123 > 20 && 1 ]]        # 文字列として比較している

これは算術式および条件式の && と、コマンドをつなげるリスト演算子の && は別物だからです。説明は省略しましたが || も同様です。

[ 0 ][ 1 ] 終了ステータスは 0(成功)

[ 0 ][ "0" ] は同じ意味で、これは文字列の長さがあるかどうかを調べています。

[ 0 ]; echo $?   # => 0
[ "0" ]; echo $? # => 0
[ 1 ]; echo $?   # => 0
[ "1" ]; echo $? # => 0
[ "" ]; echo $?  # => 1
[ ]; echo $?     # => 1

なぜ文字列の長さを調べるのかというとそれが、[ コマンドの仕様だからです。シェルスクリプトでは数値を使うことはユースケース的に少なく、シェル言語は文字列を扱う言語として設計されています。とは言うもののシェルスクリプトで数値を扱うこともそれなりにあったため、ksh(その後に続くbashやzsh)で拡張されたのが、数値として評価する ((...)) です。

if ((0)); then
  echo "true"
else
  echo "false"  # こちらが出力される
fi

if ((1)); then
  echo "true"   # こちらが出力される
else
  echo "false"
fi

もちろん Bourne シェルや純粋な POSIX シェルで同等のことができないというわけではありません。(文字列ではなく)数値として 0 と 1 を評価したいのであれば expr コマンドを使用します。

if expr 0 >/dev/null; then
  echo "true"
else
  echo "false"  # こちらが出力される
fi

if expr 1 >/dev/null; then
  echo "true"   # こちらが出力される
else
  echo "false"
fi

とは言うものの expr コマンドは外部コマンドで遅く標準出力を /dev/null に捨てねばならないので、通常は [ コマンドを使います。上記の例はシェルスクリプトでも昔から 0 が偽、1 が真として扱われていたということを示すためだけに書いています。

シェル関数に「戻り値」はありません

終了ステータスは他の言語で言う戻り値でもありません。終了ステータスには 0 - 255 の数値しか返せないという制限があります。シェル関数はコマンドと同様の性質のものであり、本来は実行が成功したのであれば 0 を返すべきものです。

func() {
  ...
  return 0 # 戻り値ではなく終了ステータス
}

本来はこのように戻り値として使うべきではない。

add() {
  return $(($1 + $2)) 
}
add 12 34
echo $? # => 46

シェル関数も終了ステータスを返す

シェル関数がシェルスクリプトを終了してないのに"終了"ステータスを返すのかというと、シェル関数が外部コマンドと本質的に等価になるように設計されているからです。これによって外部コマンドをシェル関数で上書きして処理をカスタマイズしたり環境ごとの互換性の違いを吸収したりすることが可能になります。

シェルスクリプトにとってシェル関数と外部コマンドは同じものであり、外部コマンドを言語の関数のようにシームレスに使うことが出来るのがシェルスクリプトの大きな特徴です。

おまけ もし戻り値を返したくなったら?

シェル関数から標準出力に出力してコマンド置換で受け取るのが一般的に用いられている方法です。ただしコマンド置換は遅いのでループの中などで何度も呼び出す場合はおすすめしません。

func() {
  echo "string"
}

ret=$(func) # 遅い
echo "$ret" # => string

もう一つの方法は指定された名前の変数に返す方法です。ただし eval をつかう場合は変数名を外部から来た文字列から組み立てないように注意する必要があります。そうしなければ脆弱性に繋がる可能性があるからです。bash などでは拡張機能として printf コマンドを使って変数に代入することができます。こちらの方が安全ですが、eval を使う場合でも通常は変数名は固定の文字列をソースコードに書くはずなので問題ないはず危険はないはずです。

func() { # POSIX 準拠
  eval "$1='string'"
}

func() { # bash 拡張
  printf -v "$1" '%s' "string"
}

func ret
echo "$ret" # => string

その他の方法としてグローバル変数経由で返す方法もあります。

func() { # POSIX 準拠
  RET="string"
}

func && ret=$RET
echo "$ret" # => string

まとめ

0 が終了ステータス ($?) の値ならば、それは「真」ではなく「成功」または「正常終了」という意味です。「if はコマンド実行結果の終了ステータスが 0 の場合は真とみなす」という言い方であれば許容範囲かもしれませんが、単に 0 を真と言うことはできません。他の言語と同じく算術式を評価した場合の 0 の意味は偽だからです。

シェルスクリプトの if は他の言語と違い終了ステータスで分岐する命令です。この違いはシェル言語がコマンドを連携させるために設計された言語である大きな特徴の一つです。

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4