1. ホーム
  2. bash

[解決済み] Bashスクリプトのセマンティクス?

2023-03-23 15:33:13

質問

私が知っている他のどの言語よりも、私は何か小さなことが必要になるたびにググることによって Bash を学びました。その結果、私は、動作するように見える小さなスクリプトをつなぎ合わせて作ることができます。しかし、私は 本当に 私はプログラミング言語としての Bash をもっと正式に紹介することを望んでいました。たとえば 評価順序はどうなっているのか、スコープルールはどうなっているのか。型付けはどうなっているのか、たとえば、すべては文字列なのか。プログラムの状態はどうなっているのか -- 変数名への文字列のキーバリューアサインなのか、それ以上のもの、たとえばスタックはあるのか。ヒープがあるのか?などなど。

この種の洞察を得るために GNU Bash のマニュアルを参照しようと思いましたが、それは私が欲しいものではなさそうです。オンラインにある100万以上のquot;bash tutorials"はもっとひどいです。おそらく私は最初に sh を勉強して、その上でBashを構文上の糖分として理解すべきなのでしょうか?これが正確なモデルであるかどうかは分かりませんが。

何か提案はありますか?

EDITです。 私が求めているのは理想的な例だと聞いています。私が「形式的意味論」と考えるもののかなり極端な例として、次のようなものがあります。 JavaScript の本質に関するこの論文です。 . おそらく、もう少し形式的でない例としては Haskell 2010 レポート .

どのように解決するのですか?

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

シェルの構文のもうひとつのルーツは、個々のUNIXユーティリティの寄せ集めとして育ったことに由来しています。シェルにしばしば組み込まれるもののほとんどは、実際には外部コマンドとして実装することができます。シェルの初心者が /bin/[ が多くのシステム上に存在することに気づいたとき、多くのシェル初心者はループに陥ります。

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

wat?

これは、シェルがどのように実装されているかを見れば、より理解しやすいでしょう。ここに私が練習として行った実装があります。これは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 は非常にユビキタスなので、スクリプトの作者の中には、スクリプトが POSIX に厳密に準拠したシステムで実際に動作しないかもしれないことに何年も気づかない人がいます。(あるプログラミング言語のセマンティクスとスタイルにこれほどまでに関心を持ち、シェルのセマンティクスとスタイルにこれほどまでに関心を持たない人がいることも不思議ですが、私は発散します)。

評価の順序

これはちょっとしたトリックのような質問です。Bash はその主要な構文で式を左から右へと解釈しますが、算術構文では C 言語の優先順位に従います。式は 展開 とは異なります。から EXPANSION のセクションを参照してください。

展開の順序は、波括弧の展開、チルダの展開、パラメータと変数の展開、演算の展開、コマンドの代入の順です。 と変数の展開、算術展開、コマンド置換(左から右へ展開)、単語分割、パス名展開 (左から右へ)、単語の分割、そしてパス名の展開です。

単語分割、パス名展開、パラメータ展開が理解できれば、bashが行うことのほとんどを理解することができます。単語分割の後に来るパス名展開は重要で、ファイル名に空白があってもグロブでマッチできるようにするからです。このため、グロブ展開をうまく使うほうが パース・コマンド よりも優れている理由です。

スコープ

機能の範囲

昔の 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におけるbarewordsは、その意味が適用された展開に完全に依存する文字列です。

展開なし

裸の単語は本当にただの単語であり、引用符はそれについて何も変えないということを示すのは価値があるかもしれません。

$ 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 のセクションを読んでください。かなり強力です。

整数と算術式

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

$ 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"

配列を通常のパラメータとして扱うと、0番目のインデックスを取得することになります。

$ 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 GRAMMAR のセクションは、bash マニュアルの
  2. XCU シェルコマンド言語 ドキュメント
  3. Bashガイド を Greycat の wiki に追加しました。
  4. UNIX 環境での高度なプログラミング

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