関数でbashスクリプト全体を記述する理由


59

職場では、bashスクリプトを頻繁に記述しています。私のスーパーバイザーは、次の例のように、スクリプト全体を機能に分割することを提案しています。

#!/bin/bash

# Configure variables
declare_variables() {
    noun=geese
    count=three
}

# Announce something
i_am_foo() {
    echo "I am foo"
    sleep 0.5
    echo "hear me roar!"
}

# Tell a joke
walk_into_bar() {
    echo "So these ${count} ${noun} walk into a bar..."
}

# Emulate a pendulum clock for a bit
do_baz() {
    for i in {1..6}; do
        expr $i % 2 >/dev/null && echo "tick" || echo "tock"
        sleep 1
    done
}

# Establish run order
main() {
    declare_variables
    i_am_foo
    walk_into_bar
    do_baz
}

main

「読みやすさ」以外にこれを行う理由はありますか?これは、いくつかのコメントと行間隔で等しく確立できると思いますか?

それはスクリプトをより効率的に実行しますか(実際には反対のことを期待します)、または前述の可読性の可能性を超えてコードを簡単に変更できるようにしますか?それとも本当に単なる文体的な好みですか?

スクリプトはそれをうまく実証していませんが、実際のスクリプトの関数の「実行順序」は非常に線形になる傾向があることに注意してください- 行っwalk_into_barたものに依存しi_am_foodo_baz設定されたものに作用しますwalk_into_bar-実行順序を任意に交換できることは、私たちが一般的に行っていることではありません。たとえば、突然のdeclare_variableswalk_into_barにを置きたくない場合、それが問題を引き起こします。

上記のスクリプトをどのように作成するかの例を次に示します。

#!/bin/bash

# Configure variables
noun=geese
count=three

# Announce something
echo "I am foo"
sleep 0.5
echo "hear me roar!"

# Tell a joke
echo "So these ${count} ${noun} walk into a bar..."

# Emulate a pendulum clock for a bit
for i in {1..6}; do
    expr $i % 2 >/dev/null && echo "tick" || echo "tock"
    sleep 1
done

30
私はあなたの上司が好きです。私のスクリプトmain()では、上部に配置main "$@"し、下部に追加して呼び出します。これにより、高レベルのスクリプトロジックを開いたときに最初に見ることができます。
ジョンクーゲルマンはモニカをサポートします

22
読みやすさは、「コメントをいくつか追加し、行間を空けることで等しく確立できる」という考えに同意しません。おそらくフィクションを除いて、目次と各章とセクションの説明的な名前がない本を扱いたくありません。プログラミング言語では、これは関数が提供できる読みやすさであり、コメントはそうではありません。
ライモイド

6
関数で宣言された変数は宣言される必要があることに注意してくださいlocal-これは、重要な変数のスコープを提供します。
スパイダーボリス

8
私はあなたの上司に同意しません。スクリプトを関数に分解する必要がある場合は、おそらく最初からシェルスクリプトを作成しないでください。代わりにプログラムを作成してください。
el.pescado

6
関数は、スクリプト内または複数のスクリプト内で繰り返されるプロセス用です。また、統一された方法論を導入することもできます。たとえば、関数を使用してsyslogに書き込む。誰もが同じ機能を使用している限り、syslogエントリはより一貫しています。あなたの例のようなシングルユース機能は、スクリプトを不必要に複雑にします。場合によっては、問題が発生します(変数のスコープ)。
-Xalorous

回答:


40

Kfir Laviのブログ投稿「Defensive Bash Programming」を読んだ後、この同じスタイルのbashプログラミングを使い始めました。彼にはいくつかの正当な理由がありますが、個人的にはこれらが最も重要だと思います。

  • プロシージャは記述的になります。コードの特定の部分が何をすべきかを理解するのがはるかに簡単です。コードの壁の代わりに、「ああ、そのfind_log_errors関数はエラーのログファイルを読み取ります」と表示されます。それを、神を使用するawk / grep / sed行の多くを見つけることと比較して、長いスクリプトの途中でどのタイプの正規表現を知っているか-コメントがない限り、そこで何をしているのかわかりません。

  • set -xおよびで囲むことにより、関数をデバッグできますset +x。残りのコードが正常に機能することがわかったら、このトリックを使用して、その特定の機能のみのデバッグに集中できます。確かに、スクリプトの一部を囲むことができますが、それが長い部分である場合はどうでしょうか?このようなことをする方が簡単です:

     set -x
     parse_process_list
     set +x
    
  • と印刷の使用cat <<- EOF . . . EOF。コードをより専門的にするために何度も使用しました。また、parse_args()with getoptsfunctionは非常に便利です。繰り返しになりますが、これはすべてをテキストの巨大な壁としてスクリプトに押し込むのではなく、読みやすくするのに役立ちます。これらを再利用することも便利です。

