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

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

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

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

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

是不是因为调试工具非常有限所以使用得这么少?

答案1

看来 Bash 是一种图灵完备的语言

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

如果我们只需要图灵完备性,我们就不会有任何编程语言根本不, 甚至不汇编语言。计算机程序员都会直接写入机器码,因为我们的 CPU 也是图灵完备的。

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

大型、复杂的 shell 脚本(例如configureGNU Autoconf 输出的脚本)由于多种原因而不典型:

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

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

    POSIX 之前的 Bourne shell 甚至没有内置算术;你必须呼唤expr或者bc完成这件事。

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

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

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

    如果您对这个问题的答案是只为“现代”系统编写 shell 脚本,那么您必须面对这样一个事实:大多数 Unix shell 的最后一个公共参考点是POSIX shell 标准,自 1989 年推出以来基本上没有变化。基于该标准有许多不同的 shell,但它们都不同程度地偏离了该标准。再次采用关联数组,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 脚本函数,更不用说限定为文件了。

    这有效地使得所有变量全局,这再次损害了模块化和可组合性。

    在后 POSIX shell 中有对此问题的解决方案——当然bashksh93至少zsh是——但这只是让你回到上面的第 1 点。

    您可以在 GNU Autoconf 宏编写的风格指南中看到这一点的效果,其中他们推荐您使用宏本身的名称作为变量名称的前缀,导致变量名称非常长,纯粹是为了将冲突的可能性减少到可接受的接近于零的程度。

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

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

    可以说 shell 脚本语言的标准库是 的内容PATH,但这只是说,为了完成任何重要的事情,shell 脚本必须调用另一个整个程序,可能是用更加强大语言开始。

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

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

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

    Bourne shell为此提供了&、和 管道,但这在很大程度上仅适用于编写多个程序,而不适用于实现 CPU 或 I/O 并行性。wait你不太可能能够挂钩核心或仅使用 shell 脚本来饱和 RAID 阵列,如果这样做,您可能可以使用其他语言获得更高的性能。

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

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

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

  6. Shell脚本语言没有指针或者参考

    这使您无法完成许多在其他编程语言中可以轻松完成的事情。

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

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

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

我可以继续说下去,但我想你已经明白了重点。简单来说,有很多更加强大Unix 类型系统的编程语言。

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

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

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

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

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

但这并不意味着任意大的 shell 脚本都易于编写、易于调试或快速执行。

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

PATH“纯粹的”Bash,没有任何对?中的事物的呼唤。压缩器可能可以使用echo十六进制转义序列,但是做起来相当痛苦。由于以下原因,解压缩器可能无法以这种方式编写无法在 shell 中处理二进制数据。你最终会打电话给od等将二进制数据转换为文本格式,这是 shell 处理数据的本机方式。

一旦您开始谈论按照预期的方式使用 shell 脚本,作为驱动 中其他程序的粘合剂PATH,大门就打开了,因为现在您仅限于其他编程语言可以完成的事情,也就是说您根本没有限制。通过调用其他程序来获得其全部功能的 shell 脚本的PATH运行速度不如用更强大的语言编写的整体程序,但它跑步。

这就是重点。如果您需要一个程序快速运行,或者需要它本身功能强大而不是借用别人的功能,那么您就不会用 shell 编写它。

一个简单的视频游戏?

这是带壳的俄罗斯方块。如果你去寻找的话,还有其他类似的游戏。

只有非常有限的调试工具

在支持大规模编程所需的功能列表中,我会将调试工具支持排在第 20 位左右。很多程序员更加依赖printf()调试无论语言如何,都比正确的调试器更好。

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

答案2

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

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

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

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

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

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

我的回答适用于 bash 以外的 shell。每个 shell 的一些细节有所不同,但总体思路是相同的。

答案3

我突然想到一些不为大型程序使用 shell 脚本的原因:

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

相关内容