用纯 Bash 编写的程序能有多复杂?

Bre*_*lad 23 shell bash programming

经过一些非常快速的研究,似乎 Bash是一种图灵完备的语言

我想知道,为什么 Bash 几乎只用于编写相对简单的脚本?由于 Linux 附带了 Bash shell,因此您可以像其他流行的计算机语言一样运行 shell 脚本,而无需任何外部解释器或编译器。这是一个巨大的优势,在某些情况下可以弥补语言本身的平庸。

那么,此类程序的复杂程度是否有限制?纯 Bash 是用来写复杂程序的吗?是否可以在纯 Bash 中编写文件压缩器/解压缩器?编译器?一个简单的视频游戏?

是否仅仅因为只有非常有限的调试工具而很少使用?

War*_*ung 38

似乎 Bash 是一种图灵完备的语言

图灵完备性的概念与在大型编程语言中有用的许多其他概念完全分开:可用性、表达性、可理解性、速度等。

如果图灵完备性是我们所需要的所有,我们不会有任何编程语言,在所有的,甚至没有汇编语言。计算机程序员都只会用机器代码编写,因为我们的 CPU 也是图灵完备的。

为什么 Bash 几乎只用于编写相对简单的脚本?

大型、复杂的 shell 脚本——例如configureGNU Autoconf 输出的脚本——是非典型的,原因有很多:

  1. 直到最近,您还不能指望到处都有兼容 POSIX 的 shell

    许多系统,尤其是较旧的系统,在技术上确实在系统的某处具有与 POSIX 兼容的 shell ,但它可能不在可预测的位置,例如/bin/sh. 如果您正在编写一个 shell 脚本并且它必须在许多不同的系统上运行,那么您如何编写shebang 行?一种选择是继续使用/bin/sh,但选择限制自己使用POSIX 之前的 Bourne shell 方言,以防它在这样的系统上运行。

    Pre-POSIX Bourne shell 甚至没有内置算术;你必须呼唤exprbc完成它。

    即使使用 POSIX shell,您也会错过自1990 年代初Perl 首次流行以来我们期望在 Unix 脚本语言中找到的关联数组和其他功能。

    这一历史事实意味着有一个长达数十年的传统,即忽略现代 Bourne 系列 shell 脚本解释器中的许多强大功能,纯粹是因为您不能指望它们无处不在。

    事实上,这种情况一直持续到今天:Bash直到版本 4 才获得关联数组,但您可能会惊讶于仍在使用的系统中有多少基于 Bash 3。Apple 仍然在 2017 年随 macOS 一起发布 Bash 3——显然是为了许可原因——Unix/Linux 服务器通常在生产环境中运行很长时间,几乎没有受到任何影响,因此您可能拥有一个仍在运行 Bash 3 的稳定旧系统,例如 CentOS 5 机器。如果您的环境中有这样的系统,则不能在必须在其上运行的 shell 脚本中使用关联数组。

    如果你对这个问题的回答是你只为“现代”系统编写 shell 脚本,那么你必须处理这样一个事实,即大多数 Unix shell 的最后一个共同参考点是POSIX shell 标准,自它是1989 年推出。有许多不同的外壳基于该标准,但它们都在不同程度上偏离了该标准。再次使用关联数组bashzsh、 和ksh93都具有该功能,但存在多个实现不兼容。那么,您的选择是使用 Bash,或使用 Zsh,或使用ksh93.

    如果您对这个问题的回答是“所以只安装 Bash 4”,或者ksh93,或者其他什么,那么为什么不“只”安装 Perl、Python 或 Ruby 呢?这在许多情况下是不可接受的;默认值很重要。

  2. Bourne 系列 shell 脚本语言都不支持模块

    在 shell 脚本中,最接近模块系统的是.命令——也就是source更现代的 Bourne shell 变体——相对于适当的模块系统,它在多个级别上失败,其中最基本的是命名空间

    无论使用何种编程语言,当较大的整体程序中的任何单个文件超过几千行时,人类的理解就会开始标记。我们将大型程序组织成许多文件的真正原因是,我们最多可以将它们的内容抽象为一两个句子。文件 A 是命令行解析器,文件 B 是网络 I/O 泵,文件 C 是库 Z 和程序其余部分之间的垫片,等等。当您将许多文件组装成单个程序的唯一方法是文本包含时,您对程序可以合理增长的大小设置了限制。

    为了比较,就像C 编程语言没有链接器,只有#include语句一样。这种 C-lite 方言不需要诸如extern或 之类的关键字static。这些特性的存在是为了允许模块化。

  3. POSIX没有定义将变量范围限定为单个 shell 脚本函数的方法,更不用说文件了。

    这有效地使所有变量成为 global,这再次损害了模块化和可组合性。

    有办法解决这一在后壳POSIX -肯定bashksh93zsh至少-但只是带你回到上述第1点。

    您可以在 GNU Autoconf 宏编写的样式指南中看到这种效果,他们建议您在变量名前加上宏本身的名称,导致非常长的变量名纯粹是为了将冲突的可能性降低到可接受的范围内零。

    在这个分数上,即使是 C 也更好,一英里。不仅大多数 C 程序主要使用函数局部变量编写,C 还支持块作用域,允许单个函数中的多个块重用变量名而不会交叉污染。

  4. Shell 编程语言没有标准库。

    可能会争辩说,shell 脚本语言的标准库是 的内容PATH,但这只是说要完成任何有意义的事情,shell 脚本必须调用另一个完整的程序,可能是用更强大的语言编写的首先。

    也没有像 Perl 的CPAN那样广泛使用的 shell 实用程序库存档。如果没有大量可用的第三方实用程序代码库,程序员必须手动编写更多代码,因此她的工作效率较低。

    即使忽略大多数 shell 脚本依赖于通常用 C 编写的外部程序来完成任何有用的事情这一事实,所有这些pipe()fork()exec()调用链都有开销。与IPC和其他操作系统上的进程启动相比,这种模式在 Unix 上相当有效,但在这里它有效地取代了你用另一种脚本语言调用子例程所做的事情,这仍然更有效率。这严重限制了 shell 脚本执行速度的上限。

  5. Shell 脚本几乎没有通过并行执行来提高其性能的内置功能。

    Bourne shell 具有用于此的&,wait和管道,但这主要仅用于组合多个程序,而不用于实现 CPU 或 I/O 并行性。你不太可能能够盯住核心或仅与shell脚本饱和RAID阵列,如果你这样做,你很可能实现在其他语言更高的性能。

    管道尤其是通过并行执行提高性能的弱方法。它只允许两个程序并行运行,并且在任何给定的时间点,这两个程序中的一个很可能在与另一个程序之间的I/O 上被阻塞

    有一些现代方法可以解决这个问题,例如xargs -PGNUparallel,但这只是转移到上面的第 4 点。

    由于实际上没有充分利用多处理器系统的内置能力,shell 脚本总是比用一种可以使用系统中所有处理器的语言编写的良好程序慢。configure再次以 GNU Autoconf脚本为例,将系统中的内核数量加倍对提高其运行速度几乎无济于事。

  6. Shell 脚本语言没有指针引用

    这可以防止您执行其他编程语言中可以轻松完成的许多事情。

    一方面,无法间接引用程序内存中的另一个数据结构意味着您只能使用内置数据结构。你的 shell 可能有关联数组,但它们是如何实现的?有几种可能性,每种都有不同的权衡:红黑树AVL 树哈希表是最常见的,但还有其他的。如果您需要一组不同的权衡,您就会陷入困境,因为如果没有引用,您将无法手动滚动许多类型的高级数据结构。你被给予的东西困住了。

    或者,您可能需要一种数据结构,而该数据结构甚至没有内置到您的 shell 脚本解释器中的足够替代品,例如有向无环图,您可能需要它来对依赖图进行建模。我已经编程了几十年,我能想到的在 shell 脚本中做到这一点的唯一方法是滥用文件系统,使用符号链接作为虚假引用。当您仅依赖图灵完备性时,您会得到那种解决方案,它不会告诉您解决方案是否优雅、快速或易于理解。

    高级数据结构只是指针和引用的一种用途。它们还有许多其他应用程序,这在 Bourne 系列 shell 脚本语言中根本无法轻松完成。

