[解決済み] Bashスクリプトのセマンティクス?
質問
私が知っている他のどの言語よりも、私は何か小さなことが必要になるたびにググることによって 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
配列と位置パラメータの主な違いです。
-
位置パラメーターはスパースではありません。もし
$12
が設定されている場合は$11
も設定されていることを確認できます。(空の文字列が設定されている可能性もありますが$#
は12より小さくはならない)。もし"${arr[12]}"
が設定されている場合、以下の保証はありません。"${arr[11]}"
が設定され、配列の長さは 1 のように小さくなる可能性があります。 - 配列の 0 番目の要素は、その配列の 0 番目の要素であることを明確にします。位置パラメーターでは、0番目の要素は 最初の引数 でなく、シェルまたはシェルスクリプトの名前です。
-
には
shift
のように、配列をスライスして再代入する必要があります。arr=( "${arr[@]:1}" )
. また、次のようにすることもできます。unset arr[0]
とすることもできますが、そうすると最初の要素がインデックス 1 になってしまいます。 - 配列はグローバルとしてシェル関数間で暗黙的に共有できますが、それらを見るためにシェル関数に位置パラメータを明示的に渡す必要があります。
ファイル名の配列を作成するためにパス名展開を使用するのは、しばしば便利です。
$ dirs=( */ )
コマンド
コマンドはキーポイントですが、マニュアルで説明するよりも深く掘り下げて説明されています。このページでは
SHELL GRAMMAR
セクションを読んでください。コマンドの種類は
-
単純なコマンド(例
$ startx
) -
パイプライン (例
$ yes | make config
) (笑) -
リスト(例
$ grep -qF foo file && sed 's/foo/bar/' file > newfile
) -
複合コマンド(例
$ ( cd -P /var/www/webroot && echo "webroot is $PWD" )
) - コプロセス (複雑、例なし)
- ファンクション (単純なコマンドとして扱える、名前の付いた複合コマンド)
実行モデル
実行モデルにはもちろんヒープとスタックの両方が含まれます。これはすべての UNIX プログラムに共通するものです。Bash はシェル関数のためのコールスタックも持っており、ネストされた
caller
をネストすることで見ることができます。
リファレンスです。
-
は
SHELL GRAMMAR
のセクションは、bash マニュアルの - は XCU シェルコマンド言語 ドキュメント
- は Bashガイド を Greycat の wiki に追加しました。
- UNIX 環境での高度なプログラミング
特定の方向にさらに広げて欲しい場合は、コメントをお願いします。
関連
-
[解決済み] Bashスクリプトのソースディレクトリをスクリプト自体から取得するにはどうすればよいですか?
-
[解決済み] Bashシェルスクリプトでディレクトリが存在するかどうかを確認するにはどうすればよいですか?
-
[解決済み] Bashで通常のファイルが存在しないかどうかを判断する方法を教えてください。
-
[解決済み] Bashで文字列変数を連結する方法
-
[解決済み] Bashで文字列が部分文字列を含むかどうかをチェックする方法
-
[解決済み] Bash prints リテラルの改行をエコーする \n
-
[解決済み] Bashスクリプトからプログラムが存在するかどうかを確認するにはどうすればよいですか?
-
[解決済み] Bashで文字列をデリミターで分割するには?
-
[解決済み] Bashでコマンドライン引数を解析するには?
-
[解決済み】Bashでファイル名と拡張子を抽出する。
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み] bashのループリストで空白をエスケープするには?
-
[解決済み] bashでWebサーバーの応答を待つループを作成するには?
-
[解決済み] Ansibleで新しいユーザーとパスワードを作成する
-
[解決済み] bash で "set -o nounset" を使用したときに変数が設定されているかどうかをテストする。
-
[解決済み] ssh 鍵を生成する bash スクリプトの enter キー押下を自動化する。
-
[解決済み] Bashの引数で引用符を維持するには?重複
-
[解決済み] シェルのワイルドカード文字展開を停止しますか?
-
[解決済み] Bashのファイル拡張子は何ですか?
-
[解決済み] SSH接続をチェックするbashスクリプトを作成するには?
-
[解決済み] 引用符で囲まれたパラメータを受け取り、再送するためのBashスクリプト