シェルスクリプトのreturn

先週Amazonから届いたスターオーシャン4をやってるのだが、字が小さい。何でだろう?テレビが小さいからか?バトル画面で表示されているHPとかの数字がちゃんと読み取れないくらい小さいので、正直ちょっと困っている。

それはそうと今日は、久々にシェルスクリプト書いてて嵌った話。

add.sh

実際に書いていたのはもうちょっと長いんだけど、話を単純にするため、受け取った二つの引数の和を求めるシェルスクリプトを書いてみる。(エラー処理は省略)

#!/bin/sh

ret=`expr $1 + $2`
echo $ret

とりあえず、実行してみる。

$ sh add.sh 1 2
3
$ sh add.sh 100 1000
1100
$

上記のスクリプトを書いた後、二つの数の和を求める処理がいろんな箇所で必要になりそうだったので、add関数を定義することにした。

#!/bin/sh

add () {
    ret=`expr $1 + $2`
    return $ret
}

add $1 $2
echo $?

再び実行してみる。

$ sh add.sh 1 2
3
$ sh add.sh 100 1100
76
$

前者はともかく後者は期待したのと違う答えが返ってくる。ホントに上記の簡単なスクリプトならもっと早く気付いたかもしれないんだけど、実際書いたやつのはもうちょっと込み入っていたので、どこがおかしいのか気付くのに時間がかかってしまった。echoデバッグをやってると、関数内でreturnする前の値はちゃんと計算できてるのにreturnされた値は全然違う値になってしまっているのに気付く。

bashのソースを読んでみる

単純に値の有効範囲を越えてしまったんだろうと思いつつ、念のため、ソース読んで確認してみる。/bin/shは/bin/bashにリンクされていたので、GNUのサイトからbashのソースをダウンロード。適当に漁ってると、builtins/return.defなんてそのまんまなファイルを発見。(バージョンは4.0)

builtins/return.def

int
return_builtin (list)
     WORD_LIST *list;
{
  if (no_options (list))
    return (EX_USAGE);
  list = loptend;       /* skip over possible `--' */

  return_catch_value = get_exitstat (list);

  if (return_catch_flag)
    longjmp (return_catch, 1);
  else
    {
      builtin_error (_("can only `return' from a function or sourced script"));
      return (EXECUTION_FAILURE);
    }
}

おそらくreturn_catch_valueがreturnによって返される値だと思われるので、さらにこの変数の代入に使用されているget_exitstat関数を見てみる。

/* Get an eight-bit status value from LIST */
int
get_exitstat (list)
WORD_LIST *list;
{
  int status;
  intmax_t sval;
  char *arg;

  if (list && list->word && ISOPTION (list->word->word, '-'))
    list = list->next;

  if (list == 0)
    return (last_command_exit_value);

  arg = list->word->word;
  if (arg == 0 || legal_number (arg, &sval) == 0)
    {
      sh_neednumarg (list->word->word ? list->word->word : "`'");
      return 255;
    }
  no_args (list->next);

  status = sval & 255;
  return status;
}

型がintで特にキャストもしてなかったので、一瞬あれ?っと思ったが、関数の中身をよく見てみると、255より大きい値にはならないように手が加えられている。というか、コメントに「8bitの値」って書かれてますね。さらに、さっきのreturn_builtin関数の方に戻ってみると、定義の真上にこんなコメントが。

/* If we are executing a user-defined function then exit with the value
   specified as an argument.  if no argument is given, then the last
   exit status is used. */

returnで返される値は終了コードであって、ほかのプログラミング言語みたいに何でも返していいわけではない、ってことですかね。

回避策

returnの代わりにechoを使う。

#!/bin/sh

add () {
    ret=`expr $1 + $2`
    echo $ret
    return
}

ret=`add $1 $2`
echo $ret