https://www.zhihu.com/question/21418449

Mort | Zsh vs. Bash:不完全对比解析(1)

2014-10-07  bdpqlxz

Zsh和Bash,究竟有何不同

已经有不少人写过类似“为什么Zsh比Bash好”“为什么Zsh比* shell好”的文章了,讲解如何配置Zsh或折腾各种oh-my-zsh主题的教程也是一搜一大箩,但是却极少看到Zsh和Bash这两个Shell作为脚本语言时的具体差异比较。那么,这里就是一篇,从语言特性的角度上简单整理了两者一些细微的不兼容之处,供编写可移植Shell脚本时参考。(仅仅是从我自己过去的经验教训中总结出来的,所以应该也是不完全的。)

开始之前:理解Zsh的仿真模式(emulation mode)

一种流行的说法是,Zsh是与Bash兼容的。这种说法既对,也不对,因为Zsh本身作为一种脚本语言,是与Bash不兼容的。符合Bash规范的脚本无法保证被Zsh解释器正确执行。但是,Zsh实现中包含了一个屌炸天的仿真模式(emulation mode),支持对两种主流的Bourne衍生版shell(bash、ksh)和C shell的仿真(csh的支持并不完整)。在Bash的仿真模式下,可以使用与Bash相同的语法和命令集合,从而达到近乎完全兼容的目的。为了激活对Bash的仿真,需要显式执行:

$ emulate bash

等效于:

$ emulate sh

