与我所知道的任何其他语言相比,每次我需要一些小东西时,我都会通过谷歌搜索“学习”Bash。因此,我可以将看似有效的小脚本拼凑在一起。然而,我真的不知道发生了什么,我希望能更正式地介绍 Bash 作为一种编程语言。例如:评估顺序是什么?什么是范围规则?什么是打字规则,例如一切都是字符串吗?程序的状态是什么——它是字符串到变量名的键值分配吗?还有更多吗,例如堆栈?有堆吗?等等。
我想查阅 GNU Bash 手册以获得这种见解,但这似乎不是我想要的;它更像是一个语法糖的 list ,而不是对核心语义模型的解释。在线的百万零一“bash 教程”只会更糟。也许我应该首先研究 sh
,并将 Bash 理解为最重要的语法糖?不过,我不知道这是否是一个准确的模型。
有什么建议么?
编辑: 我被要求提供理想情况下我正在寻找的例子。我认为“形式语义”的一个相当极端的例子是 this paper on "the essence of JavaScript" 。也许一个稍微不太正式的例子是 Haskell 2010 report 。
最佳答案
shell 是操作系统的接口(interface)。它本身通常是一种或多或少健壮的编程语言,但具有旨在使其易于与操作系统和文件系统进行交互的功能。 POSIX shell(以下简称为“shell”)语义有点笨拙,结合了 LISP(s 表达式与 shell word splitting 有很多共同点)和 C(shell 的大部分 arithmetic syntax 语义)的一些特性来自 C)。
shell 语法的另一个根源来自它作为单个 UNIX 实用程序的大杂烩。 shell 中通常内置的大部分内容实际上都可以作为外部命令实现。当许多 shell 新手意识到 /bin/[
存在于许多系统上时,它会抛出一个循环。
$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X, without the `]`
t
哇?
如果您查看 shell 是如何实现的,这会更有意义。这是我作为练习所做的一个实现。它是用 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()
我希望上面的内容清楚地表明 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 无处不在,以至于一些脚本作者多年来都没有意识到脚本实际上可能无法在 POSIXly 严格的系统上运行。 (我也想知道人们怎么会如此关心一种编程语言的语义和风格,而对 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
$ …
环境和过程“范围”
子shell 继承其父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 中的裸词是字符串,其含义完全取决于应用于它的扩展。
无扩展
证明一个裸词真的只是一个词可能是值得的,而引号对此没有任何改变。
$ 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 属性填充名称,以告诉 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
称为位置参数。我觉得这很令人困惑,因为它没有将它包含在参数计数 $#
中,但它是一个编号参数,所以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
$ …
如果使用引号或反斜杠来防止分词,数组将保持指定的分词:
$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2
数组和位置参数之间的主要区别是:
$12
,则可以确定也设置了 $11
。 (可以设置为空字符串,但$#
不会小于12。)如果设置了"${arr[12]}"
,就不能保证设置了"${arr[11]}"
,数组的长度可以小到1。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 也有一个 shell 函数的调用堆栈,通过嵌套使用
caller
内置函数可见。引用:
SHELL GRAMMAR
部分 如果您希望我在特定方向上进一步扩展,请发表评论。
关于bash - Bash 脚本的语义?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23207168/