为什么脚本语言(例如Perl、Python和Ruby)不适合作为shell语言?

353

这些类似Bash (bash)、Z shell (zsh)、Fish (fish)的shell语言和上面提到的脚本语言有什么不同,使它们更适合于shell?

当使用命令行时,shell语言似乎更容易。例如,对我来说使用bash比在IPython的shell配置文件中使用要顺畅得多,尽管有相反的报道。我认为大多数人都会同意,中到大型编程的很大一部分在Python中比在Bash中更容易。我使用Python作为我最熟悉的语言。对于PerlRuby也是如此。

我试图表达原因,但无法做到,除了假设两者在字符串处理方面的不同有所关联。

提出这个问题的原因是我希望开发一种可同时使用的语言。如果您知道这样的语言,请也发布一下。

S.Lott解释说,这个问题需要一些澄清。我问的是关于shell语言与脚本语言的特性比较。因此,比较并不涉及各种交互式(REPL)环境的特点,例如历史记录和命令行替换。另一个表达该问题的方式是:

一个适合于设计复杂系统的编程语言是否能够同时表达有用的单行代码,以访问文件系统或控制任务?一个编程语言是否能够很好地进行扩展和缩小?

12个回答

482

我能想到几个区别;只是随便列举一下,没有特定顺序:

  1. Python & Co. are designed to be good at scripting. Bash & Co. are designed to be only good at scripting, with absolutely no compromise. IOW: Python is designed to be good both at scripting and non-scripting, Bash cares only about scripting.

  2. Bash & Co. are untyped, Python & Co. are strongly typed, which means that the number 123, the string 123 and the file 123 are quite different. They are, however, not statically typed, which means they need to have different literals for those, in order to keep them apart.
    Example:

                    | Ruby             | Bash    
    -----------------------------------------
    number          | 123              | 123
    string          | '123'            | 123
    regexp          | /123/            | 123
    file            | File.open('123') | 123
    file descriptor | IO.open('123')   | 123
    URI             | URI.parse('123') | 123
    command         | `123`            | 123
    
  3. Python & Co. are designed to scale up to 10000, 100000, maybe even 1000000 line programs, Bash & Co. are designed to scale down to 10 character programs.

  4. In Bash & Co., files, directories, file descriptors, processes are all first-class objects, in Python, only Python objects are first-class, if you want to manipulate files, directories etc., you have to wrap them in a Python object first.

  5. Shell programming is basically dataflow programming. Nobody realizes that, not even the people who write shells, but it turns out that shells are quite good at that, and general-purpose languages not so much. In the general-purpose programming world, dataflow seems to be mostly viewed as a concurrency model, not so much as a programming paradigm.

我有一种感觉,试图通过在通用编程语言上添加功能或DSL来解决这些问题是行不通的。至少,在我看来,我还没有看到一个令人信服的实现。有RuSH(Ruby shell),它试图在Ruby中实现一个shell,有rush,它是Ruby中的一个内部DSL,用于shell编程,有Hotwire,它是一个Python shell,但在我看来,这些都无法与Bash、Zsh、fish和其他工具相提并论。
实际上,我认为当前最好的shell是Microsoft PowerShell,这非常令人惊讶,考虑到微软多年来一直拥有最差的shell。我的意思是,COMMAND.COM?真的吗?(不幸的是,它们仍然有一个糟糕的终端。它仍然是自Windows 3.0以来一直存在的“命令提示符”)

PowerShell基本上是通过忽略微软曾经做过的一切(如COMMAND.COM、CMD.EXE、VBScript和JScript等),从Unix shell开始创建,然后删除所有向后兼容的垃圾(例如用于命令替换的反引号),并进行了一些调整,以使其更加Windows友好(例如使用现在未使用的反引号作为转义字符,而不是在Windows中作为路径组件分隔符的反斜杠)。之后,魔法就发生了。

他们通过与Python相比基本上做出相反的选择来解决以上的问题1和3。Python首先关注大型程序,其次是脚本。Bash只关心脚本。PowerShell首先关注脚本,其次是大型程序。对我来说,一个定义性时刻是观看Jeffrey Snover(PowerShell的首席设计师)的访谈视频时,面试官询问他可以用PowerShell编写多大的程序,Snover毫不犹豫地回答:“80个字符。”在那一刻,我意识到这终于是一个“懂得”shell编程的微软员工(可能与PowerShell既不是由微软的编程语言组(即λ演算数学迷)开发,也不是由操作系统组(内核迷)开发,而是由服务器组(即实际使用shell的系统管理员)开发有关),我应该认真看看PowerShell。