そして明らかに、これはC、Java、またはValaを知っているがbashの経験が限られている人にとってははるかに読みやすいです。効率性に関しては、できることはそれほど多くありません。bash自体は最も効率的な言語ではなく、スピードと効率の面では人々はperlとpythonを好みます。ただし、次niceの機能を使用できます。

nice -10 resource_hungry_function

コードのすべての行でniceを呼び出すのに比べて、これは入力の全体を減らし、スクリプトの一部のみを低い優先度で実行したい場合に便利に使用できます。

私の意見では、関数をバックグラウンドで実行することは、バックグラウンドで実行するステートメントの束全体を持ちたい場合にも役立ちます。

このスタイルを使用したいくつかの例:


3
その記事からの提案を非常に真剣に受けとめるべきかどうかはわかりません。確かに、いくつかの優れたアイデアがありますが、明らかにシェルスクリプトに慣れている人ではありません。ない単一の例のいずれかでの変数は、(!)引用され、それは、彼らが既存のenv VARSと競合する可能性があるため、多くの場合、非常に悪い考えですUPPER CASE変数名を使用することを提案しています。この答えのあなたのポイントは理にかなっていますが、リンクされた記事は他の言語に慣れているだけで、そのスタイルをbashに強制しようとしている人によって書かれたようです。
テルドン

1
@terdon記事に戻って読み直しました。著者が大文字変数の命名に言及している唯一の場所は、「Immutable Global Variables」です。グローバル変数を関数の環境内にある必要があるものと見なす場合、それらを大文字にすることは理にかなっています。ちなみに、bashのマニュアルには、大文字と小文字の区別に関する規則は記載されていません。ここで 受け入れられた答えでさえ「通常」と言っており、唯一の「標準」はGoogleによるものであり、IT業界全体を代表しているわけではありません。
セルギーKolodyazhnyy

@terdonは別のメモで、変数の引用は記事で言及されるべきであり、ブログのコメントでも指摘されていることに100%同意します。また、他の言語に慣れているかどうかに関係なく、このスタイルのコーディングを使用している人を判断しません。この質問と回答全体は、それに対する利点があることを明確に示しており、他の言語に慣れている人の程度はおそらくここでは無関係です。
セルギーKolodyazhnyy

1
@terdonよく、記事は「ソース」資料の一部として投稿されました。すべてを自分の意見として投稿することもできましたが、この記事から学んだことの一部は、時間をかけて研究した結果であると信用しなければなりませんでした。著者のリンク先ページには、LinuxとITの一般的な経験があることが示されているため、この記事では実際にそれを示していないと思いますが、Linuxとシェルスクリプトに関してはあなたの経験を信頼しています。 。
セルギーKolodyazhnyy

1
それは素晴らしい答えですが、Bashの変数スコープがファンキーであることも追加したいと思います。そのため、関数を使用localしてすべてを呼び出して関数内で変数を宣言することを好みmain()ます。これにより、物事がはるかに管理しやすくなり、面倒な状況を回避できます。
Housni

65

読みやすさは一つのことです。しかし、モジュール化にはこれだけではありません。(半モジュール化は、関数に対してより適切である可能があります。)

関数では、いくつかの変数をローカルに保つことができます。これにより、信頼性向上し、物事が台無しになる可能が低くなります。

機能のもう1つの利点は再利用性です。関数がコーディングされると、スクリプトで複数回適用できます。別のスクリプトに移植することもできます。

現在、コードは線形ですが、将来的にはBashの世界でマルチスレッド化またはマルチプロセッシングの領域に入るかもしれません。関数で物事を行うことを学べば、並列へのステップに十分に備えることができます。

