web-dev-qa-db-ja.com

Bashスクリプトのセマンティクス?

私が知っている他のどの言語よりも、私は何か小さなことが必要になるたびにグーグルでバッシュを「学びました」。その結果、機能しているように見える小さなスクリプトを一緒にパッチワークすることができます。しかし、私は本当に何が起こっているのかわからないので、プログラミング言語としてのBashのより正式な紹介を望んでいました。例:評価順序は何ですか?スコープルールは何ですか?タイピングの分野は何ですか、例:すべてが文字列ですか?プログラムの状態は何ですか?変数名への文字列のキーと値の割り当てです。それ以上のものがありますか?スタック?ヒープはありますか?等々。

この種の洞察についてはGNU Bashマニュアルを参照することを考えましたが、それは私が望んでいるものではないようです。これは、構文の説明というよりは、構文糖衣の洗濯物リストです。コアセマンティックモデル。オンラインでの百万と1つの「bashチュートリアル」はもっと悪いだけです。おそらく最初にshを研究し、これに加えてBashを糖衣構文として理解する必要がありますか?これかどうかはわかりませんただし、正確なモデルです。

助言がありますか?

編集:私は理想的に私が探しているものの例を提供するように頼まれました。私が「形式的セマンティクス」と見なすもののかなり極端な例は 「JavaScriptの本質」に関するこの論文 です。おそらく、少し形式的でない例は Haskell 2010レポート です。

86
jameshfisher

シェルは、オペレーティングシステムのインターフェイスです。これは通常、それ自体が多かれ少なかれ堅牢なプログラミング言語ですが、オペレーティングシステムやファイルシステムとの対話を容易にするように設計された機能を備えています。 POSIXシェル(以下、単に「シェル」と呼びます)のセマンティクスは、LISPのいくつかの機能(S式はシェルと多くの共通点があります 単語分割 )とCを組み合わせた、ちょっとした意味です。 (シェルの 算術構文 セマンティクスの多くはCから来ています)。

