为何脚本语言(如Perl、Python和Ruby)不适合作为Shell语言?

353 投票
12 回答
68393 浏览
提问于 2025-04-16 03:41

Bash (bash), Z shell (zsh), Fish (fish) 这些 shell 语言和上面提到的脚本语言有什么不同,使得它们更适合在命令行中使用呢?

在使用命令行时,shell 语言似乎要简单得多。比如我觉得用 bash 比在 IPython 的 shell 配置中使用要顺畅得多,尽管有些人可能有不同的看法。我想大多数人会同意,中到大规模的编程在 Python 中比在 Bash 中更容易。我最熟悉的语言就是 Python。对于 PerlRuby 也是如此。

我试着想清楚原因,但除了认为这两者在处理字符串时的不同可能与此有关外,我实在说不出其他的理由。

我问这个问题的原因是希望能开发出一种同时适用于这两种语言的语言。如果你知道有这样的语言,请分享一下。

正如 S.Lott 所说,这个问题需要一些澄清。我是在询问 shell 语言 的特性和脚本语言的特性之间的区别。所以这个比较并不是关于各种交互式环境(比如 REPL)的特点,比如历史记录和命令行替换。换句话说,我想问的是:

一种适合设计复杂系统的编程语言,是否也能同时表达一些有用的单行命令,以便访问文件系统或控制任务?一种编程语言是否能在需要时有效地扩展,也能在不需要时缩小?

12 个回答

54

一个命令行语言应该简单易用。你希望输入一些一次性的命令,而不是写小程序。也就是说,你想输入

ls -laR /usr

而不是

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

这也意味着,命令行语言并不太在乎你输入的参数是选项、字符串、数字还是其他东西。