追加するもう1つのポイント。Etsitpab Niolivが以下のコメントで気づいているように、首尾一貫したエンティティとして機能からリダイレクトするのは簡単です。しかし、関数を使用したリダイレクトにはもう1つの側面があります。つまり、リダイレクトは関数定義に沿って設定できます。例えば。:

f () { echo something; } > log

これで、関数呼び出しに明示的なリダイレクトは必要ありません。

$ f

これにより多くの繰り返しが不要になり、信頼性が向上し、物事を整然と保つことができます。

こちらもご覧ください


55
非常に良い答えですが、関数に分割された方がずっと良いでしょう。
ピエールアラード16

1
関数を追加すると、そのスクリプトを別のスクリプトにインポートできるようになります(sourceまたは. scriptname.shを使用し、それらの関数を新しいスクリプト内にあるかのように使用します
。– SnakeDoc

これについては、別の回答で既に説明しています。
トマス

1
感謝します。しかし、私はむしろ他の人も重要にしたいと思います。
トマス

7
今日、エコーの代わりにスクリプトの出力の一部を(電子メールで送信するために)ファイルにリダイレクトする必要がある場合に直面しました。目的の関数の出力をリダイレクトするには、単にmyFunction >> myFileを実行する必要がありました。とても便利です。関連するかもしれません。
Etsitpab Nioliv 16

39

私のコメントでは、関数の3つの利点について言及しました。

  1. 正確性のテストと検証が簡単になります。

  2. 関数は将来のスクリプトで簡単に再利用(ソース)できます

  3. あなたの上司はそれらが好きです。

そして、3番の重要性を決して過小評価しないでください。

もう1つの問題に対処したいと思います。

...したがって、実行順序を任意に交換できることは、一般的に行われていることではありません。たとえば、突然のdeclare_variableswalk_into_barにを置きたくない場合、それが問題を引き起こします。

コードを関数に分割する利点を得るには、関数を可能な限り独立させるようにしてください。walk_into_bar他で使用されていない変数が必要な場合は、その変数をで定義し、ローカルにする必要がありますwalk_into_bar。コードを関数に分離し、それらの相互依存関係を最小化するプロセスにより、コードがより明確でシンプルになります。

理想的には、機能は個別に簡単にテストできる必要があります。相互作用のためにテストが容易でない場合、それはリファクタリングの恩恵を受ける可能性がある兆候です。


私は、それらの依存関係をモデル化して強制することと、それらを回避するためのリファクタリングが時々賢明であると主張します機能します)。非常に複雑なユースケースがかつてフレームワーク影響を与え、それを実現しました
チャールズダフィー

4
機能に分割する必要があるものは、あるべきですが、この例ではそれを取りすぎています。本当に私を悩ませているのは変数宣言関数だけだと思います。グローバル変数、特に静的変数は、その目的専用のコメント付きセクションでグローバルに定義する必要があります。動的変数は、それらを使用および変更する関数に対してローカルでなければなりません。
-Xalorous

@Xalorous 外部ファイルから値を読み取るプロシージャを開発する前の中間的かつ迅速な手順として、グローバル変数がプロシージャで初期化されるプラクティスを見てきました ...定義を分離し、初期化ですが、めったに曲がって3番のアドバンテージを得る必要はありません;-)
-Hastur

14

C / C ++、python、perl、ruby、またはその他のプログラミング言語コードの場合と同じ理由で、コードを関数に分割します。より深い理由は抽象化です-低いレベルのタスクを高いレベルのプリミティブ(関数)にカプセル化するため、物事の処理方法を気にする必要がありません。同時に、コードがより読みやすく(保守しやすく)なり、プログラムロジックがより明確になります。

ただし、コードを見ると、変数を宣言する関数があるのはかなり奇妙です。これにより、私は本当に眉を上げます。


私見の過小評価された答え。では、main関数/メソッドで変数を宣言することをお勧めしますか?
デビッドTabernero M.

13

私は完全に再利用性可読性、上司に微妙にキスすることに同意しますが、の関数にはもう1つの利点があります:変数スコープLDPショー

#!/bin/bash
# ex62.sh: Global and local variables inside a function.

