bash - Bash 脚本的语义?

标签 bash

与我所知道的任何其他语言相比,每次我需要一些小东西时,我都会通过谷歌搜索“学习”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 脚本的参数。您可以使用 "$@" 一次访问所有这些参数,该扩展与数组有很多共同之处。您可以使用 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 称为位置参数。我觉得这很令人困惑,因为它没有将它包含在参数计数 $# 中,但它是一个编号参数,所以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。
  • 数组的第零个元素无疑是该数组的第零个元素。在位置参数中,第零个元素不是第一个参数,而是 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 内置函数可见。

    引用:
  • bash 手册的 SHELL GRAMMAR 部分
  • XCU Shell Command Language 文档
  • Greycat 维基上的 Bash Guide
  • Advanced Programming in the UNIX Environment

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

    关于bash - Bash 脚本的语义?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23207168/

    相关文章:

    linux - 使用 bash 脚本从具有相同文件夹名称 XML 的不同位置删除超过 4 周的 xml 文件

    python - 允许用户配置 cron

    linux - 在 Ubuntu 中返回自定义进程命令

    C++:在 linux shell 脚本中运行 gdb

    bash - 如何将数字拆分为字符数组

    bash - 我如何克服脚本中的分离确认请求

    linux - `./example.sh` 和 `sh example.sh` 有什么区别

    linux - 通过脚本检查根完整性

    java - 无法在java代码中运行命令

    bash - bash中如何排序?