用纯Bash编写程序有多复杂?[关闭]


17

经过一些快速的研究,看来Bash 是图灵完备的语言

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

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

仅仅因为调试工具非常有限而使用稀疏吗?


2
在很多un * x软件包的构建过程中使用的sh脚本configure并不是“相对简单”的。
user4556274 '16

@ user4556274不是,但是通常也不是手工编写的,而是从大量的m4宏中编写的。
库萨兰达

2
Bash中有一个x86汇编程序,因此,是的,Bash偶尔用于编写复杂的程序。人们为什么不经常这样做?可能是因为解释器的速度又慢又笨拙,而且容易产生“有趣的”错误(请参阅Shellshock)。同样,Bash脚本往往难以保持大小。看上面的汇编器;您能否从消息来源得知它是否遵循AT&T或Intel语法?
佐藤桂

configure脚本也很慢,完成了大量无用的工作,并且成为一些有趣的咆哮的主题。当然,外壳程序可以用于大型程序,但是后来人们又用康韦的《生活游戏》和《我的世界》制作了计算机,并且还有诸如Brainf ** k和Hexagony之类的编程语言。显然,有些人只是喜欢用很小而又令人困惑的原子建造东西。您甚至可以卖出带有这个主意的游戏 ……
ilkkachu

那么,这个问题可以回答吗?他们搁置它并说这是无法回答的,但是我得到了一些很好的答案。协调一致是一件好事,因为我是本SE的新手,以便将我引向该SE上哪些问题是不希望的。
Bregalad

Answers:


30

看来Bash是图灵完备的语言

图灵完整性的概念与其他语言中的许多其他概念完全不同,这些语言可用于大规模编程:可用性,表达性,可理解性,速度等。

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

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

大型,复杂的shell脚本(例如configureGNU Autoconf输出的脚本)由于许多原因是非典型的:

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

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

    POSIX之前的Bourne外壳甚至没有内置的算法。您必须呼吁exprbc完成这项工作。

    即使使用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年问世。基于该标准的外壳有许多种,但它们都与该标准有不同程度的分歧。要再次采取关联数组bashzshksh93所有有特点,但也有多种实现不兼容。因此,您的选择是使用Bash或使用Zsh或使用ksh93

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

  2. Bourne系列外壳脚本语言均不支持模块

    在shell脚本中,最接近模块系统的是该.命令(又名source更现代的Bourne shell变体),相对于适当的模块系统,该命令在多个级别上失败,其中最基本的是namepacing

    无论使用哪种编程语言,当较大的整体程序中的任何单个文件超过几千行时,人类的理解就会开始出现。我们将大型程序构造为多个文件的原因是,我们最多可以将其内容抽象为一两个句子。文件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还支持块作用域,从而允许单个函数中的多个块重用变量名而不会造成交叉污染。

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

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

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

    即使忽略大多数shell脚本依赖于通常用C编写的外部程序来获得任何有用的东西所做的其实还有所有那些开销pipe()→交通fork()→交通exec()调用链。与IPC和在其他OS上启动的进程相比,该模式在Unix上相当有效,但是在这里它可以用另一种脚本语言的子例程调用有效地替换您要做的事情,这仍然要高效得多。这严重限制了shell脚本执行速度的上限。

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

    的Bourne shell都&wait并为这个管道,但是这在很大程度上只为组成多个程序,而不是为实现CPU和I / O并行性非常有用。您不可能仅使用Shell脚本就可以固定内核或使RAID阵列饱和,如果这样做,您可能会用其他语言获得更高的性能。

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

    有一些绕过的方法,例如xargs -PGNUparallel,但这只是转到上面的第4点。

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

  6. 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中,您具有echoset -x,它们在一起足以调试很多问题。


2
“ Shell脚本几乎没有内置的功能来执行并行执行。” 在我看来,与大多数其他语言相比,shell对并行处理有更好的支持。使用单个字符,&您可以并行运行进程。您可以wait为子进程完成。您可以使用命名管道来设置管道以及更复杂的管道网络。最重要的是,以正确的方式进行并行处理非常简单,只需很少的样板代码即可,并且避免了共享内存多线程的风险和困难。
山姆·沃特金斯

@SamWatkins:我已经更新了上面的第5点,以解决您的回复。虽然我也喜欢在各个进程之间传递消息,以此来避免共享内存并行性中固有的许多问题,但我在这里要重点是提高性能,而不是可组合性等,通常需要共享内存并行性。
沃伦·扬

Shell脚本非常适合进行原型制作-但最终项目应该使用适当的编程语言,最好是使用编译语言。然后在极端情况下进行组装,就像在FFmpeg项目中看到的那样。Cmake是应该对Autotools进行处理的一个很好的例子-它使用C语言编写,不需要Perl或Texinfo或M4。令人尴尬的是,Autotools在30年后仍然仍然非常依赖Shell脚本wikipedia.org/wiki/GNU_Build_System#Criticism
Steven Penny

9

我们可以在任何地方散步或游泳,那么为什么还要打扰自行车,汽车,火车,轮船,飞机和其他车辆呢?当然,散步或游泳可能会很累,但是不需要任何额外的设备都有很大的好处。

一方面,尽管bash是图灵完备的,但它并不擅长处理整数(不是太大),字符串,(一维)字符串数组以及从字符串到字符串的有限映射之类的数据。任何其他类型的数据都需要繁琐的编码,这使得编写程序变得困难,并且经常会施加实际上不够好的性能。例如,bash中的浮点运算既困难又缓慢。

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

除了之外,Bash还没有任何使编写健壮的程序更容易的功能set -e。它没有(有用的)类型,名称空间,模块或嵌套的数据结构。错误是编程中的第一难点。尽管编写无错误程序的难易程度并不总是选择语言的决定因素,但bash在该数量上的排名很低。除了将程序组合在一起之外,Bash在性能方面的排名也很差。

长期以来,bash不能在Windows上运行,甚至在今天,它也不存在于默认的Windows安装中,并且就其没有接口的意义而言,它也不能完全本机运行(甚至在WSL中)。 Windows的本机功能。Bash无法在iOS上运行,并且默认情况下未在Android上安装。因此,除非您正在编写仅适用于Unix的应用程序,否则bash根本无法移植。

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

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


1
我相信关于可移植性的神话已经被讨论过很多次了,不确定我是否会将该特定词用作否定词,因为它也适用于大多数其他语言,包括Java。即使在Windows服务器上运行的PHP与* nix服务器上运行的PHP也有一些细微的区别,如果您足够愚蠢地在Windows服务器上运行任何内容,则必须始终注意这些区别。许多事情都无法在android或iO上运行,因此也不知道这怎么可能是有效的注释。
Lizardx

7

不在大程序中使用shell脚本的一些原因,就在我的头上:

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

对于我来说,这是最准确的负面因素,因为做过很多大型bash脚本的人,这些大致也将是我列出的负面因素。但是,我发现的一件事是,在比较类似功能时,Bash实际上并不比其他编译语言慢很多。我有一个偷偷摸摸的怀疑,就是我试图用python在bash中编写一些更复杂的东西,速度的差异不会使所涉及的艰巨工作变得值得。但是,仅Bash我觉得太有限了,但是Bash + gawk效果很好,gawk几乎是真实的。
Lizardx
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.