func ()
{
  local loc_var=23       # Declared as local variable.
  echo                   # Uses the 'local' builtin.
  echo "\"loc_var\" in function = $loc_var"
  global_var=999         # Not declared as local.
                         # Therefore, defaults to global. 
  echo "\"global_var\" in function = $global_var"
}  

func

# Now, to see if local variable "loc_var" exists outside the function.

echo
echo "\"loc_var\" outside function = $loc_var"
                                      # $loc_var outside function = 
                                      # No, $loc_var not visible globally.
echo "\"global_var\" outside function = $global_var"
                                      # $global_var outside function = 999
                                      # $global_var is visible globally.
echo                      

exit 0
#  In contrast to C, a Bash variable declared inside a function
#+ is local ONLY if declared as such.

これは実際のシェルスクリプトではあまり見られませんが、より複雑なスクリプトには適しているようです。凝集度を下げると、コードの別の部分で予期される変数を破壊するバグを回避できます。

再利用性とは、多くの場合、共通の関数ライブラリを作成し、sourceそのライブラリをすべてのスクリプトに組み込むことを意味します。これは、それらをより速く実行するのに役立ちませんが、それらをより速く書くのに役立ちます。


1
明示的にを使用している人はほとんどいませんがlocal、機能に分割されたスクリプトを作成するほとんどの人は、まだ設計原則に従っています。Usign localは、バグの導入を難しくしています。
-Voo

local関数とその子が変数を使用できるようにするため、関数Aから受け渡すことができるが、関数Bには使用できない変数があると便利です。関数Bは、同じ名前で異なる目的の変数を使用する場合があります。したがって、これはスコープを定義するのに適しています
。Vooが

10

すでに他の回答で与えられたものとはまったく異なる理由:トップレベルの唯一の非関数定義ステートメントがの呼び出しである場合、このテクニックが時々使用される理由の1つmainは、スクリプトが誤って厄介なことをしないようにすることですスクリプトが切り捨てられた場合。スクリプトがプロセスAからプロセスB(シェル)にパイプされると、スクリプトは切り捨てられる可能性があり、プロセスAはスクリプト全体の書き込みが完了する前に何らかの理由で終了します。これは、プロセスAがリモートリソースからスクリプトをフェッチする場合に特に発生する可能性があります。セキュリティ上の理由からそれは良い考えではありませんが、それは行われていることであり、問​​題を予測するためにいくつかのスクリプトが修正されています。


5
面白い!しかし、私は、各プログラムでそれらのことに注意を払わなければならないのは厄介だと思います。一方、まさにこのmain()パターンはif __name__ == '__main__': main()、ファイルの最後で使用するPythonで一般的です。
マーティンUeding 16

1
pythonイディオムには、import実行せずに他のスクリプトに現在のスクリプトを許可するという利点がありmainます。同様のガードをbashスクリプトに入れることができると思います。
ジェイクコブ

@ジェイク・コブはい。私は今、すべての新しいbashスクリプトでそれを行っています。すべての新しいスクリプトで使用される機能のコアインフラストラクチャを含むスクリプトがあります。そのスクリプトは、ソースまたは実行できます。ソースされている場合、そのメイン関数は実行されません。ソースと実行の検出は、BASH_SOURCEに実行中のスクリプトの名前が含まれているという事実によって行われます。コアスクリプトと同じ場合は、スクリプトが実行されています。それ以外の場合は、ソースされています。
DocSalvager

7

プロセスにはシーケンスが必要です。ほとんどのタスクはシーケンシャルです。注文を台無しにすることは意味がありません。

しかし、スクリプト作成を含むプログラミングに関する非常に大きなことはテストです。テスト、テスト、テスト。現在、スクリプトの正確性を検証するために必要なテストスクリプトは何ですか?

あなたの上司は、スクリプトキディからプログラマーになるまであなたを導こうとしています。これは入るのに良い方向です。あなたの後に来る人はあなたを好きになるでしょう。

しかし。プロセス指向のルーツを常に覚えておいてください。関数が通常実行される順序で関数を順序付けることが理にかなっている場合は、少なくとも最初のパスとしてそれを行います。

後で、一部の関数が入力を処理し、他の出力、他の処理、他のモデリングデータ、および他のデータを操作していることがわかります。 。

