ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Zsh和Bash,究竟有何不同

2020-06-28 10:02:24  阅读:241  来源: 互联网

标签:shell 有何 echo sh array Zsh Bash


偶然发现这个zsh和bash的介绍文章,作者讲的很基础细节,转载记录学习下,也有很多其他好文章,向作者取经学习。

转载于:破冰 https://www.xshell.net/thinking/1513.html

坑很深。

已经有不少人写过类似“为什么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/shdash(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++)); do  echo ${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++)); do  echo $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有一个不同寻常的特性:支持使用方括号进行更复杂的globbingarray[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 

标签:shell,有何,echo,sh,array,Zsh,Bash
来源: https://www.cnblogs.com/xiaoqiangink/p/13201414.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有