Shell是操作系统的接口。它本身通常是一种或多或少健壮的编程语言,但是具有旨在使其易于与操作系统和文件系统进行交互的功能。POSIX shell的语义(以下简称为“ shell”)有些杂乱无章,结合了LISP(s表达式与shell单词拆分有很多共同点)和C(大部分shell的算术语法)语义来自C)。
Shell语法的另一个根源是它的成长,它是各个UNIX实用程序的混搭。外壳程序中通常内置的大多数功能实际上都可以实现为外部命令。当它们意识到/bin/[
存在于许多系统上时,会抛出许多壳新生物进行循环。
$ if '/bin/[' -f '/bin/['; then echo t; fi
t
at?
如果您看一下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:
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.
扩展,命令解析,执行。尽管外壳程序的语义比我上面编写的实现要丰富得多,但所有外壳程序的语义都与这三件事之一绑定在一起。
并非所有命令fork
。实际上,有一些命令没有多少意义上可以作为外部对象来实现(因此必须这样做fork
),但即使是严格符合POSIX要求的命令,也经常可以作为外部对象来使用。
Bash通过添加新功能和关键字来增强POSIX Shell,以此为基础。它几乎与sh兼容,并且bash无处不在,以至于一些脚本作者花了好几年的时间才意识到脚本实际上可能无法在POSIXly严格的系统上运行。(我还想知道人们如何能如此关心一种编程语言的语义和样式,而对外壳的语义和样式却关心得很少,但是我会有所不同。)
评估顺序
这是一个棘手的问题:Bash从左到右以其主要语法解释表达式,但在其算术语法中遵循C优先级。表达式与扩展不同。从EXPANSION
bash手册的部分:
扩展顺序为:大括号扩展;波浪线扩展,参数和变量扩展,算术扩展和命令替换(以从左到右的方式完成);分词 和路径名扩展。
如果您了解分词,路径名扩展和参数扩展,则可以很好地理解bash的大部分功能。请注意,单词拆分后的路径名扩展至关重要,因为它可以确保名称中带有空格的文件仍可以由全局匹配。这就是为什么通常使用glob扩展优于解析命令的原因。
范围
功能范围
就像旧的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'
123
您可以结合以下作用域规则:
$ foo() {
> local -x bar=123
> bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar
$
打字纪律
嗯,类型。是的 Bash确实没有类型,并且所有内容都扩展为字符串(或者一个单词可能更合适。)但是让我们研究一下扩展的不同类型。
弦乐
几乎所有东西都可以视为字符串。bash中的Bareword是字符串,其含义完全取决于对其应用的扩展。
没有扩展
可能值得证明裸词实际上只是一个词,而引号对此没有任何改变。
$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
子串扩展
$ fail='echoes'
$ set -x
$ "${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
$ echo $foo
20
$ echo "${foo:0:1}"
2
数组
参数和位置参数
在讨论数组之前,可能值得讨论位置参数。一个shell脚本的参数可以使用编号为参数,进行访问$1
,$2
,$3
等您可以一次使用访问所有这些参数"$@"
,它扩大在许多共同之处与阵列。您可以使用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中$#
,但是它是一个带编号的参数,所以。$0
是外壳程序或当前外壳程序脚本的名称。
数组
数组的语法是根据位置参数建模的,因此,如果愿意的话,将数组视为一种命名的“外部位置参数”是很健康的。可以使用以下方法声明数组:
$ 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"
$ …
如果使用引号或反斜杠来防止单词拆分,则数组将保持指定的单词拆分:
$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2
数组和位置参数之间的主要区别是:
- 位置参数不稀疏。如果
$12
已设置,则可以确定$11
也已设置。(可以将其设置为空字符串,但$#
不得小于12。)如果"${arr[12]}"
设置为,则不能保证"${arr[11]}"
已设置,并且数组的长度可以小至1。
- 数组的第零元素无疑是该数组的第零元素。在位置参数中,第零个元素不是第一个参数,而是shell或shell脚本的名称。
- 对于
shift
数组,您必须像那样对其进行切片和重新分配arr=( "${arr[@]:1}" )
。您也可以这样做unset arr[0]
,但这将使第一个元素位于索引1处。
- 数组可以作为全局变量在Shell函数之间隐式共享,但是必须将位置参数显式传递给Shell函数才能看到它们。
使用路径名扩展来创建文件名数组通常很方便:
$ 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手册的部分
- 该XCU shell命令行语言的文档
- 《灰猫》维基上的Bash指南。
- UNIX环境中的高级编程
如果您希望我在特定方向上进一步扩展,请发表评论。