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


87

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

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

助言がありますか?

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



2
bashに「コアセマンティックモデル」があるとは確信していません(まあ、おそらく「ほとんどすべてが文字列です」)。私はそれがずっとずっと構文糖衣だと思います。
ゴードンデイヴィソン2014

4
「シンタックスシュガーの洗濯物リスト」と呼ばれるものは、実際には拡張のセマンティックモデルであり、実行の非常に重要な部分です。バグと混乱の90%は、拡張モデルを理解していないことが原因です。
その他の男

4
シェルスクリプトの書き方のように読んだら、なぜこれが幅広い質問だと思うのかがわかります。しかし、本当の問題は、特にシェル言語とbashの正式なセマンティクスと基礎何ですか?、そしてそれは単一の首尾一貫した答えを持つ良い質問です。再開するための投票。
kojiro 2014

1
私はlinuxcommand.orgでかなりのことを学びました。また、コマンドラインシェルスクリプトの記述に関するより詳細な本の無料のPDFもあります
samrap 2014

回答:


107

シェルは、オペレーティングシステムのインターフェイスです。これは通常、それ自体が多かれ少なかれ堅牢なプログラミング言語ですが、オペレーティングシステムやファイルシステムとの対話を容易にするように設計された機能を備えています。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の優先順位に従います。ただし、式は拡張とは異なります。EXPANSIONbashマニュアルのセクションから:

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

ワードスプリット、パス名の展開、パラメーターの展開を理解していれば、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には実際には型がなく、すべてが文字列に展開されます(または、単語の方が適切かもしれません)。しかし、さまざまな種類の展開を調べてみましょう。

文字列

ほとんどすべてのものを文字列として扱うことができます。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、位置パラメータと呼ばれることもあります。引数の数に含まれていないので、これは紛らわしいと思います$#が、番号付きのパラメーターなので、まあ。$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. SHELL GRAMMARbashマニュアルのセクション
  2. XCUシェルコマンド言語ドキュメント
  3. GreycatのwikiのBashガイド
  4. UNIX環境での高度なプログラミング

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


16
+1:すばらしい説明。例を挙げてこれを書くのに費やした時間を感謝してください。
jaypal singh 2014

+1 for yes | make config;-)しかし、真剣に、非常に良い記事です。
デジタルトラウマ

これを読み始めたばかりです。コメントを残します。1)あなたがいることを見たときに、さらに大きな驚きが来る/bin/[/bin/test、多くの場合、同じapplicaton 2である)「最初の単語がコマンドであると仮定します。」-あなたが割り当て...行うときに期待
カロリー・ホーバス

@KarolyHorvathはい、変数は複雑な混乱であるため、デモシェルから意図的に割り当てを除外しました。そのデモシェルは、この答えを念頭に置いて書かれていませんでした–それはずっと以前に書かれました。私はそれを作りexecle、最初の単語を環境に補間することができると思いますが、それでもそれはかなり複雑になります。
kojiro 2014

@kojiro:いや、それは複雑すぎるでしょう、それは確かに私の意図ではありませんでした!しかし、割り当ての動作は少し異なります(x)。私見では、テキストのどこかに言及する必要があります。(x):そしていくつかの混乱の原因...私は人々がa = 1働いていないことについて不平を言っているのを見た回数をもう数えることさえできません)。
Karoly Horvath 2014

5

あなたの質問への答え「タイピングの分野は何ですか、例えばすべてが文字列です」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環境を介して後続のコマンドにエクスポートするために各変数をマークします。

1

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.