Bash脚本的语义?


87

每次我需要一些小东西时,我都会通过谷歌搜索来“学习” Bash,这比我所知道的任何其他语言都多。因此,我可以将看起来有用的小脚本拼凑在一起。不过,我并不真正知道发生了什么事情,我希望的是更正式的介绍bash作为编程语言。例如:什么是评估顺序?范围规则是什么?打字纪律是什么,例如,一切都是字符串吗?程序的状态是什么?它是字符串到变量名的键值分配吗?还有更多,例如堆栈吗?有堆吗?等等。

我本来想咨询GNU Bash手册以获得这种见解,但这似乎并不是我想要的。它更多是语法糖的清单,而不是核心语义模型的解释。在线的百万个“ bash教程”更糟。也许我应该先学习sh,然后再将Bash理解为一种语法糖?不过,我不知道这是否是一个准确的模型。

有什么建议么?

编辑:我被要求提供理想情况下我正在寻找的示例。我认为“形式语义学”的一个极端例子是这篇关于“ JavaScript本质”的文章Haskell 2010报告也许是一个不太正式的例子。


3
高级Bash脚本编程指南的“万和一个”吗?
choroba 2014年

2
我不相信bash具有“核心语义模型”(嗯,也许“几乎所有东西都是字符串”);我认为这真的是语法糖。
Gordon Davisson

4
您所谓的“语法糖清单”实际上是扩展的语义模型-执行中极为重要的部分。90%的错误和混乱是由于不了解扩展模型所致。
另一个人

4
我可以理解为什么有人读了像我如何编写Shell脚本一样会认为这是一个广泛的问题?但是真正的问题是,shell语言和bash的形式语义和基础特别是什么?,这是一个很好的问题,只需一个连贯的答案。投票重新开放。
kojiro 2014年

1
我在linuxcommand.org上学到了很多东西,甚至还有关于命令行编写shell脚本的更深入书籍的免费pdf
samrap 2014年

Answers:


107

Shell是操作系统的接口。它本身通常是一种或多或少健壮的编程语言,但是具有旨在使其易于与操作系统和文件系统进行交互的功能。POSIX shell的语义(以下简称为“ shell”)有些杂乱无章,结合了LISP(s表达式与shell单词拆分有很多共同点)和C(大部分shell的算术语法)语义来自C)。