另外,命令行中的编程结构是附加的,甚至不一定是内置的。比如,在BashBourne shellsh)中结合使用if[,还有seq命令来生成序列等等。

最后,命令行有一些特定的需求,这些在编程中可能用得少,或者用得不同。比如,管道、文件重定向、进程/作业控制等等。

60

这是一种文化现象。Bourne shell已经有将近25年的历史了,它是最早的脚本语言之一,也是Unix管理员们解决问题的第一个“好”方法。简单来说,它就像是把其他工具连接起来的“胶水”,让你可以完成一些常见的Unix任务,而不需要每次都去编译一个复杂的C程序。

按照现代的标准来看,它的语法非常糟糕,还有一些奇怪的规则和标点符号用法(在1970年代每个字节都很重要的时候,这些是有用的),让非管理员的人很难理解。但它确实“完成了任务”。它的缺陷和不足通过后来的改进(比如ksh、bash、zsh)得到了修正,而不需要重新设计它的基本思想。管理员们仍然坚持使用这种核心语法,因为尽管它很奇怪,但没有其他工具能更好地处理简单的事情而不造成干扰。

对于复杂的任务,Perl出现了,变成了一种半管理员、半应用程序的语言。但事情越复杂,就越被视为应用程序的工作,而不是管理员的工作,因此商业人士往往会寻找“程序员”而不是“管理员”来完成这些任务,尽管合适的技术人员通常两者都能胜任。所以,大家的关注点就转向了这方面,而Perl在应用能力上的进化最终导致了……Python和Ruby的出现。(这有点过于简单化,但Perl确实是这两种语言的灵感来源之一。)

结果是什么呢?专业化。管理员们觉得现代的解释性语言对于他们每天的工作来说太复杂了。总体来说,他们是对的。他们不需要对象,也不关心数据结构。他们需要的是“命令”。他们需要的是“胶水”。没有其他东西能比Bourne shell更好地处理“命令”(也许Tcl可以,但这里已经提到过了);而Bourne shell已经足够好了。

程序员们——如今越来越需要了解devops——看着Bourne shell的局限性,心想怎么会有人愿意忍受它。但他们熟悉的工具,虽然确实倾向于Unix风格的输入输出和文件操作,却并不一定更适合这个目的。我曾用Ruby写过备份脚本和文件重命名的简单程序,因为我对Ruby更熟悉,而不是bash,但任何一个专职的管理员都可以用bash做到同样的事情——可能用更少的代码和更少的开销,但无论如何,它都能正常工作。

人们常常会问“为什么大家都用Y而不是Z,明明Z更好?”——但技术的进化,就像其他一切的进化一样,往往停留在“足够好”的阶段。除非“更好”的解决方案被视为一个无法接受的挫折,否则它不会胜出。Bourne类型的脚本可能让感到沮丧,但对于那些经常使用它的人来说,以及它所设计的工作,它总是能完成任务。

482

我想到了一些区别,随便说说,没有特别的顺序:

  1. Python等语言是为了方便编写脚本而设计的,而Bash等则是专门为了脚本而设计的,毫不妥协。换句话说,Python既能处理脚本,也能处理其他任务,而Bash只关注脚本。

  2. Bash等是无类型的,而Python等是强类型的。这意味着数字123、字符串123和文件123是完全不同的东西。不过,它们不是静态类型的,这意味着为了区分这些类型,它们需要用不同的表示方式。
    举个例子:

                    | 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等语言设计得可以处理上万行、十万行,甚至百万行的程序,而Bash等则设计得可以处理只有10个字符的程序。

  4. 在Bash等中,文件、目录、文件描述符和进程都是一等公民,而在Python中,只有Python对象是一等公民。如果你想操作文件、目录等,必须先把它们包装成Python对象。

  5. Shell编程基本上就是数据流编程。没人意识到这一点,甚至连写Shell的人也不知道,但事实证明,Shell在这方面很出色,而通用编程语言就不那么擅长。在通用编程的世界里,数据流通常被视为一种并发模型,而不是编程范式。

我觉得,试图通过在通用编程语言上添加功能或领域特定语言来解决这些问题并不奏效。至少,我还没见过一个令人信服的实现。有RuSH(Ruby Shell),它试图在Ruby中实现一个Shell,还有rush,这是一个在Ruby中用于Shell编程的内部DSL,还有Hotwire,这是一个Python Shell,但在我看来,这些都无法与Bash、Zsh、fish等竞争。

实际上,在我看来,目前最好的Shell是Microsoft PowerShell,这让我很惊讶,因为微软在过去几十年里一直有着最糟糕的Shell。我是说,COMMAND.COM?真的?(不幸的是,他们的终端仍然很糟糕。仍然是自Windows 3.0以来的“命令提示符”。)

PowerShell基本上是通过忽略微软过去做的一切(COMMAND.COMCMD.EXE、VBScript、JScript),而是从Unix Shell开始,然后去掉所有向后兼容的冗余(比如命令替换的反引号),并稍微调整一下,使其更适合Windows(比如使用现在不再使用的反引号作为转义字符,而不是在Windows中用作路径分隔符的反斜杠)。然后,魔法就发生了。

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

问题2通过将参数静态类型化来解决。因此,你可以直接写123,PowerShell就知道它是字符串、数字还是文件,因为cmdlet(在PowerShell中,Shell命令称为cmdlet)会向Shell声明其参数的类型。这有很深远的影响:与Unix不同,在Unix中,每个命令负责解析自己的参数(Shell基本上将参数作为字符串数组传递),而在PowerShell中,参数解析是由Shell完成的。cmdlet指定所有选项、标志和参数,以及它们的类型、名称和文档(!)给Shell,然后Shell可以在一个集中地方执行参数解析、标签补全、智能感知、内联文档弹出等功能。(这并不是革命性的,PowerShell的设计者承认像数字命令语言(DCL)和IBM OS/400命令语言(CL)这样的Shell是先例。对于任何使用过AS/400的人来说,这应该听起来很熟悉。在OS/400中,你可以写一个Shell命令,如果你不知道某些参数的语法,可以简单地省略它们,然后按F4,这会弹出一个菜单(类似于HTML表单),里面有标记字段、下拉框、帮助文本等。这只有在操作系统知道所有可能的参数及其类型时才能实现。)在Unix Shell中,这些信息通常重复三次:在命令本身的参数解析代码中,在bash-completion脚本中用于标签补全,以及在手册页中。

问题4通过PowerShell对强类型对象的操作来解决,这包括文件、进程、文件夹等。

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

PowerShell的另一个好处是命名约定:所有cmdlet的命名格式都是动作-对象,而且还有特定动作和特定对象的标准化名称。(再次,这对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家族。例如,Tcl已经被提到过。Rexx也是另一种脚本语言。Emacs Lisp也是一种。还有一些已经提到的主机/中型Shell,比如OS/400命令行和DCL。此外,还有Plan9的rc。

撰写回答