web-dev-qa-db-ja.com

bash関数:中括弧と括弧で本文を囲みます

通常、bash関数は、中括弧を使用して本体を囲むことで定義されます。

foo()
{
    ...
}

今日、関数を多用するシェルスクリプトで作業しているときに、呼び出し元の関数と同じ名前の変数、つまりそれらの変数が同じであるという問題に遭遇しました。次に、関数内のローカル変数をローカルとして定義することで、これを防ぐことができることを発見しました。local var=xyz

次に、ある時点で、次のような括弧を使用して関数を定義することも同様に有効であると説明されているスレッド( 中括弧の代わりに括弧を使用してbash関数本体を定義する )を発見しました。

foo()
(
    ...
)

これの効果は、関数本体がサブシェルで実行されることです。これには、関数が独自の変数スコープを持っているという利点があり、ローカルなしでそれらを定義できます。関数ローカルスコープを持つことは、すべての変数がグローバルであるよりもはるかに理にかなっており、はるかに安全であるように思われるので、私はすぐに自分自身に問いかけます。

  • 括弧の代わりに関数本体を囲むためにデフォルトで中括弧が使用されるのはなぜですか?

ただし、サブシェルで関数を実行することの大きな欠点もすぐに発見しました。具体的には、関数内からスクリプトを終了しても機能しなくなり、代わりに呼び出しツリー全体に沿って戻りステータスを処理する必要があります(ネストされた関数)。これは私をこのフォローアップの質問に導きます:

  • 中括弧の代わりに括弧を使用することの他の大きな欠点(*)はありますか(中括弧が好まれるように見える理由を説明するかもしれません)?

(*)(私が時間をかけて遭遇した例外関連の議論から)エラーステータスを明示的に使用する方がどこからでも終了できるよりもはるかに優れていると主張する人もいることを私は知っていますが、私は後者が好きです。

どうやら両方のスタイルには長所と短所があります。したがって、経験豊富なbashユーザーの一部が私に一般的なガイダンスを提供してくれることを願っています。

  • 関数本体を囲むために中括弧を使用するのはいつですか?また、括弧に切り替えることをお勧めするのはいつですか?

編集:回答からのポイント

あなたの答えをありがとう、私の頭はこれに関してもう少し明確になりました。だから私が答えから奪うのは:

  • スクリプトの他の潜在的なユーザー/開発者を混乱させないためだけに、従来の中括弧を使用します(また、全体が括弧で囲まれている場合は中括弧を使用します)。

  • 中括弧の唯一の本当の欠点は、親スコープ内の任意の変数を変更できることですが、状況によってはこれが利点になる場合があります。これは、変数をlocalとして宣言することで簡単に回避できます。

  • 一方、括弧を使用すると、出口を台無しにしたり、スクリプトを強制終了したり、変数スコープを分離したりするなど、重大な望ましくない影響が生じる可能性があります。

27
flotzilla

括弧の代わりに関数本体を囲むためにデフォルトで中括弧が使用されるのはなぜですか?

関数の本体は、任意の複合コマンドにすることができます。これは通常{ list; }ですが、技術的には他の3つの形式の複合コマンドが許可されています:(list)((expression))、および[[ expression ]]

CおよびC++、Java、C#、JavaScriptなどのCファミリの言語はすべて、中括弧を使用して関数本体を区切ります。中括弧は、これらの言語に精通しているプログラマーにとって最も自然な構文です。

中括弧の代わりに括弧を使用することのその他の大きな欠点(*)はありますか(中括弧が好まれるように見える理由を説明している可能性があります)?

はい。サブシェルからはできないことがたくさんあります。

  • グローバル変数を変更します。変数の変更は親シェルに伝播されません。
  • スクリプトを終了します。 exitステートメントは、サブシェルのみを終了します。

サブシェルを開始することも、パフォーマンスに深刻な打撃を与える可能性があります。関数を呼び出すたびに、新しいプロセスを起動します。

スクリプトが強制終了されると、奇妙な動作が発生する可能性もあります。親シェルと子シェルが受信するシグナルが変化します。これは微妙な効果ですが、trapハンドラーがある場合やスクリプトをkillしている場合、これらの部分は希望どおりに機能しません。

関数本体を囲むために中括弧を使用するのはいつですか。また、括弧に切り替えることをお勧めするのはいつですか。

常に中括弧を使用することをお勧めします。明示的なサブシェルが必要な場合は、中括弧の中に括弧のセットを追加します。括弧だけを使用することは非常に珍しい構文であり、スクリプトを読む多くの人々を混乱させるでしょう。

foo() {
   (
       subshell commands;
   )
}
16
John Kugelman

それは本当に重要です。 bash関数は値を返さず、使用する変数はグローバルスコープからのものであるため(つまり、スコープの「外部」から変数にアクセスできます)、関数の出力を処理する通常の方法は、値をに格納することです。変数を呼び出してから呼び出します。

()で関数を定義すると、その通りです。サブシェルが作成されます。そのサブシェルには、元のシェルと同じ値が含まれますが、それらを変更することはできません。そのため、グローバルスコープ変数を変更するリソースが失われます。

例を参照してください。

$ cat a.sh
#!/bin/bash

func_braces() { #function with curly braces
echo "in $FUNCNAME. the value of v=$v"
v=4
}

func_parentheses() (
echo "in $FUNCNAME. the value of v=$v"
v=8
)


v=1
echo "v=$v. Let's start"
func_braces
echo "Value after func_braces is: v=$v"
func_parentheses
echo "Value after func_parentheses is: v=$v"

それを実行しましょう:

$ ./a.sh
v=1. Let's start
in func_braces. the value of v=1
Value after func_braces is: v=4
in func_parentheses. the value of v=4
Value after func_parentheses is: v=4   # the value did not change in the main Shell
5
fedorqui

ディレクトリを変更したいときはサブシェルを使用する傾向がありますが、常に同じ元のディレクトリからであり、pushd/popdを使用したりディレクトリを自分で管理したりする必要はありません。

for d in */; do
    ( cd "$d" && dosomething )
done

これは関数本体からも機能しますが、中括弧を使用して関数をdefineしても、サブシェルから使用することは可能です。

doit() {
    cd "$1" && dosomething
}
for d in */; do
    ( doit "$d" )
done

もちろん、declareまたはlocalを使用して、中括弧で定義された関数内で変数スコープを維持することもできます。

myfun() {
    local x=123
}

つまり、notがサブシェルであることが、その関数の明らかな正しい動作に悪影響を与える場合にのみ、関数をサブシェルとして明示的に定義します。

雑学クイズ:補足として、bashは実際には常に関数を中括弧の複合コマンドとして扱うことを考慮してください。かっこが含まれている場合があります。

$ f() ( echo hi )
$ type f
f is a function
f () 
{ 
    ( echo hi )
}
4
kojiro

注:ブレースリストが同じプロセスで実行されない場合があります。

a=22; { echo $a; a=46; echo $a; }; echo $a
says 22 46 46

だが

a=22; { echo $a; a=46; echo $a; }|cat; echo $a
says 22 46 22

Fedorquiに感謝します:)

0
Pasha Orehov