後で、多くのスクリプトで使用する小さなヘルパー関数のライブラリを作成したことに気付くかもしれません。


7

私が説明するように、コメントと間隔は、関数が読みやすい読みやすさに近づきません。機能がなければ、木々の森を見ることができません-大きな問題は多くの詳細に隠れています。言い換えれば、人々は細部と全体像に同時に集中することはできません。それは短いスクリプトでは明らかではないかもしれません。短いままである限り、十分に読みやすい場合があります。しかし、ソフトウェアは大きくなりますが、小さくはなりません。確かに、それはあなたの会社のソフトウェアシステム全体の一部であり、確かに非常に大きく、おそらく数百万行です。

次のような指示を与えたかどうかを検討してください。

Place your hands on your desk.
Tense your arm muscles.
Extend your knee and hip joints.
Relax your arms.
Move your arms backwards.
Move your left leg backwards.
Move your right leg backwards.
(continue for 10,000 more lines)

途中で、または5%を達成した時点で、最初のいくつかのステップが何であるかを忘れていたでしょう。ほとんどの問題を見つけることはできませんでした。なぜなら、木の代わりに森が見えなかったからです。機能と比較してください:

stand_up();
walk_to(break_room);
pour(coffee);
walk_to(office);

行ごとのシーケンシャルバージョンにいくつのコメントを入れても、それは確かにはるかに理解しやすいものです。また、コーヒーを作るのを忘れたことに気付く可能性がはるかに高くなり、おそらく最後にsit_down()を忘れた可能性があります。grepおよびawkの正規表現の詳細を考えているときは、「コーヒーが作られていない場合はどうなるか」という全体像を考えることはできません。

主に機能を使用すると、全体像を見ることができ、コーヒーを作るのを忘れた(または誰かがお茶を好む)ことがわかります。別の時には、異なる考え方で、詳細な実装を心配します。

もちろん、他の回答で説明した他の利点もあります。他の回答で明確に述べられていない別の利点は、関数がバグの防止と修正に重要な保証を提供することです。適切な関数walk_to()の変数$ fooが間違っていることに気付いた場合、その関数のその他の6行を調べるだけで、その問題の影響を受ける可能性のあるすべての要素と、可能なすべての要素を見つけることができます。それが間違っている原因となっています。(適切な)関数がなければ、システム全体のあらゆるものが$ fooの不正確な原因となり、すべてのものが$ fooの影響を受ける可能性があります。したがって、プログラムのすべての行を再検討しないと、$ fooを安全に修正することはできません。$ fooが関数に対してローカルである場合、


1
これはbash構文ではありません。しかし、それは残念です。そのような関数に入力を渡す方法はないと思います。(つまりpour();< coffee)。それはもっと(c++またはphp)のように見えます。

2
括弧なしの@ tjt263はbash構文です:コーヒーを注ぐ。括弧を使用すると、他のほとんどすべての言語になります。:)
レイ・モリス

6

プログラミングに関するいくつかの関連する真実:

  • 上司がそうではないと主張しても、プログラムは変わります。
  • コードと入力のみがプログラムの動作に影響します。
  • 命名は難しいです。

コメントは、コード*でアイデアを明確に表現できないための一時的なギャップとして始まり、変更によって悪化(または単に間違っている)します。したがって、可能な場合は、概念、構造、推論、セマンティクス、フロー、エラー処理、およびコードをコードとして理解することに関連する他のすべてを表現してください

ただし、Bash関数には、ほとんどの言語では見られない問題がいくつかあります。

  • 名前空間はBashでひどいです。たとえば、localキーワードの使用を忘れると、グローバル名前空間が汚染されます。
  • を使用local foo="$(bar)"すると、の終了コードが失われますbar
  • 名前付きパラメーターはないため"$@"、異なるコンテキストでの意味を覚えておく必要があります。

*これが気に入らない場合は申し訳ありませんが、コメントを数年間使用し、コメントなしで開発した後**、もう何年もの間、どちらが優れているかは明らかです。

**ライセンス、APIドキュメントなどにコメントを使用する必要があります。


