看来Bash是图灵完备的语言
图灵完整性的概念与其他语言中的许多其他概念完全不同,这些语言可用于大规模编程:可用性,表达性,可理解性,速度等。
如果图灵完备性是我们所需要的所有,我们不会有任何编程语言,在所有的,甚至没有汇编语言。因为我们的CPU也是图灵完备的,所以计算机程序员都只会写机器代码。
为什么Bash几乎专门用于编写相对简单的脚本?
大型,复杂的shell脚本(例如configure
GNU Autoconf输出的脚本)由于许多原因是非典型的:
直到最近,您还不能指望到处都有POSIX兼容的外壳。
从技术上讲,许多系统,尤其是较旧的系统,确实在系统上的某个位置具有POSIX兼容的外壳,但它可能不在诸如的可预测位置/bin/sh
。如果您正在编写Shell脚本,并且该脚本必须在许多不同的系统上运行,那么如何编写shebang行?一种选择是继续使用/bin/sh
,但选择将自己限制在POSIX Bourne之前的Shell方言中,以防它在这样的系统上运行。
POSIX之前的Bourne外壳甚至没有内置的算法。您必须呼吁expr
或bc
完成这项工作。
即使使用POSIX shell,您也会错过自1990年代初期 Perl首次流行以来,我们期望在Unix脚本语言中找到的关联数组和其他功能。
历史的事实意味着,有几十年的传统,就是纯粹地忽略了现代Bourne家族外壳脚本解释器中的许多强大功能,因为您不能指望它们随处可见。
实际上,这种情况一直持续到今天:直到第4版,Bash才获得关联阵列,但您可能会惊讶地发现仍在使用的基于Bash 3的系统有多少。苹果仍然在2017年推出了带有macOS的Bash 3- 显然是针对许可原因 -而且Unix / Linux服务器通常在生产环境中运行很长时间,但几乎没有受到影响,因此您可能有一个稳定的旧系统仍在运行Bash 3,例如CentOS 5盒。如果您的环境中有这样的系统,则不能在必须在其上运行的shell脚本中使用关联数组。
如果您对这个问题的答案是只为“现代”系统编写shell脚本,那么您就必须解决以下事实:大多数Unix shell的最后一个公共参考点是POSIX shell标准,因为它是1989年问世。基于该标准的外壳有许多种,但它们都与该标准有不同程度的分歧。要再次采取关联数组bash
,zsh
和ksh93
所有有特点,但也有多种实现不兼容。因此,您的选择是仅使用Bash或仅使用Zsh或仅使用ksh93
。
如果您对这个问题的回答是,“那么只需安装Bash 4”或ksh93
或其他,那么为什么不“仅”安装Perl或Python或Ruby?在许多情况下这是不可接受的;默认问题。
Bourne系列外壳脚本语言均不支持模块。
在shell脚本中,最接近模块系统的是该.
命令(又名source
更现代的Bourne shell变体),相对于适当的模块系统,该命令在多个级别上失败,其中最基本的是namepacing。
无论使用哪种编程语言,当较大的整体程序中的任何单个文件超过几千行时,人类的理解就会开始出现。我们将大型程序构造为多个文件的原因是,我们最多可以将其内容抽象为一两个句子。文件A是命令行解析器,文件B是网络I / O泵,文件C是库Z和程序的其余部分之间的填充程序,以此类推。 ,您限制了程序可以合理增长的大小。
为了进行比较,就好像C编程语言没有链接器,只有#include
语句。这样的C-lite方言将不需要诸如extern
或的关键字static
。存在那些特征以允许模块化。
POSIX 并未定义将变量的作用域限定为单个shell脚本函数的方法,更不用说将其范围限定为文件了。
这有效地使所有变量变为全局变量,这再次损害了模块性和可组合性。
在POSIX后的shell中肯定有解决方案- 至少在中bash
,ksh93
并且zsh
-但这只是使您回到上面的第1点。
您可以在GNU Autoconf宏编写的样式指南中看到此效果,他们建议您在变量名前加上宏本身的名称,从而导致变量名很长,纯粹是为了将冲突的机会减少到可以接受的程度零。
即使是C,在这一点上也要好一英里。大多数C程序不仅主要使用函数局部变量编写,而且C还支持块作用域,从而允许单个函数中的多个块重用变量名而不会造成交叉污染。
Shell编程语言没有标准库。
可能会争辩说shell脚本语言的标准库是的内容PATH
,但这只是说要完成任何结果,shell脚本必须调出另一个整个程序,可能是用更强大的语言编写的一个程序。首先。
没有像Perl的CPAN那样广泛使用的Shell实用程序库存档。没有大量可用的第三方实用程序代码库,程序员必须手动编写更多代码,因此效率较低。
即使忽略大多数shell脚本依赖于通常用C编写的外部程序来获得任何有用的东西所做的其实还有所有那些开销pipe()
→交通fork()
→交通exec()
调用链。与IPC和在其他OS上启动的进程相比,该模式在Unix上相当有效,但是在这里它可以用另一种脚本语言的子例程调用有效地替换您要做的事情,这仍然要高效得多。这严重限制了shell脚本执行速度的上限。
Shell脚本几乎没有内置功能来通过并行执行来提高其性能。
的Bourne shell都&
,wait
并为这个管道,但是这在很大程度上只为组成多个程序,而不是为实现CPU和I / O并行性非常有用。您不可能仅使用Shell脚本就可以固定内核或使RAID阵列饱和,如果这样做,您可能会用其他语言获得更高的性能。
特别是,管道是通过并行执行来提高性能的较弱方法。它仅允许两个程序并行运行,并且在任何给定时间点,两个程序之一可能会在与另一个程序之间的I / O上被阻止。
有一些绕过的方法,例如xargs -P
和GNUparallel
,但这只是转到上面的第4点。
由于实际上没有内置的功能可以充分利用多处理器系统,因此外壳程序脚本总是比使用可以使用系统中所有处理器的语言编写的程序慢。再以GNU Autoconf configure
脚本为例,将系统中的内核数量加倍对提高其运行速度几乎没有作用。
Shell脚本语言没有指针或引用。
这样可以避免您用其他编程语言轻松完成很多工作。
一方面,无法间接引用程序内存中的另一个数据结构意味着您仅限于内置数据结构。您的shell可能具有关联数组,但是如何实现它们呢?有几种可能性,每种都有不同的权衡:红黑树,AVL树和哈希表是最常见的,但还有其他可能性。如果需要一组不同的折衷方案,则会遇到麻烦,因为如果没有引用,您将无法手动滚动许多类型的高级数据结构。你被束之高阁。
或者,可能是因为您需要的数据结构甚至没有在shell脚本解释器中内置适当的替代方法,例如有向无环图,为建模依赖图可能需要。我已经进行了数十年的编程,在shell脚本中想到的唯一方法就是使用符号链接作为伪引用来滥用文件系统。当您仅依赖图灵完备性时,您将获得这种解决方案,该方案不会告诉您该解决方案是优雅,快速还是易于理解。
高级数据结构仅仅是指针和引用的一种用途。它们还有许多其他应用程序,使用Bourne家族的shell脚本语言根本无法轻松完成。
我可以继续下去,但是我想你明白了。简而言之,对于Unix类型的系统,有许多更强大的编程语言。
这是一个巨大的优势,在某些情况下可以弥补语言本身的平庸性。
当然,这就是为什么GNU Autoconf使用Bourne外壳脚本语言家族的有目的限制的子集作为其configure
脚本输出的原因:这样它的configure
脚本几乎可以在任何地方运行。
与GNU Autoconf的开发人员相比,您可能会发现没有更多的人相信使用高度可移植的Bourne Shell方言进行编写的效用,但是他们自己的创作主要是用Perl编写的,还有一些m4
,只有很少一部分Shell脚本; 只有Autoconf的输出是纯Bourne Shell脚本。如果这不能解决“遍地开花”概念有多有用的问题,我不知道会有什么用。
那么,此类程序的复杂程度是否有限制?
从技术上讲,不,正如您的图灵完整性观察所建议的那样。
但这与说任意大的shell脚本易于编写,易于调试或执行速度快不同。
可以用纯bash编写文件压缩器/解压缩器吗?
“纯” Bash,没有任何呼唤中的东西PATH
?压缩器可以使用echo
十六进制转义序列和十六进制转义序列来实现,但是这样做非常痛苦。由于无法在Shell中处理二进制数据,因此解压缩器可能无法以这种方式编写。您最终将呼出,od
并且将二进制数据转换为文本格式,这是Shell处理数据的本机方式。
一旦开始谈论使用shell脚本的预期方式,即作为驱动其他程序PATH
的粘合剂,门就打开了,因为现在您只限于可以用其他编程语言完成的事情,也就是说完全没有限制。通过调用其他程序来发挥其全部功能的Shell脚本的PATH
运行速度不及使用更强大的语言编写的整体程序的运行速度,但它确实可以运行。
这就是重点。如果您需要一个程序来快速运行,或者它本身需要强大的功能而不是从他人那里借用力量,则不要在shell中编写它。
一个简单的视频游戏?
这是带壳的俄罗斯方块。如果您去寻找,也可以使用其他此类游戏。
只有非常有限的调试工具
我将调试工具支持放在支持大型编程所必需的功能列表的第20位。无论使用哪种语言,与适当的调试器相比,很多程序员在printf()
调试上的依赖要大得多。
在shell中,您具有echo
和set -x
,它们在一起足以调试很多问题。
sh
脚本configure
并不是“相对简单”的。