web-dev-qa-db-ja.com

純粋なBashでプログラムを作成するのはどのくらい複雑ですか?

いくつかの非常に迅速な調査の後、それはバッシュ チューリング完全言語です のようです。

なぜBashはほとんど単純なスクリプトを記述するためにほとんど独占的に使用されているのでしょうか。 BashシェルにはLinuxが付属しているため、他の一般的なコンピューター言語で必要なように、外部インタープリターやコンパイラーなしでシェルスクリプトを実行できます。これは大きな利点であり、場合によっては言語自体の平凡さを補うことができます。

それで、そのようなプログラムがどれほど複雑になることができるのに限界がありますか?純粋なBashを使用して複雑なプログラムを作成していますか?たとえば、純粋なBashでファイルコンプレッサー/デコンプレッサーを書くことは可能ですか?コンパイラ?シンプルなビデオゲーム?

デバッグツールが非常に限られているという理由だけで、それはそれほどまばらに使用されていますか?

17
Bregalad

バッシュはチューリング完全な言語のようです

Turing completeness の概念は、- 大規模なプログラミング の言語で有用な他の多くの概念から完全に分離されています:使いやすさ、表現力、理解しやすさ、速度など。

チューリングの完全性が私たちに必要なすべてだったとしたら、プログラミング言語はありませんまったく、さらには アセンブリ言語 もありません。私たちのCPUもチューリング完全であるため、コンピュータプログラマはすべて machine code と書くだけです。

bashが比較的単純なスクリプトを記述するためにほとんど独占的に使用されているのはなぜですか?