私は...関数の先頭でヌルにそれらを宣言することによって、ほぼすべてのローカル変数を設定local foo=""すると...その結果に基づいて行動するためにコマンドの実行を使用してそれらを設定しますfoo="$(bar)" || { echo "bar() failed"; return 1; }。これにより、必要な値を設定できない場合、すぐに関数から抜け出します。中括弧はreturn 1、失敗時にのみ実行されることを保証するために必要です。
DocSalvager

6

時は金なり

あり、他の良い答え書くための技術的な理由に光を広げるモジュール方式で使用されるように開発された作業環境で開発された潜在的に長い、スクリプトを、人のグループとあなた自身の使用のためだけではなく。

私は期待に集中したいと思います:労働環境では「時は金なり」です。したがって、バグがないこととコードのパフォーマンスは、可読性、テスト容易性、保守容易性、リファクタリング可能性再利用可能性とともに評価されます...

書く「モジュール」のコードはではないだけに必要な読書の時間減少しますコーダ自体が、テスターまたは上司で使用しても、時間を。さらに、上司の時間は通常、コーダーの時間よりも多く支払われ、上司はあなたの仕事の質を評価することに注意してください。

さらに、独立した「モジュール」でコード(bashスクリプトを含む)を記述すると、チームの他のコンポーネントと「並行」して作業でき、全体の生産時間を短縮し、せいぜいシングルの専門知識を使用して、パーツをレビューまたは書き換えます他の人に副作用はありません。「そのまま」書いたコードをリサイクルします別のプログラム/スクリプトの場合、ライブラリ(またはスニペットのライブラリ)を作成し、全体のサイズとエラーの関連する可能性を減らし、各単一部分を徹底的にデバッグおよびテストします...そしてもちろん、プログラムの論理セクションに整理します/ scriptを使用して読みやすくします。時間とお金を節約するすべてのもの。欠点は、標準に固執し、機能をコメントしなければならないことです(それでも、作業環境で行う必要があります)。

標準に準拠することは、最初は作業を遅くしますが、その後、他のすべての(およびあなたも)の作業を高速化します。実際、関係者の数が増えてコラボレーションが必要になった場合、これは避けられないニーズになります。したがって、たとえば、グローバル変数は関数ではなくグローバルに定義する必要があると信じていても、declare_variables()常に変数の最初の行で呼び出されるという名前の関数でそれらを初期化する標準を理解でき main()ます...

最後になりましたが、最新のソースコードエディターで、選択的に分離されたルーチンを表示または非表示にする可能性を過小評価しないでください(コードの折りたたみ)。これにより、コードがコンパクトに保たれ、ユーザーが再び時間を節約できるようになります。

ここに画像の説明を入力してください

ここでは、関数のみが展開される様子を見ることができwalk_into_bar()ます。他のコードも1000行の長さでしたが、1ページですべてのコードを制御できます。変数を宣言/初期化するセクションでも折り畳まれていることに注意してください。


2

他の回答で与えられた理由は別として:

  1. 心理学:生産性がコード行で測定されているプログラマーは、不必要に冗長なコードを書くインセンティブを持ちます。管理がコード行に集中するほど、プログラマーは不必要な複雑さでコードを拡張するインセンティブが増えます。複雑さが増すと、メンテナンスのコストが増加し、バグ修正に必要な労力が増加するため、これは望ましくありません。

1
downvotesが言うように、それはそれほど悪い答えではありません。注:agcは、これも可能性であり、はい、そうです。彼はそれが唯一の可能性だとは言わず、誰も非難せず、事実を述べているだけです。私は今日、直接の「コードライン」->「$$」スタイルの契約の仕事はほとんど前代未聞だと思いますが、間接的には非常に一般的であることを意味します。
user259412

2

しばしば見落とされるもう1つの理由は、bashの構文解析です。

set -eu

echo "this shouldn't run"
{
echo "this shouldn't run either"

このスクリプトには明らかに構文エラーが含まれており、bashはまったく実行すべきではありませんか?違う。

~ $ bash t1.sh
this shouldn't run
t1.sh: line 7: syntax error: unexpected end of file

コードを関数でラップした場合、これは起こりません。

set -eu

main() {
  echo "this shouldn't run"
  {
  echo "this shouldn't run either"
}

main
~ $ bash t1.sh
t1.sh: line 10: syntax error: unexpected end of file
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.