第二点通过静态类型的参数解决。因此,您只需编写123,PowerShell就知道它是字符串、数字还是文件,因为cmdlet(在PowerShell中称为shell命令)将其参数的类型声明给了shell。这具有相当深远的影响:与Unix不同,在Unix中,每个命令都负责解析自己的参数(shell基本上将参数作为字符串数组传递),而在PowerShell中,参数解析由 shell 完成。 cmdlets指定了它们所有选项、标志和参数,以及它们的类型、名称和文档(!),然后可以在一个集中的地方执行参数解析、制表符完成、IntelliSense、内联文档弹出窗口等。(这并非是革命性的,PowerShell的设计者承认像DIGITAL Command Language(DCL)和IBM OS/400 Command Language(CL)这样的shell是先前的艺术品。对于任何曾经使用过AS/400的人来说,这应该听起来很熟悉。在OS/400中,您可以编写一个shell命令,如果您不知道某些参数的语法,您可以简单地将它们留空并按下F4,这将带来一个菜单(类似于HTML表单),其中包含带标签的字段、下拉菜单、帮助文本等。这仅仅是因为操作系统知道所有可能的参数及其类型才有可能实现。)在Unix shell中,这些信息经常重复三次:在命令本身的参数解析代码中,在用于制表符完成的bash-completion脚本中以及在man页面中。

第四点通过PowerShell基于强类型对象操作来解决,其中包括文件、进程、文件夹等。

第五点特别有趣,因为PowerShell是我所知道的唯一一个shell,编写它的人实际上意识到shell本质上是数据流引擎,并故意将其实现为数据流引擎。

PowerShell的另一个好处是命名约定:所有cmdlet都命名为Action-Object,而且还有针对特定操作和特定对象的标准化名称。 (这应该听起来很熟悉,对于OS/400用户而言)。例如,与接收某些信息相关的所有内容都称为Get-Foo。而所有在(子)对象上操作的内容都称为Bar-ChildItem。因此,与ls相当的是Get-ChildItem(尽管PowerShell还提供了内置别名lsdir - 实际上,每当有意义时,它们都提供Unix和CMD.EXE别名以及缩写(在这种情况下为gci))。

但我认为最强大的功能是强类型对象管道。虽然PowerShell源自Unix shell,但有一个非常重要的区别:在Unix中,所有通信(通过管道和重定向以及通过命令参数)都是使用未经类型化和结构化的字符串完成的。在PowerShell中,所有内容都是强类型的、结构化的对象。这非常强大,以至于我真的想知道为什么没有其他人想到过它。在我的shell脚本中,我估计三分之一的命令只是作为两个不同命令之间的适配器存在,因为它们不共享相同的文本格式。在PowerShell中,许多这样的适配器消失了,因为cmdlet交换的是结构化对象,而不是未经结构化的文本。如果你观察这些命令,你会发现它们基本上由三个阶段组成:将文本输入解析为内部对象表示形式,操作对象,将它们转换回文本。再次说明,第一和第三个阶段基本上消失了,因为数据已经以对象的形式进入。

然而,设计师们通过所谓的“自适应类型系统”非常注意保留了shell脚本的动态性和灵活性。

无论如何,我不想把这变成一个PowerShell的商业广告。虽然有很多关于PowerShell不太好的事情,但大部分都与Windows或具体实现有关,而不是与概念有关。 (例如,它是在.NET中实现的,这意味着如果文件系统缓存中没有.NET框架,则第一次启动shell可能需要几秒钟时间,如果您经常只使用shell不到一秒钟,那是完全不可接受的。)

我想要强调的最重要的一点是,如果你希望查看现有的脚本语言和Shell的工作,你不应该仅停留在Unix和Ruby/Python/Perl/PHP家族。例如,已经提到了TclRexx是另一种脚本语言。 Emacs Lisp则是另外一种语言。在Shell领域,还有一些已被提到的主机/中档shell,例如OS/400命令行和DCL。还有,Plan9的rc。