我可以继续说下去,但我认为你明白这一点。简而言之,Unix 类型系统有许多更强大的编程语言。

这是一个巨大的优势,在某些情况下可以弥补语言本身的平庸。

当然,这正是 GNU Autoconf 使用 Bourne 系列 shell 脚本语言的一个特意限制子集作为其configure脚本输出的原因:这样它的configure脚本几乎可以在任何地方运行。

与 GNU Autoconf 的开发人员相比,您可能不会发现更多的人相信用高度可移植的 Bourne shell 方言编写的实用性,但他们自己的创作主要是用 Perl 编写的,加上一些m4,只有一点点 shell脚本; 只有 Autoconf 的输出是纯 Bourne shell 脚本。如果这不能回避“Bourne 无处不在”概念有多大用处的问题,我不知道会有什么用。

那么,此类程序的复杂程度是否有限制?

从技术上讲,不,正如您的图灵完备性观察所表明的那样。

但这与说任意大的 shell 脚本编写起来令人愉快、易于调试或执行速度快不同。

是否可以在纯 bash 中编写文件压缩器/解压缩器?

“纯”Bash,没有任何调用PATH? 压缩器可能可以使用echo和十六进制转义序列,但这样做会相当痛苦。由于无法在 shell 中处理二进制数据,解压缩器可能无法以这种方式编写。您最终会调用 tood等将二进制数据转换为文本格式,这是 shell 处理数据的本机方式。

