web-dev-qa-db-ja.com

これはbashのバグですか?パイプから呼び出された場合、 `return`は関数を終了しません

私は最近bashでいくつかの奇妙な問題を抱えています。スクリプトを簡略化しようとしているときに、次の小さなコードを思いつきました。

$ o(){ echo | while read -r; do return 0; done; echo $?;}; o
0
$ o(){ echo | while read -r; do return 1; done; echo $?;}; o
1

returnは、印刷せずに関数を終了する必要があります$?、そうではないですか?それでは、パイプだけから戻ることができるかどうかを確認しました。

$ echo | while read -r; do return 1; done
bash: return: can only `return' from a function or sourced script

同じことがwhileループなしでも起こります:

$ foo(){ : | return 1; echo "This should not be printed.";}
$ foo
This should not be printed.

ここで見逃しているものはありますか?グーグル検索はこれについて何ももたらしませんでした!私のbashバージョンは、Debian Wheezyでは4.2.37(1)-releaseです。

16
Teresa e Junior

関連: https://stackoverflow.com/a/7804208/49379

サブシェルでexitまたはreturnを使用してスクリプトを終了したり、関数から戻ることができないのはバグではありません。それらは別のプロセスで実行され、メインプロセスには影響しません。

それ以外にも、(おそらく)未定義の仕様でのbashの文書化されていない動作を見ていると思います。関数では、サブシェルコマンドのトップレベルのreturnに対してエラーはアサートされず、exitのように動作します。

私見それは、メインステートメントが関数内にあるかどうかに応じて、returnの一貫性のない動作のbashバグです。

#!/bin/bash

o() {
    # Runtime error, but no errors are asserted,
    # each $? is set to the return code.
    echo | return 10
    echo $?
    (return 11)
    echo $?

    # Valid, each $? is set to the exit code.
    echo | exit 12
    echo $?
    (exit 13)
    echo $?
}
o

# Runtime errors are asserted, each $? is set to 1.
echo | return 20
echo $?
(return 21)
echo $?

# Valid, each $? is set to the exit code.
echo | exit 22
echo $?
(exit 23)
echo $?

出力:

$ bash script.sh 
10
11
12
13
script.sh: line 20: return: can only `return' from a function or sourced script
1
script.sh: line 22: return: can only `return' from a function or sourced script
1
22
23
10
yaegashi

bashのバグではありませんが、その 文書化された動作

パイプラインの各コマンドは、独自のサブシェルで実行されます

return命令は関数定義内で有効ですが、サブシェルでも有効です。親シェルには影響しないため、次の命令echoは関係なく実行されます。それでも POSIX標準 はパイプラインを構成するコマンドをサブシェル(デフォルト)または最上位(許可される拡張)のいずれかで実行できるため、移植性のないシェル構造です。

さらに、マルチコマンドパイプラインの各コマンドはサブシェル環境にあります。ただし、拡張機能として、パイプライン内の一部またはすべてのコマンドを現在の環境で実行できます。他のすべてのコマンドは、現在のシェル環境で実行されます。

うまくいけば、いくつかのオプションを使用してbashに期待どおりの動作をさせることができます。

$ set +m # disable job control
$ shopt -s lastpipe # do not run the last command of a pipeline a subshell 
$ o(){ echo | while read -r; do return 0; done; echo $?;}
$ o
$          <- nothing is printed here
17
jlliagre

POSIXのドキュメントによれば、 関数またはソーススクリプトの外でのreturnの使用は指定されていません 。したがって、処理するシェルに依存します。

SystemVシェルはエラーを報告しますが、kshでは、return関数外またはソーススクリプトはexitのように動作します。他のほとんどのPOSIXシェルと schily's osh も同様に動作します。

$ for s in /bin/*sh /opt/schily/bin/osh; do
  printf '<%s>\n' $s
  $s -c '
    o(){ echo | while read l; do return 0; done; echo $?;}; o
  '
done
</bin/bash>
0
</bin/dash>
0
</bin/ksh>
</bin/lksh>
0
</bin/mksh>
0
</bin/pdksh>
0
</bin/posh>
0
</bin/sh>
0
</bin/yash>
0
</bin/zsh>
</opt/schily/bin/osh>
0

kshおよびzshは、これらのシェルのパイプの最後の部分がサブシェルではなく現在のシェルで実行されたため、出力されませんでした。 returnステートメントは、関数を呼び出した現在のシェル環境に影響を与え、何も出力せずに関数がすぐに戻ります。

インタラクティブセッションでは、bashはエラーを報告するだけで、シェルを終了しませんでしたschily's oshはエラーを報告し、シェルを終了しました:

$ for s in /bin/*sh; do printf '<%s>\n' $s; $s -ci 'return 1; echo 1'; done
</bin/bash>
bash: return: can only `return' from a function or sourced script
1
</bin/dash>
</bin/ksh>
</bin/lksh>
</bin/mksh>
</bin/pdksh>
</bin/posh>
</bin/sh>
</bin/yash>
</bin/zsh>
</opt/schily/bin/osh>
$ cannot return when not in function

(インタラクティブセッションのzshと出力はターミナルで終了しない、bashyashschily's oshはエラーを報告しましたが、シェルを終了しませんでした)

6
cuonglm

期待どおりの動作が得られたと思います。bashでは、パイプラインの各コマンドはサブシェルで実行されます。関数のグローバル変数を変更しようとすることで、自分自身を混乱させることができます。

foo(){ x=42; : | x=3; echo "x==$x";}

ちなみに、戻りは機能していますが、サブシェルから戻ります。もう一度確認できます。

foo(){ : | return 1; echo$?; echo "This should not be printed.";}

以下を出力します:

1
This should not be printed.

したがって、returnステートメントはサブシェルを正しく終了しました

4
herbert

より一般的な答えは、bashや他のいくつかのシェルは通常、パイプラインのすべての要素を別々のプロセスに入れることです。これは、コマンドラインが

プログラム1プログラム2プログラム

とにかく、プログラムは通常別のプロセスで実行されるためです(exec program)。しかし、それは驚きです

コマンド1コマンド2コマンド

ここで、コマンドの一部またはすべてが組み込みコマンドです。簡単な例は次のとおりです。

$ a=0
$ echo | a=1
$ echo "$a"
0
$ cd /
$ echo | cd /tmp
$ pwd
/

もう少し現実的な例は

$ t=0
$ ps | while read pid rest_of_line
> do
>     : $((t+=pid))
> done
$ echo "$t"
0

ここで、whiledodoneループ全体がサブプロセスに入れられるため、tへの変更はメインシェルに表示されません。ループは終了します。そして、それがまさにあなたがしていることです。whileループにパイプを入れ、ループをサブシェルとして実行させてから、サブシェルからの戻りを試みます。

1
Scott