2
这是一个对问题两个部分都非常好的回答。关于第2点和第4点:这是因为静态类型本身,还是因为强制转换操作呢?在我所知道的大多数非shell语言(静态或动态)中,123不能定义两种不同的类型。另外,你有没有一个解释PowerShell自适应类型系统的链接?谷歌似乎没有给我任何我能理解的东西。 - Muhammad Alkarouri
2
很棒的回答!你还记得那个与Jeffrey Snover的采访链接吗? - Mauricio Scheffer
11
4个月只有13票,然后20个小时内突然涨到84票?难道我被Reddit玩了?@Mauricio Scheffer:我几乎浏览了Channel9上的所有视频,甚至包括PowerShell还叫Monad的时候,但很遗憾没有找到合适的内容。@Dave:有一个名为Pash(PoSh + Bash)的项目,但是它只在三年前发布了一次。BUSHFISH不再使用Bourne的语法,但它们仍然处理文本。@codebliss:问题是关于Python作为shell语言的。这里有一个有趣的数字,就是一个LOC - Jörg W Mittag
4
另一个理解方式是:PowerShell不支持多态或重载。在一个具有多态性的语言中,根据参数类型决定执行哪个过程。(例如,在 x.ToString() 中,取决于 x 的类型,执行哪个 ToString())。而在PowerShell中,则正好相反:语言根据过程决定将参数解释为什么类型。另外一种解释是,语法根据参数类型动态构建,并且只有一种解析令牌的方法。 - Jörg W Mittag
3
PowerShell仍然是一个Shell,因此使执行程序变得容易(尽管根据需要引用的参数而不是完全无痛),因此您可以像在任何其他地方一样使用ImageMagick,而无需先编写/查找包装它的Cmdlet或PowerShell函数。但是,通过传递图像对象并将图像过滤器实现为Cmdlet等,使用本机于PowerShell的实现可能会更酷。我仍在考虑编写某些内容,以允许像“Load-Image foo.bmp | Apply-Blur -Radius 5 | Save-Image x.png”这样的事情。 - Joey
显示剩余15条评论

60
这是一种文化。Bourne shell 几乎有 25 年历史了;它是最早的脚本语言之一,也是 Unix 管理员的首选解决方案。 (即,一种“粘合剂”将所有其他实用程序绑在一起,并在不必每次编译该死的 C 程序的情况下执行典型的 Unix 任务。)
按照现代标准,它的语法令人震惊,其奇怪的规则和以标点符号作为语句风格(在每个字节都很重要的 1970 年代很有用)使非管理员难以理解。但是它做到了工作。缺陷和缺点通过其后代(ksh、bash、zsh)的演进改进得到了解决,而无需重新构思其背后的思想。管理员坚持使用核心语法,因为尽管它很奇怪,但没有其他东西可以更好地处理简单的事情而不会妨碍工作。
对于复杂的事情,Perl 出现了,并变成了一种半管理员、半应用程序语言。但是,某些越来越复杂的东西被视为应用程序而不是管理工作,所以商业人士倾向于寻找“程序员”而不是“管理员”来完成它,尽管正确类型的极客往往两者兼备。因此,重点放在了这里,对 Perl 应用程序功能的演进改进导致了...... 嗯,Python 和 Ruby。(这是一种过度简化,但 Perl 是两种语言的几个灵感之一。)
结果?专业化。管理员往往认为现代解释型语言对于他们每天需要做的事情来说过于笨重。总的来说,他们是正确的。他们不需要对象。他们不关心数据结构。他们需要“命令”。没有比Bourne shell概念(也许除了已经在这里提到的Tcl)更好地执行“命令”的工具了;而Bourne就足够好。
程序员——现在他们越来越需要了解devops——看着Bourne shell的限制,想知道任何人怎么能忍受它。但是他们所知道的工具,虽然它们肯定倾向于Unixish风格的I/O和文件操作,但对于这个目的来说并不更好。我用Ruby编写了备份脚本和文件重命名一次性任务,因为我比bash更熟悉它,但是任何专业管理员都可以在bash中完成同样的事情——可能行数更少,开销更小,但无论哪种方式,它都能正常工作。
问“为什么每个人都使用Y,而不是更好的Z?”是一个常见的问题——但是技术的发展,就像其他一切事物的发展一样,往往会停留在“足够好”的水平。除非差异被视为无法接受的挫败,否则“更好”的解决方案不会获胜。Bourne类型的脚本可能对您来说很令人沮丧,但对于经常使用它以及为其设计的工作的人来说,它总是完成了工作。

我认为这可能会成为一些未来技术的灵感,这些技术将使用管道(|)作为基本概念。就像JavaScript被重新打造并与许多现代语言一起复兴,然后出现了node.js一样,同样的事情也可能发生在shell脚本中。 - Sergey Orshanskiy

54

一个Shell语言必须易于使用。您想要输入一次性的即时命令,而不是小型程序。也就是说,您想要输入:

ls -laR /usr

