keep moving

一个友好的 shell

Posted on By condy

Fish – 交互友好型的Shell

原地址

这篇文章有点历史了 2005 年的产物。fish 的起始点。


介绍

在过去的十年中,花了很大精力使得计算机对用户友好。它取得了巨大发展,但在非图形程序(例如 Shell)上就显得缓慢。遗憾的是有许多的事情通过 Shell 做可以更加方便。那些命令(commands)管道(pipelines)环境变量(environment variables)多少有点复杂,但我相信现代的 Shell 更难使用,无论是对新手或老手。我写了一个新的 Shell - Fish ,或者可以叫做交互友好型(friendly interactive) Shell,其主要是为了解决我在其他 Shell 中遇到的一些问题。

fish 的特点有代码高量,高级的 tab 补全,更具发现性(discoverable)的帮助,修正的 Shell 语法以及其他。在这篇文章中,我将描述其中的一些特点以及解释为什么我认为它们是有帮助的。


语法高量

初学者喜欢语法高量,因为漂亮的颜色使得它们更加舒服。而且,语法高量也可以使得人更容易分析命令,找到错误。fish 有着一个高级的错误检测器,它用红色来高量错误。有命令拼写错误,选项拼写错误,读取不存在文件,括号、引号失配等其他常见错误。当然 fish 会将匹配的引号、括号高量。 语法高量


强大的历史系统

现代的 Shell 在一个文件中保存历史命令。你可以通过使用上、下方向键来查看以前的命令。 fish 通过整合历史搜索功能来延伸了这个概念。搜索历史,只需键入搜索字符串,按上箭头。通过使用上、下箭头,你可以搜索最近或更久的匹配。fish 还会自动移除重复的命令、高量匹配的子串。这 些特性使得搜索和重用以前的命令更加得快速。


高级的 tab 补全

tab 补全对新手、老手都会节省时间。而 fish 的 tab 补全引擎强大易使用。

  • fish 有着大量的特定命令 tab 补全,包含对于 manpage、主机名补全(例如在使用 ssh 时)。 bash 和 zsh 都支持命令特定补全,但都不默认启用。
  • 补全配备了一些说明。例如,当补全命令或 manpage 时,那些说明描述了这是什么信息。当补全变量时,就显示出了它的值。当补全 1 个文件时,这就变成了对文件类型的描述。zsh 在这方面有不足。
  • 补全还可以在有通配符*,?的情况下补全,当然也可以在花括号中,例如这样: input{a,b,c}.txt. zsh 也可以被配置成扩展通配符。但它仅仅是用一个匹配的实例来替代, 因而对于匹配多个文件就成为不可能了。
  • fish 试图通过截断长的补全来实现在一个页面上显示所有补全,但如果失败,一个内置的翻页器(pager)将被启用,其支持上下滚动,向上翻页/向下翻页和空格键。如果用户按下任何其他键,该翻页器将退出,随之而然的,相应的字符会被插入到命令缓冲区中。

补全


默认设置

zsh 提供了特定命令补全,一个历史文件,通配符的扩展还有其他的高级功能。但没有一个是默认开启的。事实上,一个用户首次使用 zsh 都会认为这是对 Bourne shell 的提升。bash 在这点上做得更好,但像特定命令补全这种功能是默认关闭的,而且默认的历史设置也不是非常有用。

我每次都抱怨 bash, zsh 有太多的可配置项,这绝不是有意黑它们,尽管我狂热地用 Shell 10 几年了,我依旧从那些记录不完全的文档中找到新的功能、特点,它们被默认关闭着或者根本无实现意义。

fish 的设计哲学就是让事情工作得正确、更少的配置而功能不减少。


上下文相关、用户友好性的帮助页

尽管 manpage 给了你一个得当的信息,让你知道了如何使用这些特定的命令,但这些文档对于 Shell 和内置的命令来说及其难用。从 bash manpage 中很难得到你想要的信息。fish 尝试着以一种易于使用的形式来提供上下文相关的文档。

要访问 fish 的 help 页面,可以使用help命令。简单地写下help然后敲击回车,这将启动用户最喜欢的浏览器来查看本地的手册。手册中有很多的主题可以被help特定查询, 例如help syntax,help editor等将打开特定主题的文档。为了使得帮助页面更加方便查询,每次 fish 的开启都会打印一条信息,描述它是如何访问的。找到一个特定的主题也是非常方便的,因为章节的名字可以被补全。

在 fish 中,内置的命令支持-h--help选项,这将打印出一个详细的解释来描述命令是怎样工作的。 唯一的例外是那些开始一个新代码区块的命令,例如for, if, while, function… 为了得到它们的帮助信息,请直接在 Shell 下输入命令。

错误报告往往是一个经常被忽视的帮助。语法错误时,fish 尝试着给出一份详细的报告来说明什么出错了,如果可能的话,也会打印出一份帮助信息。


桌面集成

因为很多用户都是在图形桌面下使用虚拟终端来操纵 Shell, Shell 应该集成到桌面中去。fish 使用 X 的剪切板来复制,粘贴,所以你可以使用 Control-Y 粘贴前剪切板中的内容到 fish 下,当然 Control-K 使得此行的剩余部份移入至剪切板。