シェルの構文のもう1つのルートは、個々のUNIXユーティリティのミッシュマッシュとしての育成に由来します。シェルに組み込まれていることが多いもののほとんどは、実際には外部コマンドとして実装できます。 _/bin/[_が多くのシステムに存在することに気付いた場合、ループに対して多くのシェルネオファイトをスローします。

_$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X, without the `]`
t
_

ワット?

シェルがどのように実装されているかを見ると、これははるかに理にかなっています。これが私が演習として行った実装です。それはPythonですが、それが誰にとってもハングアップではないことを願っています。それほど堅牢ではありませんが、有益です。

_#!/usr/bin/env python

from __future__ import print_function
import os, sys

'''Hacky barebones Shell.'''

try:
  input=raw_input
except NameError:
  pass

def main():
  while True:
    cmd = input('Prompt> ')
    args = cmd.split()
    if not args:
      continue
    cpid = os.fork()
    if cpid == 0:
      # We're in a child process
      os.execl(args[0], *args)
    else:
      os.waitpid(cpid, 0)

if __name__ == '__main__':
  main()
_

上記により、シェルの実行モデルがほぼ同じであることが明確になることを願っています。

_1. Expand words.
2. Assume the first Word is a command.
3. Execute that command with the following words as arguments.
_

拡張、コマンド解決、実行。シェルのセマンティクスはすべて、上記の実装よりもはるかに豊富ですが、これら3つのいずれかにバインドされています。

すべてのコマンドがforkというわけではありません。実際、 意味のある 外部として実装されないコマンドがいくつかあります(forkを実行する必要があるように)が、それらでさえ、多くの場合、次のように利用できます。 POSIXに厳密に準拠するための外観。

Bashは、POSIXシェルを強化するための新しい機能とキーワードを追加することにより、このベースに基づいて構築されています。これはshとほぼ互換性があり、bashは非常にユビキタスであるため、一部のスクリプト作成者は、スクリプトがPOSIXlyの厳密なシステムで実際に機能しない可能性があることに気付かずに何年もかかります。 (また、人々が1つのプログラミング言語のセマンティクスとスタイルにそれほど関心があり、シェルのセマンティクスとスタイルにはほとんど関心がないのではないかと思いますが、私は分岐しています。)

評価の順序

これはちょっとしたトリックの質問です。Bashは、プライマリ構文の式を左から右に解釈しますが、算術構文ではCの優先順位に従います。ただし、式は拡張とは異なります。 bashマニュアルのEXPANSIONセクションから:

拡張の順序は次のとおりです。ブレース拡張。チルダ展開、パラメーターと変数の展開、算術展開、およびコマンド置換(左から右の方法で実行)。単語分割;およびパス名の展開。

ワードスプリット、パス名の展開、パラメーターの展開を理解していれば、bashの機能のほとんどを理解することができます。名前に空白が含まれているファイルをglobと照合できるようにするため、ワード分割後にパス名を展開することが重要であることに注意してください。これが、一般的に、glob展開の適切な使用が コマンドの解析 よりも優れている理由です。

範囲

関数スコープ

古いECMAscriptと同様に、関数内で名前を明示的に宣言しない限り、シェルには動的スコープがあります。

_$ foo() { echo $x; }
$ bar() { local x; echo $x; }
$ foo

$ bar

$ x=123
$ foo
123
$ bar

$ …
_

環境とプロセスの「範囲」

サブシェルは親シェルの変数を継承しますが、他の種類のプロセスはエクスポートされていない名前を継承しません。

_$ x=123
$ ( echo $x )
123
$ bash -c 'echo $x'

$ export x
$ bash -c 'echo $x'
123
$ y=123 bash -c 'echo $y' # another way to transiently export a name
123
_

これらのスコープルールを組み合わせることができます。

_$ foo() {
>   local -x bar=123 # Export foo, but only in this scope
>   bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar

$
_

規律の入力

ええと、タイプ。ええBashには実際には型がなく、すべてが文字列に展開されます(または、おそらくWordの方が適切です)。しかし、さまざまな型を調べてみましょう。拡張。

文字列

ほとんどすべてのものを文字列として扱うことができます。 bashのベアワードは文字列であり、その意味は適用される展開に完全に依存します。

裸の単語が実際には単なる単語であり、引用符はそれについて何も変わらないことを示すことは価値があるかもしれません。

_$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
_
_$ fail='echoes'
$ set -x # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World
_

拡張の詳細については、マニュアルの_Parameter Expansion_セクションをお読みください。それは非常に強力です。

整数と算術式

名前にinteger属性を吹き込んで、代入式の右辺を算術として扱うようにシェルに指示できます。次に、パラメータが展開されると、文字列に展開される前に整数演算として評価されます。

_$ foo=10+10
$ echo $foo
10+10
$ declare -i foo
$ foo=$foo # Must re-evaluate the assignment
$ echo $foo
20
$ echo "${foo:0:1}" # Still just a string
2
_

配列

配列について説明する前に、位置パラメータについて説明する価値があるかもしれません。シェルスクリプトの引数には、番号付きパラメーター_$1_、_$2_、_$3_などを使用してアクセスできます。これらすべてのパラメーターには、_"$@"_を使用して一度にアクセスできます。配列と多くの共通点があります。 setまたはshiftビルトインを使用するか、次のパラメーターを使用してシェルまたはシェル関数を呼び出すだけで、位置パラメーターを設定および変更できます。

_$ bash -c 'for ((i=1;i<=$#;i++)); do
>   printf "\$%d => %s\n" "$i" "${@:i:1}"
> done' -- foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showpp() {
>   local i
>   for ((i=1;i<=$#;i++)); do
>     printf '$%d => %s\n' "$i" "${@:i:1}"
>   done
> }
$ showpp foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showshift() {
>   shift 3
>   showpp "$@"
> }
$ showshift foo bar baz biz quux xyzzy
$1 => biz
$2 => quux
$3 => xyzzy
_

Bashのマニュアルでは、位置パラメータとして_$0_を参照することもあります。引数count _$#_に含まれていないため、これは紛らわしいと思いますが、番号付きのパラメーターなので、まあ。 _$0_は、シェルまたは現在のシェルスクリプトの名前です。

配列の構文は位置パラメーターに基づいてモデル化されているため、必要に応じて、配列を名前付きの「外部位置パラメーター」と考えるのが最も適切です。配列は、次のアプローチを使用して宣言できます。

_$ foo=( element0 element1 element2 )
$ bar[3]=element3
$ baz=( [12]=element12 [0]=element0 )
_

インデックスで配列要素にアクセスできます。

_$ echo "${foo[1]}"
element1
_

配列をスライスできます。

_$ printf '"%s"\n' "${foo[@]:1}"
"element1"
"element2"
_

配列を通常のパラメーターとして扱う場合、ゼロ番目のインデックスを取得します。

_$ echo "$baz"
element0
$ echo "$bar" # Even if the zeroth index isn't set

$ …
_

引用符または円記号を使用して単語の分割を防ぐ場合、配列は指定された単語の分割を維持します。

_$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2
_

配列と位置パラメータの主な違いは次のとおりです。

  1. 位置パラメータはスパースではありません。 _$12_が設定されている場合は、_$11_も設定されていることを確認できます。 (空の文字列に設定することもできますが、_$#_は12より小さくなりません。)_"${arr[12]}"_が設定されている場合、_"${arr[11]}"_が設定されている保証はなく、配列は1まで小さくすることができます。
  2. 配列の0番目の要素は、明確にその配列の0番目の要素です。位置パラメータでは、0番目の要素は最初の引数ではなく、シェルまたはシェルスクリプトの名前です。
  3. 配列をshiftするには、arr=( "${arr[@]:1}" )のように、配列をスライスして再割り当てする必要があります。 _unset arr[0]_を実行することもできますが、これにより、インデックス1の最初の要素が作成されます。
  4. 配列は、グローバルとしてシェル関数間で暗黙的に共有できますが、それらを表示するには、シェル関数に位置パラメーターを明示的に渡す必要があります。

パス名展開を使用してファイル名の配列を作成すると便利なことがよくあります。

_$ dirs=( */ )
_

コマンド

コマンドは重要ですが、マニュアルよりも詳細に説明されています。 _Shell GRAMMAR_セクションをお読みください。さまざまな種類のコマンドは次のとおりです。

  1. 単純なコマンド(例:_$ startx_)
  2. パイプライン(例:_$ yes | make config_)(笑)
  3. リスト(例:_$ grep -qF foo file && sed 's/foo/bar/' file > newfile_)
  4. 複合コマンド(例:$ ( cd -P /var/www/webroot && echo "webroot is $PWD" )
  5. コプロセス(複雑、例なし)
  6. 関数(単純なコマンドとして扱うことができる名前付きの複合コマンド)

実行モデル

もちろん、実行モデルにはヒープとスタックの両方が含まれます。これは、すべてのUNIXプログラムに固有のものです。 Bashには、シェル関数の呼び出しスタックもあり、callerビルトインをネストして使用することで表示できます。

参照:

  1. Bashマニュアルの_Shell GRAMMAR_セクション
  2. XCUシェルコマンド言語 ドキュメント
  3. Bashガイド グレイキャットのウィキ。
  4. NIX環境での高度なプログラミング

特定の方向にさらに拡大してほしい場合は、コメントをお願いします。

106
kojiro

あなたの質問への答え「タイピングの分野は何ですか、例えば、すべてが文字列です」Bash変数は文字列です。ただし、変数が整数の場合、Bashでは変数の算術演算と比較が可能です。ルールBash変数の例外は文字列であり、変数がタイプセットされているか、そうでない場合は宣言されています

$ A=10/2
$ echo "A = $A"           # Variable A acting like a String.
A = 10/2

$ B=1
$ let B="$B+1"            # Let is internal to bash.
$ echo "B = $B"           # One is added to B was Behaving as an integer.
B = 2

$ A=1024                  # A Defaults to string
$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ echo "B = $B"           # $B STRING is a string
B = 10STRING01

$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ declare -i B
$ echo "B = $B"           # Declaring a variable with non-integers in it doesn't change the contents.
B = 10STRING01

$ B=${B/STRING01/24}      # Substitute "STRING01"  with "24".
$ echo "B = $B"
B = 1024

$ declare -i B=10/2       # Declare B and assigning it an integer value
$ echo "B = $B"           # Variable B behaving as an Integer
B = 5

オプションの意味を宣言する:

  • -変数は配列です。
  • -f関数名のみを使用します。
  • -i変数は整数として扱われます。算術評価は、変数に値が割り当てられたときに実行されます。
  • -p各変数の属性と値を表示します。 -pを使用すると、追加のオプションは無視されます。
  • -r変数を読み取り専用にします。これらの変数は、後続の割り当てステートメントによって値を割り当てることも、設定を解除することもできません。
  • -t各変数にtrace属性を与えます。
  • -x環境を介して後続のコマンドにエクスポートするために各変数をマークします。
5
Keith Reynolds

Bashのマンページには、ほとんどのマンページよりもかなり多くの情報があり、あなたが求めているもののいくつかが含まれています。 10年以上のスクリプトbashの後の私の仮定は、shの拡張としての歴史のために、いくつかのファンキーな構文を持っているということです(shとの下位互換性を維持するため)。

FWIW、私の経験はあなたのようなものでした。さまざまな本(O'Reillyの「Learningthe Bash Shell」など)は構文に役立ちますが、さまざまな問題を解決するための奇妙な方法がたくさんあり、それらのいくつかは本になく、グーグルで検索する必要があります。

1
philwalk