我想到咨询GNU Bash手册这种洞察力,但它似乎不是我想要的;它更多的是句法糖的洗衣单,而不是核心语义模型的解释。百万和一个“bash教程”在线只是更糟。也许我应该先学习sh,并理解Bash作为一个语法糖顶部这一点?我不知道这是否是一个准确的模型,但。
有什么建议么?
编辑:我被要求提供的例子,理想情况下,我想找。一个相当极端的例子,我认为一个“正式语义”是this paper on “the essence of JavaScript”.也许稍微不太正式的例子是Haskell 2010 report。
shell语法的另一个根源来自它作为单独的UNIX实用程序的混乱的成长。大多数内置shell通常是内部实际上可以实现为外部命令。当它们意识到/ bin / [存在于许多系统上时,它会抛出很多shell初始化的循环。
$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X,without the `]` t
wat?
如果你看一下shell是如何实现的,这更有意义。这里是我做的一个练习。它在Python,但我希望这不是一个hangup的任何人。这不是很强大,但它是有启发性的:
#!/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()
我希望上面的内容清楚地说明了shell的执行模型:
1. Expand words. 2. Assume the first word is a command. 3. Execute that command with the following words as arguments.
扩展,命令解析,执行。所有的shell语义都绑定在这三个东西之一,虽然他们比我上面写的实现更丰富。
不是所有的命令fork。事实上,有一些命令,不使a ton of sense实现为外部(这样他们将不得不fork),但即使是那些经常可用作外部严格的POSIX兼容性。
Bash通过添加新的功能和关键字来增强POSIX shell来建立这个基础。它几乎与sh兼容,并且bash是如此无处不在,一些脚本作者走了多年没有意识到一个脚本实际上可能不工作在POSIX严格的系统。 (我也想知道人们如何能够如此关心一种编程语言的语义和风格,而对于shell的语义和风格这么少,但我不同)。
评估顺序
这是一个棘手的问题:Bash解释表达式在其主要语法从左到右,但在其算术语法它遵循C优先级。表达式与扩展不同。从bash手册的EXPANSION部分:
The order of expansions is: brace expansion; tilde expansion,parameter
and variable expansion,arithmetic expansion,and command substitution
(done in a left-to-right fashion); word splitting; and pathname expansion.
如果你理解文字分割,路径名扩展和参数扩展,你很好地了解大多数bash的方式。请注意,路径名扩展来自后面的字符分割是至关重要的,因为它确保名称中的空格的文件仍然可以匹配glob。这就是为什么好的使用glob扩展比parsing commands,一般。
范围
功能范围
很像旧的ECMAscript,shell有动态作用域,除非你在函数中明确声明名字。
$ foo() { echo $x; } $ bar() { local x; echo $x; } $ foo $ bar $ x=123 $ foo 123 $ bar $ …
环境与过程“范围”
Subshell继承其父shell的变量,但其他种类的进程不继承未导出的名称。
$ 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
有关扩展的更多信息,请阅读手册的参数扩展部分。它相当强大。
整数和算术表达式
您可以使用整数属性来填充名称,以便让shell将赋值表达式的右侧视为算术。然后,当参数扩展时,它将被计算为整数数学,然后扩展为…一个字符串。
$ 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等访问shell脚本的参数。您可以使用“$ @”一次访问所有这些参数,该扩展与数组有许多共同之处。您可以使用set或shift内置函数设置和更改位置参数,或者只需使用以下参数调用shell或shell函数:
$ 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 $#,但它是一个编号参数,所以meh。 $ 0是shell或当前shell脚本的名称。
数组
数组的语法是在位置参数之后建模的,所以如果你喜欢,将数组看作一种命名类型的“外部位置参数”是最健康的。数组可以使用以下方法声明:
$ 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 $ …
如果使用引号或反斜杠来防止字符分割,数组将维护指定的wordsplitting:
$ foo=( 'elementa b c' 'd e f' ) $ echo "${#foo[@]}" 2
数组和位置参数之间的主要区别是:
>位置参数不稀疏。如果设置了$ 12,您也可以确保设置了$ 11。 (可以设置为空字符串,但$#不会小于12.)如果设置了“$ {arr [12]}”,则不能保证设置了“$ {arr [11]}”并且阵列的长度可以小到1。
>数组的第零个元素明确是该数组的第零个元素。在位置参数中,第零个元素不是第一个参数,而是shell或shell脚本的名称。
>要移动数组,你必须切割和重新分配它,如arr =(“$ {arr [@]:1}”)。你也可以unset arr [0],但这将使第一个元素在索引1。
>数组可以作为全局变量在shell函数之间隐式共享,但是你必须将位置参数显式地传递给shell函数,以便查看它们。
使用路径名扩展来创建文件名数组通常很方便:
$ dirs=( */ )
命令
命令是关键,但它们也覆盖在比我手册更好的深度。阅读SHELL GRAMMAR部分。不同类型的命令是:
>简单命令(例如$ startx)
>管道(例如$ yes | make config)(lol)
>列表(例如$ grep -qF foo file&& sed’s / foo / bar /’file> newfile)
>复合命令(例如$(cd -P / var / www / webroot&& echo“webroot is $ PWD”))
>协处理(复杂,无示例)
>函数(可以被视为简单命令的命名复合命令)
执行模型
执行模型当然涉及堆和堆栈。这是所有UNIX程序的特有。 Bash还有一个用于shell函数的调用堆栈,通过嵌套使用调用程序内置函数可见。
参考文献:
> bash手册的SHELL GRAMMAR部分
> XCU Shell Command Language文档
>在Greycat的wiki上的Bash Guide。
> Advanced Programming in the UNIX Environment
如果你想让我在一个特定的方向进一步扩大,请作出评论。