比我知道的任何其他语言,我每次需要一些小东西时“学会”了Googling的Bash。因此,我可以拼凑在一起似乎工作的小脚本。然而,我真的不知道发生了什么,我希望更正式地介绍Bash作为一种编程语言。例如:什么是评估顺序?什么是范围规则?什么是打字规则,例如。是一个字符串吗?什么是程序的状态 – 是字符串到变量名的键值分配;有多少,例如。堆栈?有堆吗?等等。
我想到咨询GNU Bash手册这种洞察力,但它似乎不是我想要的;它更多的是句法糖的洗衣单,而不是核心语义模型的解释。百万和一个“bash教程”在线只是更糟。也许我应该先学习sh,并理解Bash作为一个语法糖顶部这一点?我不知道这是否是一个准确的模型,但。
有什么建议么?
编辑:我被要求提供的例子,理想情况下,我想找。一个相当极端的例子,我认为一个“正式语义”是this paper on “the essence of JavaScript”.也许稍微不太正式的例子是Haskell 2010 report。
@H_
404_8@
shell是操作系统的接口。它通常是一个或多或少的健壮的编程语言本身,但具有旨在使其易于与操作系统和
文件系统进行特别交互的
功能。 POSIX shell(以下简称“shell”)语义是一个mutt,结合了LISP的一些特性(s-expressions和shell
word splitting有很多共同之处)和C(shell的大部分
arithmetic syntax语义从C)。
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
如果你想让我在一个特定的方向进一步扩大,请作出评论。