一旦你开始谈论使用shell脚本它的目的的方式,如胶水,以推动其他程序PATH,门开了,因为现在你只限于什么可以在其他的编程语言,这是说你做完全没有限制。通过调用其他程序来获得全部功能的 shell 脚本的PATH运行速度不如用更强大的语言编写的单体程序快,但它确实可以运行。

这就是重点。如果你需要一个程序运行得很快,或者如果它需要自己强大而不是借用别人的力量,你就不要在 shell 中编写它。

一个简单的视频游戏?

这是shell 中俄罗斯方块。如果你去寻找,其他这样的游戏是可用的。

只有非常有限的调试工具

我会将调试工具支持放在支持大型编程所需的功能列表中的第 20 位。与正确的调试器相比,许多程序员更依赖于printf()调试,而不管语言如何。

在 shell 中,您有echoset -x,它们一起足以调试很多问题。

  • “Shell 脚本几乎没有内置的并行执行能力。” 在我看来,与大多数其他语言相比,shell 对并行处理的支持更好。使用单个字符`&`,您可以并行运行进程。您可以“等待”子进程完成。您可以使用命名管道设置管道和更复杂的管道网络。最重要的是,以正确的方式进行并行处理很简单,样板代码很少,避免了共享内存多线程的风险和困难。 (3认同)

Gil*_*il' 9

我们可以在任何地方步行或游泳,那么为什么我们还要为自行车、汽车、火车、船、飞机和其他交通工具而烦恼呢?当然,步行或游泳可能会很累,但不需要任何额外设备有一个巨大的优势。

一方面,虽然 bash 是图灵完备的,但它并不擅长处理除整数(不是太大)、字符串、(一维)字符串数组以及从字符串到字符串的有限映射之外的数据。任何其他类型的数据都需要麻烦的编码,这使得编写程序变得困难,并且在实践中通常会导致性能不够好。例如,bash 中的浮点运算既困难又缓慢。

此外,bash 与其环境交互的方式很少。它可以运行进程,可以执行一些简单的文件访问(通过重定向),仅此而已。Bash 还有一个客户端网络客户端。Bash 可以很容易地发出空字节 ( printf \\0),但不能解析其输入中的空字节,这使得它不适合读取二进制数据。Bash 不能直接做其他事情:它必须为此调用外部程序。没关系:shell 的主要目的是运行外部程序!Shell 是将程序组合在一起的粘合剂语言。但是,如果您正在运行外部程序,则意味着该程序必须可用——然后您就降低了可移植性优势:)。

除了set -e. 它没有(有用的)类型、命名空间、模块或嵌套数据结构。Bug 是编程的第一难点;虽然编写无错误程序的难易程度并不总是选择语言的决定性因素,但 bash 在这方面排名很差。在将程序组合在一起以外的其他事情时,Bash 在性能方面的排名也很差。

很长一段时间 bash 都没有在 Windows 上运行,即使今天它也不存在于默认的 Windows 安装中,并且它没有完全本地运行(即使在 WSL 中),因为它没有接口Windows 的本机功能。Bash 不能在 iOS 上运行,默认情况下也不会安装在 Android 上。因此,除非您正在编写仅限 Unix 的应用程序,否则 bash 根本不是可移植的。

需要编译器不是可移植性问题。编译器在开发人员的机器上运行。需要解释器或第三方库可能是个问题,但在 Linux 下是通过分发包解决的问题,而在 Windows、Android 和 iOS 下,人们通常会将第三方组件捆绑在他们的应用程序包中。因此,您想到的那种可移植性问题对于普通应用程序来说并不是实际问题。

我的回答适用于 bash 以外的 shell。一些细节因外壳而异,但总体思路是相同的。


ilk*_*chu 7

一些不为大型程序使用 shell 脚本的原因,就在我的脑海中:

  • 大多数功能是通过分叉外部命令来完成的,这很慢。相比之下,像 Perl 这样的编程语言可以在内部完成等效的mkdirgrep
  • 没有简单的方法可以访问 C 库或进行直接系统调用,这意味着很难创建视频游戏等
  • 适当的编程语言可以更好地支持复杂的数据结构。虽然 Bash 确实有数组和关联数组,但我不想考虑链表或树。
  • 外壳程序用于处理文本命令。二进制数据(即包含 NUL 字节(值为零的字节)的变量)很难甚至不可能处理。有点依赖于外壳,zsh有一些支持。这也是因为外部程序的界面大多是基于文本的,并\0用作分隔符。
  • 也因为有外部命令,代码和数据的分离有点困难。见证将数据引用到另一个 shell 时的所有麻烦(即在运行时bash -c ...或时ssh -c ...