shell.ls("/usr", long=True, all=True, recursive=True)

这也意味着 shell 语言并不关心参数是选项、字符串、数字或其他类型。

此外,shell 中的编程结构是附加的,并且甚至不总是内置的。例如,在 BashBourne shellsh)中考虑使用if[ 的组合,使用seq生成序列等。

最后,shell 具有特定的需求,在编程中您需要以不同的方式考虑它们,例如管道、文件重定向、进程/作业控制等。


谢谢您的回答,但这些差异似乎并不是语言本身固有的。在Python中设计一个库非常容易,因此您可以说shell.ls('usr', flags='laR')。即使是shellflags=也可以被删除。我同意像重定向和进程控制这样的事情在shell中得到了优化,因为它们在那里很相关。 - Muhammad Alkarouri
4
你仍需要适当引用并平衡括号。而且你在这里提供了一个琐碎的论点,那么例如“ls --sort=time -R /bin /usr”呢? - Ivo van der Wijk
你提到的运行命令的观点适用于Python,但不适用于所有考虑中的语言:例如,在Perl中可以使用“`ls -laR /usr`”。 - intuited
8
那么你并不是在使用Perl,而是通过Perl执行Shell命令。 - Ivo van der Wijk
Ruby的非必需括号意味着你可以编写一个函数,几乎和bash一样好:ls :-laR, :/usr。不过,你仍然需要逗号和符号(或字符串)。 - Xiong Chiamiov
1
不好意思打广告,我写了一个 Python 库,希望能让 Python 更易于使用:https://github.com/houqp/shell.py - houqp

42
如果您知道这样的语言,请也分享一下。 Tcl是这样一种语言。主要是因为它被设计成CAD程序的Shell解释器。以下是一位Python程序员对于为什么Tcl被设计成这个样子的深刻体验:我不敢相信我在赞扬Tcl。对我来说,我已经编写、使用和改进了Tcl Shell(当然是用Tcl编写的),作为我的家庭路由器上的主要Linux登录Shell:纯Tcl readline。我喜欢Tcl的原因之一与其语法类似于传统Shell有关:
  1. Tcl语法最基本的形式是“命令 参数 参数...”。没有别的东西。这与BashC shell甚至DOS shell相同。

  2. 裸字符串被视为字符串。这也类似于传统的Shell,允许您编写open myfile.txt w+而不是open "myfile.txt" "w+"

  3. 由于Tcl的基础1和2,因此Tcl的语法非常简洁。您可以写出更少的标点符号代码:puts Hello而不是printf("Hello");。编写程序时,您不会感到太多痛苦,因为您花了很多时间思考要写什么。当您使用shell复制文件时,您不需要思考,只需键入,但反复键入(",);非常令人烦恼。


太好了!我完全忘记了 Tcl。很久以前我曾经简短地学过它,但是我完全忘记了它是一种非常适合我之前使用的某些思科路由器的完美 shell 接口。看起来 Tcl 是它们之间最接近完美平衡点的语言。我会切换到 tclsh 使用几天,看看效果如何。 - Muhammad Alkarouri
1
现在是稍微有些不太好听的部分。Tcl并不像其他编程语言那样成功,你能说出原因吗?我记得我对math命令和可能的面向对象编程不满意,但那是很久以前的事了。 - Muhammad Alkarouri
对于第一部分,我想指出tclsh是一个非常基本的shell:没有上下箭头历史记录,没有制表符补全等等。我建议您至少使用 rlwrap tclsh 来包装它以使用readline。或者更好的是,使用我上面链接的代码作为您的shell。对于第二部分,我认为这是因为大多数程序员不喜欢任何看起来不像C的东西。有趣的事实是,Netscape最初将Tcl实现为浏览器脚本语言,但管理层希望有一个类似于Java语法的东西(又是另一种类似于C的语法)。 - slebetman
1
我在发布评论后看了你的代码,我会使用它。说到不够像C语言的语言,你可能之前就遇到过这个链接了:http://james-iry.blogspot.com/2009/05/brief-incomplete-and-mostly-wrong.html - Muhammad Alkarouri
据我回忆,TCL的问题是政治上的。 - chiggsy
实际上,Tcl语法的真正酷之处并不在于它像Bash一样,而是像Lisp S-表达式一样,从而允许强大的元语言抽象和语法元编程。 - Jörg W Mittag

18

谁说它们不是呢?看看Zoidberg吧。 REPL(Read Eval Print Loops)使得糟糕的Shell,因为每个命令都必须是语法正确的,而运行程序的过程从以下变为:

foo arg1 arg2 arg3
system "foo", "arg1", "arg2", "arg3"

别提重定向了,这个更让人头疼。

所以,你需要一个自定义的shell(而不是REPL),它能够理解命令和重定向,并且使用你想用来绑定命令的语言。我认为zoid(Zoidberg shell)做得非常好。


1
打算尝试使用Zoid。你知道吗,有多少人将其作为主要shell使用的基础数量吗? - Joel Berger
1
@Joel 我有所怀疑。它在 CPAN 上的最后更新时间是在 2006 年。很难超越 bash。它无处不在,功能完备且得到维护。像 zoid 这样的 shell 很有趣,但我总是回到 bashksh - Chas. Owens
1
是的,我也看到它没有得到维护。我想知道它可能需要什么样的改进,我会有兴趣使用具有这种功能的外壳。 - Joel Berger
6
我实际上已经修复了一些重大的错误(我是新的维护者!)。查看 Zoidberg 来满足您所有基于Perl的shell需求! - Joel Berger
这是一个真正的问题:像Zoidberg shell这样基于Perl的shell和Raku自带的东西(例如Raku REPL)之间有什么区别? - jubilatious1

14

12

否。


脚本语言可能不适合作为shell使用。


问题在于 宏语言 和其他一切之间的二分法。

Shell(命令行解释器)与其他传统宏语言(如nroffm4)处于同一类别。在这些处理器中,所有内容都是字符串,并且处理器定义了从输入字符串到输出字符串的映射关系。

在所有语言中,某些边界都会被越过,但通常很清楚一个系统的类别是还是另一种类型的"真实语言"

因此,当然,您可以使用诸如Ruby之类的语言中键入所有命令,甚至可能是真正Shell的次佳选择,但它永远不会成为宏语言。因为有太多的语法需要遵循。需要用太多引号。

但是,在开始在宏语言中编程时,它自己也存在问题,因为为了摆脱所有那些语法,必须做出太多妥协。字符串是没有引号的。各种魔法需要重新引入以注入缺少的语法。我曾经用nroff进行过code-golf,只是为了与众不同。这非常奇怪。在宏语言中大型实现的源代码令人生畏。


1
非常感谢!那确实是我“我已经试图表达原因,但除了假设两者对字符串的处理方式有所不同之外,我无法表达”的遗漏点。如果有关于宏语言的研究,将会很有趣。 - Muhammad Alkarouri

7
由于两者都是正式的编程语言,你可以在一个语言中做的事情,在另一个语言中也可以做。实际上这是一个设计重点问题。Shell语言被设计用于交互式使用,而脚本语言则不是。
设计上的基本区别在于命令之间数据的存储和变量的作用域。在Bash等语言中,你必须绕过一些障碍才能存储一个值(例如,类似set a='something'的命令),而在像Python这样的语言中,你只需使用赋值语句(a = 'something')即可。当在shell语言中使用这些值时,你必须告诉语言你想要变量的值,而在脚本语言中,你必须告诉语言你想要字符串的立即值。这在交互式使用时会产生影响。
在脚本语言中,如果ls被定义为一个命令
a = some_value

ls a*b  

“a”是什么意思?这是否意味着some_value *(无论b是什么),还是您的意思是'a'任意字符串'b'?在脚本语言中,默认情况下是存储在内存中的内容。

ls 'a*b'  Now means what the Unix ls a*b means.

在类似Bash的语言中
set a=some_value

ls a*b   means what the Unix ls a*b means.

ls $a*b  uses an explicit recall of the value of a.

脚本语言可以轻松存储和调用值,而且很难对一个值进行短暂的范围限制。Shell语言可以存储和调用值,但每个命令的作用域都非常短暂。

谢谢。这就是我在问题中所提到的“字符串处理”,但你解释得更好。 - Muhammad Alkarouri

6
我认为这是一个解析问题。Shell语言默认假设普通的$ xxx命令意味着你要运行一个命令。在Python和Ruby中,你需要使用system("command")或类似的方法。
并不是说它们不适合,只是还没有人真正去做;至少我是这样认为的。Ruby中的Rush就是一个尝试,Python有IPython或类似的东西。

谢谢你分享链接。我之前用过IPython,不过我的第一印象是Rush看起来更好一些,可能是因为它的界面更流畅。我需要再仔细看看它。 - Muhammad Alkarouri

3

可扩展性和可扩展性?通用Lisp(甚至可以在Unix环境中作为登录shell运行CLISP和其他实现)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接