Shell语法的另一个根源是它的成长,它是各个UNIX实用程序的混搭。外壳程序中通常内置的大多数功能实际上都可以实现为外部命令。当它们意识到/bin/[存在于许多系统上时,会抛出许多壳新生物进行循环。

$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X, without the `]`
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:
      # 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.

扩展,命令解析,执行。尽管外壳程序的语义比我上面编写的实现要丰富得多,但所有外壳程序的语义都与这三件事之一绑定在一起。

并非所有命令fork。实际上,有一些命令没有多少意义上可以作为外部对象来实现(因此必须这样做fork),但即使是严格符合POSIX要求的命令,也经常可以作为外部对象来使用。

Bash通过添加新功能和关键字来增强POSIX Shell,以此为基础。它几乎与sh兼容,并且bash无处不在,以至于一些脚本作者花了好几年的时间才意识到脚本实际上可能无法在POSIXly严格的系统上运行。(我还想知道人们如何能如此关心一种编程语言的语义和样式,而对外壳的语义和样式却关心得很少,但是我会有所不同。)

评估顺序

这是一个棘手的问题:Bash从左到右以其主要语法解释表达式,但在其算术语法中遵循C优先级。表达式与扩展不同。从EXPANSIONbash手册的部分:

扩展顺序为:大括号扩展;波浪线扩展,参数和变量扩展,算术扩展和命令替换(以从左到右的方式完成);分词 和路径名扩展。

如果您了解分词,路径名扩展和参数扩展,则可以很好地理解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' # 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中的Bareword是字符串,其含义完全取决于对其应用的扩展。

没有扩展

可能值得证明裸词实际上只是一个词,而引号对此没有任何改变。

$ 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

数组

参数和位置参数

在讨论数组之前,可能值得讨论位置参数。一个shell脚本的参数可以使用编号为参数,进行访问$1$2$3等您可以一次使用访问所有这些参数"$@",它扩大在许多共同之处与阵列。您可以使用setshift内置来设置和更改位置参数,或者仅通过使用以下参数调用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" # Even if the zeroth index isn't set

$ …

如果使用引号或反斜杠来防止单词拆分,则数组将保持指定的单词拆分:

$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2

数组和位置参数之间的主要区别是:

  1. 位置参数不稀疏。如果$12已设置,则可以确定$11也已设置。(可以将其设置为空字符串,但$#不得小于12。)如果"${arr[12]}"设置为,则不能保证"${arr[11]}"已设置,并且数组的长度可以小至1。
  2. 数组的第零元素无疑是该数组的第零元素。在位置参数中,第零个元素不是第一个参数,而是shell或shell脚本的名称。
  3. 对于shift数组,您必须像那样对其进行切片和重新分配arr=( "${arr[@]:1}" )。您也可以这样做unset arr[0],但这将使第一个元素位于索引1处。
  4. 数组可以作为全局变量在Shell函数之间隐式共享,但是必须将位置参数显式传递给Shell函数才能看到它们。

使用路径名扩展来创建文件名数组通常很方便:

$ dirs=( */ )

指令

命令是关键,但比手册更深入地介绍了它们。阅读本SHELL GRAMMAR节。不同种类的命令是:

  1. 简单命令(例如$ startx
  2. 管道(例如$ yes | make config)(大声笑)
  3. 清单(例如$ grep -qF foo file && sed 's/foo/bar/' file > newfile
  4. 复合命令(例如$ ( cd -P /var/www/webroot && echo "webroot is $PWD" )
  5. 协同过程(复杂,无示例)
  6. 函数(可被视为简单命令的命名复合命令)

执行模型

执行模型当然涉及堆和栈。这是所有UNIX程序所特有的。Bash还具有用于Shell函数的调用堆栈,可通过嵌套使用caller内置函数来查看。

参考文献:

  1. SHELL GRAMMARbash手册的部分
  2. XCU shell命令行语言的文档
  3. 《灰猫》维基上的Bash指南
  4. UNIX环境中的高级编程

如果您希望我在特定方向上进一步扩展,请发表评论。


16
+1:很好的解释。通过示例了解编写本文所花费的时间。
jaypal singh 2014年

为+1 yes | make config ;-),但是认真的来说,这是非常好的写作。
Digital Trauma 2014年

刚开始读这个..很好。会留下一些评论。1)当你看到一个更大的惊喜来了/bin/[/bin/test通常会使用相同的词,。2)“假定第一个单词是命令。” -期待您做作业的时间…
Karoly Horvath 2014年

@KarolyHorvath是的,我有意从我的演示shell中排除了赋值,因为变量是一个复杂的烂摊子。演示shell并不是在考虑这个答案的情况下写的-它是在更早的时候编写的。我想我能做到execle插入到环境中,但这仍然会使它变得更加复杂。
kojiro 2014年

@kojiro:不,那会让它变得过于复杂,那当然不是我的意图!但是分配的工作方式略有不同(x),恕我直言,您应该在文本中的某处提及它。(x):还有一些困惑的源头……我什至无法计算我看到人们抱怨a = 1不工作了多少次。
Karoly Horvath'Apr 23'14

5

您的问题“什么是打字学科,例如,一切都是字符串”的答案Bash变量是字符串。但是,当变量是整数时,Bash允许对变量进行算术运算和比较。规则Bash变量是字符串的例外是在对所述变量进行排版或以其他方式声明时

$ A=10/2
$ echo "A = $A"           # Variable A acting like a String.
A = 10/2

$ B=1
$ let B="$B+1"            # Let is internal to bash.
$ echo "B = $B"           # One is added to B was Behaving as an integer.
B = 2

$ A=1024                  # A Defaults to string
$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ echo "B = $B"           # $B STRING is a string
B = 10STRING01

$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ declare -i B
$ echo "B = $B"           # Declaring a variable with non-integers in it doesn't change the contents.
B = 10STRING01

$ B=${B/STRING01/24}      # Substitute "STRING01"  with "24".
$ echo "B = $B"
B = 1024

$ declare -i B=10/2       # Declare B and assigning it an integer value
$ echo "B = $B"           # Variable B behaving as an Integer
B = 5

声明选项含义:

  • -a变量是一个数组。
  • -f仅使用函数名称。
  • -i该变量将被视为整数;给变量赋值时,将执行算术评估。
  • -p显示每个变量的属性和值。使用-p时,将忽略其他选项。
  • -r使变量为只读。这些变量然后不能被后续的赋值语句赋值,也不能被取消设置。
  • -t给每个变量trace属性。
  • -x将每个变量标记为通过环境导出到后续命令。

1

bash联机帮助页比大多数联机帮助页包含更多信息,其中包括您所要求的内容。经过十多年的脚本bash编写,我的假设是,由于其作为sh的扩展历史,它具有一些时髦的语法(以保持与sh的向后兼容性)。

FWIW,我的经历和您一样。尽管各种书籍(例如,O'Reilly的“ Learning the Bash Shell”等)确实在语法上有所帮助,但仍有许多解决各种问题的奇怪方法,其中一些不在本书中,必须在Google上进行搜索。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.