打开文件

用一个图形文件管理器来打开一个文件或图像是很简单的。你简单地双击它,然后它被默认的程序打开。在 Shell 中,这会变得因难。你需要知道哪个程序可以用来处理给定类型的文件,当然必须要知道如何启动它。从命令行中打开一个 HTML 文件不是一个简单的任务 ,因为大部分的游览器所期望的是一个 URL, 以及可能完整的绝对路径,而非一个文件。 fish 有一个功能,一个叫做open的命令使用类型(mime-type)数据库或 .desktop 来打开所对应的文件。


更好的 Shell 语法

然而 Shell 自 70 年代以来已经取得一些功能,现代 POSIX Shell 语法例如 bash, zsh 都是与古老的 Bourne Shell 它已经30岁了相类似的。它语法上有大量的问题,我觉得应该改变一下。但是这将导致 fish 的语法与其它的 Shell 不兼容,虽然旧脚本转化成新脚本也不是很难。


(Blocks)</rt></ruby>

在 Shell 编程中有很多情况下你要指定多个命令。这包括条件块、循环块以及函数定义。在一般的 Shell 中,块的结束毫无逻辑。条件块用反向的命令来终结,例如: if true; echo yes; fi,但循环块用done来终结,例如: while true; do each hello; done,个别的条件块(case)用;;来终结。函数用}来 终结。任意的保留字像then, do之类的也将在代码中大量出现。fish 使用单个、统一的形式来 作为块的终结:end命令。对于在 POSIX shell 和 fish 中的块语法,请看下表。

POSIX command fish command
if true; then echo hello; fi if true; echo hello; end
for i in a b c; do echo $i; done for i in a b c; echo $i; end
case $you in *) echo hi;; esac;; switch $you; case '*'; echo hi; end
hi() { echo hello; } function hi; echo hello; end

引用

原始的 Bourne Shell 是一个宏语言,它在上面做变量的替换、tokenization 和其他的一些操作而没有理解底下的语法。这就导致了不想要的负作用,考虑下面的命令行:

smurf=blue; smurf=evil; echo Smurfs are $smurf

在 Bourne Shell 中,这将输出’Smurfs are blue’.像 M4 和 Bourne 这样的宏语言是不直观的, 但一旦你理解了它们是如何运作之后,它们至少是稍微有点逻辑的,可以被预知。bash 用了 bison 语法来实现,但是仍旧选择模仿一些古老的 Bourne shell 的一些怪癖。

上面的例子将输出’Smurfs are evil’。换句话说,变量的值仍旧是对于空格做 tokenization,这就意味着你不能够写下rm $file

因为包含空格的变量 file ,rm将尝试着删除错误的多个文件(因为 file 中含有空格,仅做了替换,因而变成了删除多个文件)。 为了解决这个,它的用户确保每一处使用变量都要被引号包含,例如这样rm "$file"。这正是 shell 脚本中 bug 的源头之一,因为这是一个简单的默认的结果都会变得糟糕,很少达到期望。

总的来说,使 bash 变成一个像这样古怪的非宏语言(non-macro language),它的结果也会变得无法预知,学习起来非常因难。

fish 不是一个宏语言也不想假装是宏语言。带有空格的变量仍旧是一个token. 于此,没有必要用双引号了来说明“这整块是与单引号括起来是不一样的。”所以,单、双引号都意味着同一样东西,并且引号可以被嵌套。


变量的赋值

变量的赋值在 Bourne shell 下是空白字符相关的。foo=bar是一个赋值,但foo = bar就不是。这真是一个SB的做法。fish 在修复这个问题的时候也引入了一些东西。它借用了 csh 的语法,使用了命令 set 来对一个变量赋值。这样做的原因是在 fish 中任何东西都是一个命令(everything is a command)。循环、条件以及其他类型的高级语言构造器都被实现作为内置命令,都遵循同一个语法规则。 这使得 fish 可以更简单的学习、理解,当然也更容易实现。

将变量 smurf 赋值为 blue,可以使用下面的命令:

set smurf blue

默认的,变量局部存在于当前块,在当前块脱离范围后此变量就会消失。为了使一个变量全局化, 你需要使用-g开关。


两种创建函数的方式,不过都是不好的

bash, zsh 和其它一般的 shell 允许你用两种不同的方式来创建存储函数。作为 aliases 或作为 functions.

Aliases 使用命令alias ll="ls -l"来定义。Aliases 只是在命令行中进行简单地字符串替换 。由于这个原因,aliases 有如下的限制:

  • 在 alias 中你只能针对最后一个命令重定向输入输出。
  • 你仅能对最后一个命令指定参数。
  • aliases 定义是单个字符串,这意味复杂的函数几乎是不可能创建的。

由于上述这些限制,bash 使用了第二种方式来指定函数,使用了这样的语法:

ll () { ls $*; }