GNU Autoconfによって出力されるconfigureスクリプトなど)の大きくて複雑なシェルスクリプトは、多くの理由で非定型です。

  1. 比較的最近まで どこにでもPOSIX互換シェルを使用することはできませんでした

    多くのシステム、特に古いシステムは、技術的にはPOSIX互換シェルどこかをシステムに備えていますが、_/bin/sh_のような予測可能な場所にない場合があります。シェルスクリプトを作成していて、それを多くの異なるシステムで実行する必要がある場合、 Shebang line をどのように記述しますか? 1つのオプションは、先に進んで_/bin/sh_を使用することですが、そのようなシステムで実行される場合に備えて、POSIXより前のBourne Shell方言に制限することを選択します。

    POSIX以前のBourneシェルには、組み込みの演算さえありません。 expr または bc を呼び出す必要があります。

    POSIXシェルを使用しても、 連想配列 や、Perlが最初に普及して以来、Unixスクリプト言語に見られると期待していたその他の機能 1990年代初頭 がありません。 。

    その歴史の事実は、現代のボーンファミリーのシェルスクリプトインタープリターの強力な機能の多くを純粋に無視するという何十年にもわたる伝統があることを意味します。

    これは今日も続いています。実際、Bashは連想配列を取得しませんでした バージョン4まで ですが、まだ使用中のシステムがBash 3に基づいているのは驚くかもしれません。Appleは2017年も引き続きmacOSを搭載したBash 3を出荷します— 明らかにライセンス上の理由による —そして、Unix/Linuxサーバーは多くの場合、本番環境ではほとんど手つかずの状態で長時間動作するため、安定している可能性がありますCentOS 5ボックスなどの古いシステムがBash 3を実行している環境でそのようなシステムを使用している場合、それらで実行する必要があるシェルスクリプトで連想配列を使用することはできません。

    その問題に対する答えが「最新の」システム用のシェルスクリプトのみを作成することである場合、ほとんどのUnixシェルの最後の一般的な参照ポイントは POSIXシェル標準 であるという事実に対処する必要があります。これは、1989年に導入されて以来ほとんど変更されていません。その標準に基づいたさまざまなシェルがありますが、それらはすべて、その標準からさまざまな程度に分岐しています。連想配列を再度取得するために、bashzsh、および_ksh93_はすべてその機能を備えていますが、複数の実装の非互換性があります。その場合、選択肢はonly Bashを使用するか、またはonly Zshを使用するか、またはonly _ksh93_を使用します。

    その問題に対する答えが「Bash 4をインストールするだけ」、または_ksh93_、またはその他の場合は、Perlをインストールするだけでなく、PythonまたはRuby代わりに?それは多くの場合受け入れられません;デフォルトが重要です。

  2. Bourneファミリのシェルスクリプト言語はどれもサポートしていません modules

    シェルスクリプトでモジュールシステムに到達できる最も近いのは、_._コマンド(別名、より近代的なBourne Shellバリアントではsource)です。これは、適切なモジュールシステムと比較して複数のレベルで失敗します。基本は namespacing です。

    プログラミング言語に関係なく、より大きなプログラム全体の1つのファイルが数千行を超えると、人間の理解に問題が生じ始めます。大きなプログラムを多くのファイルに構造化する理由は、その内容を1つまたは2つの文に抽象化できるためです。ファイルAはコマンドラインパーサー、ファイルBはネットワークI/Oポンプ、ファイルCはライブラリZと残りのプログラム間のシムなどです。多くのファイルを1つのプログラムにアセンブルする唯一の方法がテキストインクルードの場合、あなたのプログラムが合理的に成長できる大きさに制限を加えます。

    比較すると、 Cプログラミング言語 にリンカーがなく、_#include_ステートメントだけがある場合のようになります。そのようなC-Lite方言は、externstaticなどのキーワードを必要としません。これらの機能は、モジュール化を可能にするために存在します。

  3. POSIX 方法を定義していません 変数を単一のシェルスクリプト関数にスコープし、ファイルにはスコープを限定しません

    これにより、実質的に すべての変数がグローバル となり、モジュール性と構成可能性が損なわれます。

    POSIX後のシェルには、これに対する解決策があります。確かに、少なくともbash、_ksh93_、およびzshにはありますが、上記のポイント1に戻るだけです。

    この影響は、スタイルガイドのGNU Autoconfマクロの記述で確認できます。ここで 推奨 変数名の前にマクロ自体の名前を付けて、非常に長い変数名は、衝突の可能性を許容できる限りゼロに近づけるためだけのものです。

    このスコアでは、1マイルでもCの方が優れています。ほとんどのCプログラムは主に関数ローカル変数で記述されているだけでなく、ブロックスコープもサポートしているため、単一の関数内の複数のブロックで、相互汚染なしに変数名を再利用できます。

  4. シェルプログラミング言語には標準ライブラリがありません。

    シェルスクリプト言語の標準ライブラリがPATHの内容であると主張することは可能ですが、それは、何らかの結果を得るには、シェルスクリプトが別のプログラム全体を呼び出す必要があることを示しています。 より強力 言語で始めます。

    また、Perlの [〜#〜] cpan [〜#〜] のように、シェルユーティリティライブラリの広く使用されているアーカイブもありません。サードパーティのユーティリティコードの利用可能な大規模なライブラリがないと、プログラマーは手動でより多くのコードを書かなければならないため、生産性が低下します。

    ほとんどのシェルスクリプトが有用な処理を行うために通常Cで記述された外部プログラムに依存しているという事実を無視しても、それらすべてのオーバーヘッドがあります pipe()fork()exec() 呼び出しチェーン。そのパターンは [〜#〜] ipc [〜#〜] と比較してUnixでかなり効率的であり、他のOSでプロセスを起動しますが、ここでは---で行うことを効果的に置き換えています サブルーチン呼び出し 別のスクリプト言語で使用します。これは、シェルスクリプトの実行速度の上限に深刻な上限を課します。

  5. シェルスクリプトには、並列実行によってパフォーマンスを向上させる組み込み機能がほとんどありません。

    Bourneシェルには、_&_、wait、およびパイプラインがありますが、これは、CPUまたはI/Oの並列処理を実現するためではなく、複数のプログラムを作成する場合にのみ役立ちます。 peg コアを使用したり、シェルスクリプトだけでRAIDアレイを飽和させたりすることはできません。そうすることで、他の言語ではるかに高いパフォーマンスを実現できるでしょう。

    特にパイプラインは、並列実行によってパフォーマンスを向上させるための弱い方法です。 2つのプログラムが並行して実行されるだけで、2つのプログラムのうちの1つは、任意の時点で他のプログラムとの間のI/Oで blocked になる可能性があります。

    _xargs -P_GNU parallel のように、これを回避するための方法は後日ありますが、これは上記の4に移ります。

    マルチプロセッサシステムを最大限に活用する組み込み機能が事実上ないため、シェルスクリプトは、システム内のすべてのプロセッサを使用できる言語で適切に記述されたプログラムよりも常に遅くなります。 GNU Autoconf configureスクリプトの例をもう一度見ると、システムのコア数を2倍にしても、実行速度はほとんど向上しません。

  6. シェルスクリプト言語には pointers または referencesがありません

    これにより、他のプログラミング言語で簡単に実行できる一連のことを実行できなくなります。

    まず、プログラムのメモリ内の別のデータ構造を間接的に参照できないことは、組み込みの データ構造 に制限されていることを意味します。あなたのシェルは 連想配列 を持っているかもしれませんが、それらはどのように実装されていますか?いくつかの可能性があり、それぞれ異なるトレードオフがあります。 red-black treesAVL trees 、および hash tables が最も一般的ですが、その他。異なるトレードオフのセットが必要な場合、参照がなければ、多くのタイプの高度なデータ構造を手動でロールする方法がないため、行き詰まります。あなたは与えられたもので立ち往生しています。

    または、シェルスクリプトインタープリターに適切な代替手段が組み込まれていない directed acyclic graph などのデータ構造が必要な場合もあります。モデルa ディペンデンシーグラフ 。私は何十年もプログラミングをしてきましたが、シェルスクリプトでそれを行うには、シンボリックリンクを偽の参照として使用して file system を悪用するしかありません。これは、チューリング完全性だけに依存しているときに得られるソリューションであり、ソリューションがエレガント、高速、または理解しやすいかどうかについては何もわかりません。

    高度なデータ構造は、ポインタと参照の1つの用途にすぎません。 それらの他のアプリケーションの山 があり、これはBourneファミリーのシェルスクリプト言語では簡単に実行できません。

私は何度も続けることができますが、あなたはここで要点を理解していると思います。簡単に言うと、Unixタイプのシステムには多くの より強力な プログラミング言語があります。

これは大きな利点であり、場合によっては言語自体の平凡さを補うことができます。

確かに、それがまさにGNU Autoconfがconfigureスクリプト出力にシェルスクリプト言語のBourneファミリの意図的に制限されたサブセットを使用する理由です。そのため、configureスクリプトはほぼどこでも実行されます。

GNU Autoconfの開発者よりも、移植性の高いBourne Shell方言で書くユーティリティの信者の大きなグループを見つけることはおそらくないでしょう。いくつかの _m4_ 、そしてシェルスクリプトのほんの少しだけ; Autoconfのoutputだけが純粋なBourne Shellスクリプトです。 「どこでもボーン」というコンセプトは便利ですが、どうなるかわかりません。

それで、そのようなプログラムがどれほど複雑になることができるのに限界がありますか?

技術的には、いいえ、チューリング完全性の観察が示唆するように。

ただし、これは、任意の大きさのシェルスクリプトは、作成が快適で、デバッグが容易で、実行が高速であると言っているのと同じではありません。

純粋なbashでファイルコンプレッサー/デコンプレッサーを書くことは可能ですか?

「純粋な」バッシュ、PATHにあるものへの呼び出しなしコンプレッサーはおそらくechoと16進エスケープシーケンスを使用して実行できますが、実行するのはかなり大変です。 シェルでバイナリデータを処理できない が原因で、解凍プログラムはその方法で書き込むことができない場合があります。バイナリデータをテキスト形式に変換するために od などを呼び出すことになります。これは、シェルのネイティブなデータ処理方法です。

意図したとおりにシェルスクリプトの使用について話し始めると、PATHで他のプログラムを駆動するための接着剤として、扉が開きます。これで、他のプログラミング言語で実行できることだけに制限されます。 、つまり、制限がまったくないということです。 PATH内の他のプログラムを呼び出すことですべての機能を発揮するシェルスクリプトは、より強力な言語で記述されたモノリシックプログラムほど高速ではありませんが、doesは実行されます。

そして、それがポイントです。高速で実行するプログラムが必要な場合、または他の人から力を借りるのではなく、それ自体で強力である必要がある場合は、シェルでプログラムを記述しないでください。

シンプルなビデオゲーム?

これが シェルのテトリス です。あなたが探しに行くなら、他のそのようなゲームが利用可能です。

非常に限られたデバッグツールしかありません

大規模なプログラミングをサポートするために必要な機能のリストで、デバッグツールのサポートを約20位にしました。言語に関係なく、多くのプログラマーは適切なデバッガーよりも printf() debugging に大きく依存しています。

シェルには、echoと_set -x_があり、これらを組み合わせて非常に多くの問題をデバッグできます。

30
Warren Young

どこにでも歩いたり泳いだりできるのに、なぜ自転車、車、電車、ボート、飛行機、その他の乗り物に悩むのでしょうか。確かに、ウォーキングや水泳は疲れる可能性がありますが、追加の機器を必要としないことには大きな利点があります。

1つには、bashはチューリング完全ですが、整数(大きすぎない)、文字列、(1次元)文字列の配列、および文字列から文字列への有限マップ以外のデータの操作は得意ではありません。他の種類のデータには、面倒なエンコーディングが必要です。これにより、プログラムの作成が難しくなり、実際には十分ではないパフォーマンスが課せられることがよくあります。たとえば、bashでの浮動小数点演算は困難で低速です。

さらに、bashにはその環境と対話する方法がほとんどありません。プロセスを実行したり、(リダイレクトを介して)単純な種類のファイルアクセスを実行したりできます。 Bashには、クライアント側のネットワーククライアントもあります。 bashはnullバイトを簡単に出力できますが(printf \\0)、その入力のnullバイトを解析しないため、バイナリデータの読み取りに適していません。 Bashは他のことを直接行うことはできません。そのために外部プログラムを呼び出す必要があります。そして、それは問題ありません:シェルは、外部プログラムを実行する主な目的のために設計されています!シェルは、プログラムを組み合わせるための接着剤言語です。ただし、外部プログラムを実行している場合は、そのプログラムを使用できる必要があります。つまり、移植性の利点が減少します。どこでも使用できるいくつかのプログラム(ほとんど POSIXユーティリティ に固執する必要があります。 )。

Bashには、set -eを除いて、堅牢なプログラムを簡単に作成できるような機能はありません。 (有用な)タイプ、名前空間、モジュール、またはネストされたデータ構造はありません。バグはプログラミングの最大の困難です。バグのないプログラムを簡単に作成できることが言語を選択する際の決定的な要因になるとは限りませんが、bashはその点でランク付けが不十分です。また、プログラムを組み合わせる以外のことを行うと、Bashはパフォーマンスのランクが低くなります。

長い間、bashはWindowsでは実行されませんでした。現在でも、デフォルトのWindowsインストールには存在せず、(WSLでも)ネイティブに完全には実行されません。 Windowsのネイティブ機能。 BashはiOSでは実行されず、Androidにはデフォルトでインストールされません。したがって、Unixのみのアプリケーションを作成しているのでない限り、bashはまったく移植可能ではありません。

コンパイラーの必要性は、移植性の問題ではありません。コンパイラは開発者のマシンで実行されます。インタープリターまたはサードパーティのライブラリーを要求することは問題になる可能性がありますが、Linuxでは、配布パッケージによって解決された問題です。Windowsでは、AndroidおよびiOSでは、通常、サードパーティのコンポーネントをバンドルしますしたがって、移植性に関するこの種の懸念は、ありふれたアプリケーションの実用的な問題ではありません。

私の答えはbash以外のシェルに適用されます。いくつかの詳細はシェルごとに異なりますが、一般的な考え方は同じです。

大規模なプログラムでシェルスクリプトを使用しないいくつかの理由は、私の頭の先にあります。

  • ほとんどの機能は、遅い外部コマンドをforkすることで実行されます。対照的に、Perlのようなプログラミング言語は、内部でmkdirまたはgrepと同等のことを実行できます。
  • Cライブラリにアクセスしたり、直接システムコールを行ったりする簡単な方法はありません。ビデオゲームを作成するのは難しいでしょう
  • 適切なプログラミング言語は、複雑なデータ構造をより適切にサポートします。 Bashには配列と連想配列がありますが、リンクリストやツリーについては考えたくありません。
  • シェルは、テキストの場合に作成されるコマンドを処理するために作成されます。バイナリデータ(つまり、NULバイト(値がゼロのバイト)を含む変数)は、処理が困難です。シェルによって多少異なりますが、zshはいくつかのサポートを備えています。これは、外部プログラムのインターフェイスがほとんどテキストベースであり、\0がセパレーターとして使用されているためでもあります。
  • また、外部コマンドのため、コードとデータの分離は少し困難です。別のシェルにデータを引用するときのすべての問題を目撃してください(つまり、bash -c ...またはssh -c ...を実行しているとき)
7
ilkkachu