Bash脚本的语义?

我比任何其他语言都知道,每当我需要一些小东西的时候,我都会通过Googlesearch“学习”Bash。 因此,我可以拼凑出一些似乎可行的小脚本。 然而,我并不知道发生了什么,我希望能更正式地介绍Bash作为一种编程语言。 例如:什么是评估顺序? 什么是范围规则? 什么是打字纪律,例如,一切都是一个string? 程序的状态是什么 – 它是variables名string的键值赋值; 有没有比这更多,例如堆栈? 有堆吗? 等等。

我以为这种洞察力参考了GNU Bash手册,但似乎并不是我想要的。 它更像是一个句法糖的清单,而不是对核心语义模型的解释。 网上的百万个“bash教程”只会更糟。 也许我应该先研究sh ,然后把Bash理解为一个语法糖呢? 不过,我不知道这是否是一个准确的模型。

有什么build议么?

编辑:我被要求提供什么理想的例子我在找。 我认为“forms语义学”的一个极端例子就是关于“JavaScript的本质”的文章 。 Haskell 2010报告也许是一个稍微不太正式的例子。

shell是操作系统的接口。 它本身通常是一种或多或less强大的编程语言,但其function旨在使与操作系统和文件系统专门交互变得容易。 POSIX shell(以下简称“shell”)的语义有点像mutt,结合了LISP的一些特性(sexpression式和shell的分词有很多相同之处)和C(shell的大部分算术语法语义来自C)。

shell的语法的另一个根源来自它作为单个UNIX实用程序混杂的培养。 大部分通常在shell中构build的东西实际上可以作为外部命令来实现。 当他们意识到/bin/[存在于许多系统上时,它会引发许多shell初学者的循环。

 $ 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. 

扩展,命令parsing,执行。 所有的shell的语义都被绑定在这三件事之一上,尽pipe它们比我上面写的实现要丰富得多。

不是所有的命令fork 。 事实上,有一些命令不会像外部实现那样有意义 (这样他们就不得不fork ),但是即使是那些通常也可以作为外部的POSIX符合性的外部参数。

Bash通过添加新的function和关键字来增强POSIX shell的基础。 它几乎与sh兼容,bash如此无处不在,一些脚本作者多年没有意识到脚本可能实际上不能在POSIXly严格的系统上工作。 (我也想知道人们如何关心一种编程语言的语义和风格,而关于shell的语义和风格则很less,但是我分歧)。

评估顺序

这是一个小问题:Bash从左到右的解释expression式的主要语法,但是在它的算术语法中它遵循C的优先级。 expression式不同于扩展 ,但。 从bash手册的EXPANSION部分:

扩展的顺序是:支撑扩展; 代字符扩展,参数和variables扩展,算术扩展和命令replace(按照从左到右的方式完成); 分词; 和path名称扩展。

如果你理解分词,path扩展和参数扩展,你就可以理解大部分bash的function了。 请注意,wordsplitting之后的path名扩展是至关重要的,因为它确保名称中带有空格的文件仍然可以与glob匹配。 这就是为什么使用glob扩展比parsing命令更好的原因。

范围

函数范围

就像旧的ECMAscript一样,shell除非在函数中明确声明名字,否则它具有dynamic范围。

 $ foo() { echo $x; } $ bar() { local x; echo $x; } $ foo $ bar $ x=123 $ foo 123 $ bar $ … 

环境与过程“范围”

子壳inheritance其父壳的variables,但其他types的进程不会inheritance未导出的名称。

 $ 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 $ 

打字纪律

嗯,types 是啊。 Bash实际上没有types,并且所有东西都扩展为一个string(或者一个可能更合适)。但是让我们来看看不同types的扩展。

string

几乎任何东西都可以视为一个string。 bash中的裸字是string,其含义完全取决于应用的扩展。

没有扩张

certificate一个光秃秃的词实际上只是一个词而已,而引用并没有改变这一点,这可能是值得的。

 $ 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部分。 这是相当强大的。

整数和算术expression式

你可以使用integer属性来填充名字,以告诉shell将赋值expression式的右边视为算术。 然后,当参数展开时,它将被评估为整数math,然后展开成一个string。

 $ 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脚本的参数。您可以使用"$@"一次访问所有这些参数,这个扩展与数组有很多共同之处。 您可以使用setshift builtins设置和更改位置参数,或者只需使用这些参数调用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脚本的名称。

数组

数组的语法是build立在位置参数之后的,因此,如果你愿意的话,把数组想象成一种命名的“外部位置参数”,这种做法是很正常的。 可以使用以下方法声明数组:

 $ 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 bc' 'def' ) $ echo "${#foo[@]}" 2 

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

  1. 位置参数不稀疏。 如果设置了$12 ,则可以确定设置了$11 。 (可以设置为空string,但$#不会小于12.)如果设置了"${arr[12]}" ,则不能保证设置了"${arr[11]}"并且arrays的长度可以小到1。
  2. 数组的第零个元素明确地是该数组的第零个元素。 在位置参数中,第零个元素不是第一个参数 ,而是shell或shell脚本的名称。
  3. shift一个数组,你必须分割和重新分配它,比如arr=( "${arr[@]:1}" ) 。 你也可以不unset arr[0] ,但是会使索引1的第一个元素。
  4. 数组可以作为全局variables在shell函数之间隐式地共享,但是你必须显式地传递位置参数给shell函数才能看到它们。

使用path名扩展来创build文件名数组通常很方便:

 $ dirs=( */ ) 

命令

命令是关键,但是它们的深度也比手册中的要深。 阅读SHELL GRAMMAR部分。 不同types的命令是:

  1. 简单的命令(例如$ startx
  2. pipe道(例如$ yes | make config )(lol)
  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内build的嵌套使用可见。

参考文献:

  1. bash手册的SHELL GRAMMAR部分
  2. XCU Shell命令语言文档
  3. Greycat wiki上的Bash指南
  4. UNIX环境下的高级编程

如果您希望我进一步向特定方向扩展,请提出意见。

你的问题的答案“什么是打字纪律,例如,一切都是一个string”Bashvariables是string。 但是,当variables是整数时,Bash允许对variables进行算术运算和比较。 规则Bashvariables的例外是string是当所述variables被排版或以其他方式声明时

 $ 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 

声明选项含义:

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

bash manpage比大多数manpages有更多的信息,包括你要求的一些东西。 bash经过十多年的脚本编写之后,我的假设是,由于它的历史是sh的扩展,所以它有一些时髦的语法(为了保持与sh的向后兼容性)。

FWIW,我的经历和你的一样。 尽pipe各种书(如O'Reilly的“学习Bash Shell”等)对语法有所帮助,但解决各种问题的方法有很多,其中有些不在书中,必须用Google进行search。