这解决了 aliases 的问题,但我认为这语法太糟糕。它看起来像 C 的代码,但每一个期望它可以像 C 一样工作的人发现这是不可能的。你不能够在括号中指定参数,这些代码只是在这个地方看起来像 C 的代码。花括号中是一系列伪命令,在上述例子中跳过分号会导致语法错误。最奇怪的是除去ls(之间的空白也会导致语法错误。显然,这不是一个经过仔细考虑加上去的语法。fish 为定义函数只使用一种语法,这种定义以仅仅是一种常见的一般命令:

function ll; $argv; end

这种写法比上面的例子要罗嗦一点,但它用一种方式解决了上面两种语法上,并且与 fish 的其他语法统一。


不能确定语法是否正确

因为在一般的 shell 中变量如同命令一样的使用,检查脚本语法是不可能的。

例如,这小小的 bash/zsh 代码片断可能抑或不可能是合法的,这取决于你的运气:

if true; then if [$RANDOM -lt 1024]; then END=fi; else END=true; fi; $END

bash, zsh 尝试着确定在当前缓冲区中命令是否结束,但是可能就会存在这种情况,命令失败了。

fish 不允许变量作为命令来解决这种情况。使用eval命令或者使用函数会更加清晰。


不重要的问题

字符串'$foo',"$foo"`$foo`看起来很类似,然而这三个是完全不一样的东西。fish 通过令'$foo'"$foo"相同来来解决上述问题,但同时也使得子 shell(sub-shell) 的语法要使用圆括号了。

大量的 UNIX 标准命令,像printf, echo, killtest在 bash/zsh 中都是内置的命令。就我认为而言,做这件事只有一个好处:一点小小的性能提升, 但是因此引入的缺点却有很多:

  • 这些内置命令中存在的 bugs 威胁着整个 shell
  • 命令随 shell 的变更而变更它们的意义
  • 其它 shell 的用户不会从你写过的命令中受益
  • 它打破了 UNIX 只做一件事情但把它做好(doing only one thing but doing it well)</ruby> 的哲学
  • 内存使用率增加

对于这些原因,fish 尽可能少地实现内置命令。包含块命令例如for, end,fish 实现了 24 个内置的命令,而 bash 的内置命令在 60 到 70 之间。


总结

在常规 shell 语法中没有什么大的问题使得 shell 无法运作。30 年间,shell 对于计算机用户来说已经成为一个有利的工具。但是即使是很好的设计,强大的程序仍然需要更新来移除一些旧的错误。对于初学者来说,如上介绍的 fish 的改变使得 shell 语言更容易理解、记忆,一旦学过,对于有经验的用户来说,仅可能引入少量的 bugs。一般的计算机语言已经在过去的 30 年间进化、取代多次了,为什么 shell 语言不变一下呢?


将来的计划

fish 的代码基础被优美地构建。它也被很好的记录并且尽可能保持小。但是,因为代码是新的而没有经过测试,它比其它的 shell 包含更多的 bugs。它需要一个审计员来确保安全性、稳定性以及更复杂的测试组。我想要加入的功能还有很多,但它们都不需要完全的重写。

在语法方面还有一个重要的部分没有实现,就是 IO 对于块和函数的重定向。在当前这个情况下,在管道中使用函数以及重定向函数的输出都是可行的,但函数不能够输出二进制数据,而且输入重定向是不被支持的。对于代码块的输入/输出重宝向也是不可能的。

将来的版本将允许如下的代码:

for i in (find . -name "*.c"); echo $i; grep "mbstowcs" $i; end | less # 目前在2.0.0-2中已经实现

为了在less中查看多个命令的输出,可以使用:

cat foo.txt | while test -z quit
    read type
    switch $type
        case quit
            set quit 1
        ...
    end
end

我正在实现这些功能,希望在最近几周释出一个带有这些功能的新版本。

fish 当前的特定命令补全大约有60多个,依然有许多额外的命令需要特定补全。我希望可以使用 Doclifter 来自动地从 manpage 转换到 tab 补全说明。

另一个需要注意的就是文档了。当一个工程已经有很多文档时,fish 将从 shell scripting tutorials、UNIX 哲学以及多种其它形式的文档中受益。

在语法合法性检查和的错误报告上面仍然有更多工作。fish 已经做了一些基本的语法检查, 但是在未来版本中在运行前能够检测到更多的语法错误。

我还计划实现一些小功能:

  • undo/redo 的支持
  • 交互性的目录历史,可以使用 Alt-up, Alt-down 来把上一级目录插入到当前的命令缓冲区来
  • 多行编辑
  • 鼠标的支持,可以在窗口上点击来移动光标,也可以通过点击来选择一个补全
  • 用一种朴素的颜色来显示建议补全
  • 存储上一个命令的输出。计算器通常使用变量 ans 来指代上一个计算的结果。如果$ans是上一个命令的输出,那该有多好啊

如果你认为 fish 听起来有趣,请过来一起纺织它吧。你可以从这里上下载。fish基于Gnu General Public(GPL)协议释放了,它的i386 rpm和i386 deb包可以获取。

现在一般的发行版源里都有 fish.