Zsh是不会根据文件开头的shebang(如#!/bin/sh#!/bin/bash)自动采取兼容模式来解释脚本的,因此,要让Zsh解释执行一个其他Shell的脚本,你仍然必须手动emulate sh或者emulate ksh,告诉Zsh对何种Shell进行仿真。

那么,Zsh究竟在何时能够自动仿真某种Shell呢?

对于如今的绝大部分GNU/Linux(Debian系除外)和Mac OS X用户来说,系统默认的/bin/sh指向的是bash

$ file /bin/sh
/bin/sh: symbolic link to `bash'

不妨试试用zsh来取代bash作为系统的/bin/sh

# ln -sf /bin/zsh /bin/sh

所有的Bash脚本仍然能够正确执行,因为Zsh在作为/bin/sh存在时,能够自动采取其相应的兼容模式(emulate sh)来执行命令。也许正是因为这个理由,Grml直接选择了Zsh作为它的/bin/sh,对现有的Bash脚本能做到近乎完美的兼容。

无关主题:关于/bin/sh和shebang的可移植性

说到/bin/sh,就不得不提一下,在Zsh的语境下,sh指的是大多数GNU/Linux发行版上/bin/sh默认指向的bash,或者至少是一个Bash的子集(若并非全部GNU Bash的最新特性都被实现的话),而非指POSIX shell。因此,Zsh中的emulate sh可以被用来对Bash脚本进行仿真。

众所周知,Debian的默认/bin/sh是 dash(Debian Almquist shell),这是一个纯粹POSIX shell兼容的实现,基本上你要的bash和ksh里的那些高级特性它都没有。“如果你在一个#!/bin/sh脚本中用到了非POSIX shell的东西,说明你的脚本写得是错的,不关我们发行版的事情。”Debian开发者们在把默认的/bin/sh换成dash,导致一些脚本出错时这样宣称道。当然,我们应该继续假装与POSIX shell标准保持兼容是一件重要的事情,即使现在大家都已经用上了更高级的shell。

因为有非GNU的Unix,和Debian GNU/Linux这类发行版的存在,你不能够假设系统的/bin/sh总是GNU Bash,也不应该把#!/bin/sh用作一个Bash脚本的shebang(——除非你愿意放弃你手头Shell的高级特性,写只与POSIX shell兼容的脚本)。如果想要这个脚本能够被方便地移植的话,应指定其依赖的具体Shell解释器:

#!/usr/bin/env bash

这样系统才能够总是使用正确的Shell来运行脚本。

(当然,显式地调用bash命令来执行脚本,shebang怎样写就无所谓了)


echo命令 / 字符串转义

Zsh比之于Bash,可能最容易被注意到的一点不同是,Zsh中的echoprintf是内置的命令。

$ which echo
echo: shell built-in command$ which printf
printf: shell built-in command

Bash中的echoprintf同样是内置命令:

$ type echo
echo is a shell builtin$ type printf
echo is a shell builtin

感谢读者提醒,在Bash中不能通过which来确定一个命令是否为外部命令,因为which本身并不是Bash中的内置命令which在Zsh中是一个内置命令。

Zsh内置的echo命令,与我们以前在GNU Bash中常见的echo命令,使用方式是不兼容的。

首先,请看Bash:

$ echo \
$ echo \\\\

我们知道,因为这里传递给echo的只是一个字符串(允许使用反斜杠\转义),所以不加引号与加上双引号是等价的。Bash输出了我们预想中的结果:每两个连续的\转义成一个\字符输出,最终2个变1个,4个变2个。没有任何惊奇之处。

你能猜到Zsh的输出结果么?










$ echo \
$ echo \\\

(゜Д゜*)

解释稍后。

我们还知道,要想避免一个字符串被反斜杠转义,可以把它放进单引号。正如我们在Bash中所清楚看到的这样,所有的反斜杠都照原样输出:

$ echo '\\'
\
$ echo '\\\\'
\\\

再一次,你能猜到Zsh的输出结果么?










$ echo '\\'$ echo '\\\\'
\

((((((゜Д゜*))))))))))))

这个解释是这样的:在前一种不加引号(或者加了双引号)的情形下,传递给echo内部命令的字符串将首先被转义,echo \\中的\\被转义成\echo \\\\中的\\\\被转义成\\。然后,在echo这个内部命令输出到终端的时候,它还要把这个东西再转义一遍,一个单独的\没法转义,所以仍然是作为\输出;连续的\\被转义成\,所以输出就是\。因此,echo \\echo \\\\的输出相同,都是\

为了让Zsh中echo的输出不被转义,需要显式地指明-E选项:

$ echo -E \
$ echo -E \\\\

于是,我们也就知道在后一种加单引号的情形下,如何得到与原字符串完全相同的输出了:

$ echo -E '\\'
\
$ echo -E '\\\\'
\\\

而Bash的echo默认就是不对输出进行转义的,若要得到转义的效果,需显式地指定-e选项。Bash和Zsh中echo命令用法的不兼容,在这里体现出来了。

变量的自动分字(word splitting)

在Bash中,你可以通过调用外部命令echo输出一个字符串:

echo $text

我们知道,Bash会对传递给命令的字符串进行分字(根据空格或换行符),然后作为多个参数传给echo。当然,作为分隔符的换行,在最终输出时就被抹掉了。于是,更好的习惯是把变量名放在双引号中,把它作为一个字符串传递,这样就可以保留文本中的换行符,将其原样输出。

echo "$text"

在Zsh中,你不需要通过双引号来告诉解释器“$text是一个字符串”。解释器不会把它转换成一个由空格或者\n分隔的参数列表或者别的什么。所以,没有Bash中的trick,直接echo $text就可以保留换行符。但是,如前一节所说,我们需要一个多余的工作来保证输出的是未转义的原始文本,那就是-E选项:

echo -E $text

从这里我们看到,Zsh中的变量在传递给命令时是不会被自动切分成words然后以多个参数的形式存在的。它仍然保持为一个量。这是它与传统的Bourne衍生shell(ksh、bash)的一个重要不兼容之处。这是Zsh的特性,而不是一个bug。

通配符展开(globbing)

通配符展开(globbing)也许是Unix shell中最为实用化的功能之一。比起正则表达式,它的功能相当有限,不过它的确能满足大部分时候的需求:依据固定的前缀或后缀匹配文件。需要更复杂模式的时候其实是很少见的,至少在文件的命名和查找上。

Bash和Zsh对通配符展开的处理方式有何不同呢?举个例子,假如我们想要列举出当前目录下所有的.markdown文件,但实际上又不存在这样的文件。在Zsh中:(注意到这里使用了内置的echo,因为我们暂时还不想用到外部的系统命令)

$ echo *.markdown
zsh: no matches found: *.markdown

Bash中:

$ echo *.markdown
*.markdown

Zsh因为通配符展开失败而报错;而Bash在通配符展开失败时,会放弃把它作为通配符展开、直接把它当做字面量返回。看起来,Zsh的处理方式更优雅,因为这样你就可以知道这个通配符确实无法展开;而在Bash中,你很难知道究竟是不存在这样的文件,还是存在一个文件名为'*.markdown'的文件。

接下来就是不那么和谐的方面了。

在Zsh中,用ls查看当然还是报错:

$ ls *.markdown
zsh: no matches found: *.markdown

Bash,这时候调用ls也会报错。因为当前目录下没有.markdown后缀的文件,通配符展开失败后变成字面的'*.markdown',这个文件自然也不可能存在,所以外部命令ls报错:

$ ls *.markdown
ls: cannot access *.markdown: No such file or directory

同样是错误,差别在哪里?对于Zsh,这是一个语言级别的错误;对于Bash,这是一个外部命令执行的错误。这件差别很重要,因为它意味着后者可以被轻易地catch,而前者不能。

想象一个常见的命令式编程语言,Java或者Python。你可以用try...catch或类似的语言结构来捕获运行时的异常,比较优雅地处理无法预料的错误。Shell当然没有通用的异常机制,但是,你可以通过检测某一段命令的返回值来模拟捕获运行时的错误。例如,在Bash里可以这样:

$ if ls *.markdown &>/dev/null; then :; else echo $?; fi
2

于是,在通配符展开失败的情形下,我们也能轻易地把外部命令的错误输出重定向到/dev/null,然后根据返回的错误码执行后续的操作。

不过在Zsh中,这个来自Zsh解释器自身的错误输出却无法被重定向:

$ if ls *.markdown &>/dev/null; then :; else echo $?; fi
zsh: no matches found: *.markdown
1

大部分时候,我们并不想看到这些丑陋多余的错误输出,我们期望程序能完全捕获这些错误,然后完成它该完成的工作。但这也许是一种正常的行为。理由是,在程序语言里,syntax error一般是无法简单地由用户在运行阶段自行catch的,这个报错工作将直接由解释器来完成。除非,当然,除非我们用了邪恶的eval

$ if eval "ls *.markdown" &>/dev/null; then :; else echo $?; fi
1

Eval is evil. 但在Zsh中捕获这样的错误,似乎没有更好的办法了。必须这么做的原因就是:Zsh中,通配符展开失败是一个语法错误。而在Bash中则不是。

基于上述理由,依赖于Bash中通配符匹配失败而直接把"*"当作字面量传递给命令的写法,在Zsh中是无法正常运行的。例如,在Bash中你可以:(虽然在大部分情况下能用,但显然不加引号是不科学的)

$ find /usr/share/git -name *.el

因为Zsh不会在glob扩展失败后自动把"*"当成字面量,而是直接报错终止运行,所以在Zsh中你必须"*.el"加上引号,来避免这种扩展:

$ find /usr/share/git -name "*.el"

字符串比较

在Bash中判断两个字符串是否相等:

[ "$foo" = "$bar" ]

或与之等效的(现代编程语言中更常见的==比较运算符):

[ "$foo" == "$bar" ]

注意等号左右必须加空格,变量名一定要放在双引号中。(写过Shell的都知道这些规则的重要性)

在条件判断的语法上,Zsh基本和Bash相同,没有什么改进。除了它的解释器想得太多,以至于不小心把==当做了一个别的东西:

$ [ foo == bar ]; echo $?
zsh: = not found

要想使用我们最喜欢的==,只有把它用引号给保护起来,不让解释器做多余的解析:

$ [ foo "==" bar ]; echo $?
1

所以,为了少打几个字符,还是老老实实用更省事的=吧。

数组

同样用一个简单的例子来说明。Bash:

array=(alpha bravo charlie delta)
echo $array
echo ${array[*]}
echo ${#array[*]}
for ((i=0; i < ${#array[*]}; i++)); doecho ${array[$i]}
done

输出:

alpha
alpha bravo charlie delta
4
alpha
bravo
charlie
delta

很容易看到,Bash的数组下标是从0开始的$array取得的实际上是数组的第一个元素的值,也就是${array[0]}(这些行为和C有点像)。要想取得整个数组的值,必须使用${array[*]}${array[@]},因此,获取数组的长度可以使用${#array[*]}。在Bash中,必须记得在访问数组元素时给整个数组名连同下标加上花括号,比如,${array[*]}不能写成$array[*],否则解释器会首先把$array当作一个变量来处理。

再来看这段Zsh:

array=(alpha bravo charlie delta)
echo $array
echo $array[*]
echo $#array
for ((i=1; i <= $#array[*]; i++)); doecho $array[$i]
done

输出:

alpha bravo charlie delta
alpha bravo charlie delta
4
alpha
bravo
charlie
delta

在Zsh中,$array$array[*]一样,可以用来取得整个数组的值。因此获取数组的长度可直接用$#array

Zsh的默认数组下标是从1而不是0开始的,这点更像C shell。(虽然一直无法理解一个名字叫C的shell为何会采用1作为数组下标开始这种奇葩设定)

最后,Zsh不需要借助花括号来访问数组元素,因此Bash中必需的花括号都被略去了。

关联数组

Bash 4.0+和Zsh中都提供了对类似AWK关联数组的支持。

declare -A array
array[mort]=foo

和普通的数组一样,在Bash中,必须显式地借助花括号来访问一个数组元素:

echo ${array[mort]}

而Zsh中则没有必要:

echo $array[mort]

说到这里,我们注意到Zsh有一个不同寻常的特性:支持使用方括号进行更复杂的globbing,array[mort]这样的写法事实上会造成二义性:究竟是取array这个关联数组以mort为key的元素值呢,还是以通配符展开的方式匹配当前目录下以"array"开头,以"m""o""r""t"任一字符结尾的文件名呢?

array[mort]=作为命令开始的情况下,不存在歧义,这是一个对关联数组的赋值操作。在前面带有$的情况下,Zsh会自动把$array[mort]识别成取关联数组的值,这也没有太大问题。问题出在它存在于命令中间,却又不带$的情况,比如:

read -r -d '' array[mort] << 'EOF'
hello world
EOF

我们的本意是把这个heredoc赋值给array[mort]数组元素。在Bash中,这是完全合法的。然而,在Zsh中,解释器会首先试图对"array[mort]"这个模式进行glob展开,如果当前目录下没有符合该模式的文件,当然就会报出一个语法错误:

zsh: no matches found: array[mort]

这是一件很傻的事情,为了让这段脚本能够被Zsh解释器正确执行,我们需要把array[mort]放在引号中以防止被展开:

read -r -d '' 'array[mort]' << 'EOF'
hello world
EOF

这是Zsh在扩展了一些强大功能的同时带来的不便之处(或者说破坏了现有脚本兼容性的安全隐患,又或者是让解释器混乱的pitfalls)。

顺便说一句,用Rake构建过项目的Rails程序员都知道,有些时候需要在命令行下通过方括号给rake传递参数值,如:

$ rake seeder:seed[100]

Zsh这个对方括号展开的特性确实很不方便。如果不想每次都用单引号把参数括起来,可以完全禁止Zsh对某条命令后面的参数进行glob扩展:(~/.zshrc

alias rake="noglob rake"

嗯,对于rake命令来说,glob扩展基本是没有用的。你可以关掉它。

分号与空语句

虽然有点无聊,但还是想提一下:Bash不允许语句块中使用空语句,最小化的语句是一个noop命令(:);而Zsh允许空语句

刚开始写Bash的时候,总是记不得什么时候该加分号什么时候不该加。比如

if [ 1 ]
then:
fi

如果放在一行里写,应该是

if [ 1 ]; then :; fi

then后面是不能接分号的,如果写成

if [ 1 ]; then; :; fi

就会报错:

bash: syntax error near unexpected token `;'

解释是:then表示一个代码段的开始,fi表示结束,这中间的内容必须是若干行命令,或者以分号;结尾的放在同一行内的多条命令。我们知道在传统的shell中,分号本身并不是一条命令,空字符串也不是一条命令,因此,then后面紧接着的分号就会带来一条语法错误。(有些时候对某个“语言特性”的所谓解释只是为了掩饰设计者在一开始犯的错误,所以就此打住)

在Zsh中,上述两种写法都合法。因为它允许只包含一个分号的空命令。

$ ;

当然,因为分号只是一个语句分隔符,所以没有也是可以的。这种写法在Zsh中合法:(then的语句块为空)

if [ 1 ]; then fi


第二弹

其实只是先挖个坑而已。我也不知道有没有时间写,暂且记上。

Zsh vs. Bash:不完全对比解析(2)

  • 别名,函数定义和作用域
  • 协进程(coprocess)
  • 重定向
  • 信号和陷阱(trap)

Linux服务器上zsh和bash的对比

使用默认指令列模式(bash shell)的管理员可能想仔细看看zshell或是zsh。由于它于bash相似,功能又有所加强,zsh在Linux社区获得了关注。那么zsh有什么不同之处呢?本文就列出在Linux服务器上zsh和bash的数据形式的对比。

作者:Mark 译来源:TechTarget中国|2011-06-13 14:03
移动端
收藏
分享

使用默认指令列模式(bash shell)的管理员可能想仔细看看zshell或是zsh。由于它于bash相似,功能又有所加强,zsh在Linux社区获得了关注。

那么zsh有什么不同之处呢?首先,zsh在感觉和功能上都和bash相似。但是一些增强功能让zsh变成一个有趣的选择。下面是一台Linux服务器上zsh和bash的数据形式的对比:

Zsh增强功能:标签完成和拼写错误修正

用过bash标签完成的管理员会发现zsh中的增加功能令人印象深刻。这些功能包括菜单中现有的自动完成命令选项,该菜单可以通过使用箭头键滚动。举例来说,键入以下命令将提供可能命令行标记的列表:

$ ls -

或是

$ rm -

选择要取消的特定程序,程序列表就和取消命令一起可用了。

另一个功能在内置页面程序中,它提供到less命令的快捷方式。要访问它,输入:

$<filename

这和在命令行上运行less文件名一样。

对笨拙的打字员来说,拼写错误修正功能可用了。例如,如果你输入了一条错误命令,zsh会提示修正:

$ lls

zsh: 要将 'lls'修改为 'ls' [nyae]吗?

要修改它,输入y,命令就更正为ls,接着命令就准备运行了。

其它选项也很实用。输入n拒绝命令修正,输入a中断命令,输入e跳转到命令行进行编辑。这个自动修正功能也能用于命令行标记和文件名,包括修改无效Git分支名称一类的机密事务。

开始使用zsh

为了快速地开始使用zsh,可利用Robby Russell收集的zsh主题、功能和工具,它们被预先打包成“Oh My Zsh”。

$ wget --no-check-certificate https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh

手动的zsh安装指令也可用,需要用户克隆Git repo并复制在.zshrc的草稿模板中。

“Oh My Zsh”知识库包含一个主题和功能集合用于现有zsh环境的建立和改变。它也能和卸载脚本一起用来简化移除:

$ uninstall_oh_my_zsh

与zsh shell一起供给的还有一些很好的文档和zsh参考卡。GitHub等网站上的在线资源是.zshrc文件的例子,它相当于zsh版的.bashrc文件,这些资源同时也提供如何定制zsh的示例或是示范加强命令行经验的炫酷技巧。

一些zsh功能可和bash一起用,但在bash上设置、配置更加复杂,这也解释了为什么人们有多页.bashrc文件。如果是Shell的高度使用者,zsh会是吸引你用来取代bash的选择。它的使用快速且简单,而它的一些重要功能也让与shell的互动更有趣。

原文:http://www.searchsv.com.cn/showcontent_49287.htm

Zsh vs. Bash不完全对比解析,zsh是一种更强大的被成为“终极”的Shell相关推荐

  1. 14.Linux rpm,brew软件安装包命令,zsh和bash,内置命令和外部命令,ps命令,top命令解析,swp,为什么cpu利用率很低,负载却很高?

    解释一下rpm, opt,opt-get, brew,brewhome rpm,apt,apt-get,brew,和homebrew都是与软件包管理有关的术语.它们分别用于不同的操作系统. rpm是一 ...

  2. Linux服务器上zsh和bash的对比

    使用默认指令列模式(bash shell)的管理员可能想仔细看看zshell或是zsh.由于它于bash相似,功能又有所加强,zsh在Linux社区获得了关注. 那么zsh有什么不同之处呢?首先,zs ...

  3. Fish vs. Zsh vs. Bash以及为什么要改用Fish

    事实上,大多数开发人员都喜欢Unix和类似Unix(基于Linux)的操作系统,例如macOS,Ubuntu等.它们稳定,强大,高度可定制,并且具有强大的Unix Shell. 什么是UNIX She ...

  4. Mac zsh切换bash bash切换zsh

    切换bash    ---->>>chsh -s /bin/bash 切换zsh      ------->>>chsh -s /bin/zsh

  5. MacOS 的 zsh 和 bash 切换

    目录 一.从 `bash` 切换到 `zsh` 1.使用系统自带的 `zsh` 2.使用第三方的 `zsh` 2.1.Clone代码到本地 2.2.备份你已存在的 `~/.zshrc` 文件 2.3. ...

  6. bash上的mysql在zsh用不了_Zsh和Bash的兼容性问题

    兼容 Zsh在大部分时候是兼容Bash, 是的"大部分时候" 如果你要强求的话,可以在Zsh里执行下面命令 emulate bash 数组 如果在loop里使用了数组,那么脚本在B ...

  7. zsh与bash区别

    zsh与bash区别 1. 两者之间比较 2. 配置zsh 1. 两者之间比较 zsh越来越受更多人欢迎,号称"终极shell",所以想去了解下它的过人之处,总之,从一个交互式终端 ...

  8. zsh和bash的区别

    看了一些资料,总结出来一句话:zsh完美兼容bash,并且有比bash更强大的功能,用起来也比bash更优雅. 参考1:Linux服务器上zsh和bash的区别 参考2:使用 zsh 的九个理由 参考 ...

  9. Mac中的Zsh和Bash你了解吗

    使用Mac系统的朋友应该比较熟悉Zsh和Bash这两个shell,但是对二者具体有什么区别可能不太了解.本文将从这两个shell入手,对相关概念以及二者区别进行解释. 1.什么是shell? shel ...

最新文章

  1. 【绝对靠谱】Vue生成二维码Qrcode,可插入二维码中心logo图标,可以设置二维码颜色大小等属性
  2. 用子函数的方法求一维数组中所有元素之和
  3. WINDOWS SERVER 2003 组策略应用
  4. 初等数论--同余--MILLER-RABIN素性检测算法优化
  5. 网络工程师人手必备!常用网络命令合集请收下
  6. 慕课网Spark SQL日志分析 - 4.从Hive平滑过渡到Spark SQL
  7. [51nod] 1301 集合异或和
  8. 一篇小文带你走进RabbitMQ的世界
  9. UILable和UITextField的详细讲解
  10. RMQ+1/-1算法 [转]
  11. svr预测出来是一条直线_如何预测股价目标位,涨跌目标预测法之“解消点”帮你寻找...
  12. python代码-你见过哪些令你瞠目结舌的 Python 代码技巧?
  13. css基本选择器,id选择器,class选择器,标签选择器,*通配符选择器,逗号 空格 + >
  14. android notify,android4.0 MediaPlayer的notify监听机制的全面剖析
  15. 地下城与勇士(DNF)安图恩副本(黑雾之源、震颤的大地、舰炮防御战、擎天之柱、能量阻截战、黑色火山、安徒恩的心脏)(童年的回忆)
  16. 操作系统锁的实现方法有哪几种_深入理解多线程(四)—— Moniter的实现原理
  17. python资产负债表_用Python清理雅虎财务资产负债表
  18. 2021年安全员-C证考试及安全员-C证考试技巧
  19. Redis数据结构之——跳表skiplist
  20. 项目管理师备考笔记:十大管理之范围管理

热门文章

  1. main函数的参数详解,它们是何时何处传入的?(main函数的参数值是从操作系统命令行上获得的)
  2. 罚函数法求解约束问题最优解
  3. JMM内存模型如何为并发保驾护航
  4. (39.3) Spring Boot Shiro权限管理【从零开始学Spring Boot】
  5. Java 中日期的几种常见操作 —— 取值、转换、加减、比较
  6. jaxb和dozer简介
  7. 教您怎么从spring 官网下载参考文档
  8. 如何配置天融信NGFW4000防火墙基于长连接的访问策略
  9. HTML5的Video标签的属性,方法和事件汇总
  10. Android ViewFlipper滑动屏幕切换