指南网上关于zsh的文章很多,但是95%以上都是讲如何使用和配置的。关于如何用zsh编程的文章很少,能找到的大多也是寥寥数语,不系统。国外有好几本关于zsh的书,配置、使用、编写补全脚本等内容很多,对编程有用的页面不多,而且比较零散,不方便查询。至于官方文档?那就是连拥有多年编程经验的开发者都为之疯狂的神奇存在。可读性极差,基本没有例子。如果您不熟悉文档的结构和内容,很难找到您想要的内容。但是内容涵盖的很全面,近500页的口才,耐心看完总能找到。还有一个官方的“GettingStarted”文档,最后一次更新是在2002年,有300多页。至于可读性,比官网文档稍微好一点,还是有一定参考价值的。官网上也有一些链接,里面的内容比较零散,大家也可以看看。许多人使用bash语法在zsh中编写脚本。虽然它们可以正常运行,但遗憾的是它们无法利用zsh的许多优秀特性。熟悉zsh的独特特性对于编写脚本很有帮助。本系列文章与zsh的安装、使用、配置无关(如果需要配置文件可以参考我的.zshrc,里面有更详细的注释),也没有oh-my-zsh相关内容。安装zsh后,无需配置即可开始学习编写脚本。读者不需要有bash基础(了解一些更好),但需要接触过任何一种编程语言,了解一些编程的基本概念。为什么要用zsh写脚本很多人对zsh的认识停留在界面漂亮、主题多、插件多、补全性强等方面,而对zsh的语言特性了解不多。由于zsh与bash基本兼容,所以很多人使用bash语法编写zsh脚本,或者偶尔使用一些zsh特有的技巧,很难体会到zsh作为一种编程语言的强大。另外,有人认为几乎所有的类Unix系统都默认安装了bash,而zsh往往需要自己安装,所以为了通用性还不如用bash来写脚本。这种说法也有一定的道理,但并不影响所有的开发者。如果你是开源软件的开发者,避免使用zsh是有道理的,以防止清洁用户使用他们自己的软件,因为他们不想安装他们不能使用的zsh(但现在zsh的数量用户积累到一定程度)。另外,写脚本在公司内部使用等大部分场景不需要考虑这个因素。如果用在公司里,还有其他的因素。首先是zsh的部署成本。但是因为大部分情况下需要部署其他软件,即使是自己的脚本也可以用zsh打包部署(zsh去掉无用文件后只有1M多),所以基本不是问题。而如果使用系统默认的bash,也会涉及到版本不同导致的问题,比如不同系统的bash版本不同,或者系统升级后,升级bash导致之前的脚本挂掉等等。所以即使使用bash,也最好统一部署或者自带特定版本,而不是使用系统默认的,减少不必要的麻烦。二是非常重要的学习成本。因为会写bash的人很多,而会写zsh的人相对较少。如果只能自己写,那么和别人合作就会有问题。但是zsh的学习成本并没有那么大,尤其是对于bash开发者来说,只需要几十分钟的学习就可以大致了解zsh脚本,而且一步步写出来是很自然的,而且有时是记不住的也可以用bash语法编写。所以学习成本不是那么可观。第三个是用zsh开发的好处。如果zsh与bash相比没有明显的优势,为什么要学习和使用它?那么我们先从bash的痛点说起。我想经常写bash脚本的人,很少有人会竖起大拇指说bash真的很好用。相反,我听到几个开发人员说我写了一个超过2000(或其他行数)行的shell(bash)脚本。但几乎没有人认为编写一个2000多行的Python脚本是一件多么特别的事情。语法差(几乎所有从任何其他语言迁移过来的开发者都必须重新熟悉并习惯它的语法),严重依赖外部命令(因为文件系统错误等问题,如果外部命令挂了,脚本会被shock,不同的命令版本在使用上会有微秒级的差异,难以调试和测试,频繁新建进程性能低),功能又弱又蹩脚(很多需要经常使用的功能不全面或者不好用,比如字符串处理和数组使用)等等,让很多开发者头疼不已,甚至有人主张禁止使用shell脚本,全部换成Python等,但是Python并不适合所有场景,还有其他问题,也是浪费时间。zsh并没有解决所有这些问题,但与bash相比,它有很大的改进。比如zsh支持多种语法风格,开发者很容易找到亲切感;对外部命令的依赖比bash轻很多,大部分常用功能不需要使用外部命令,性能更好,调试更方便;在功能上和bash相比也有了很大的提升,应对不太复杂的场景绰绰有余。有人可能会说“一步到位”并使用Powershell更好。Powershell确实是比Python更好的shell脚本语言,但是使用它还有其他问题。首先,Powershell的学习成本肯定比zsh高。如果你想省点事,这不是一个好的选择。其次,Linux下的Powershell目前还处于测试版。很难说将来是否会有很多人使用它。如果很少人使用,那么生态环境就会成为问题。比如遇到问题后找不到解决方案,配套的软件和库不完善等等。再次,Powershell解释器的启动速度非常感人。在我的机器上,Windows下的Powershell空脚本耗时将近200毫秒,Linux下耗时更长(我只在WSL上安装试用,时间翻倍),而对于zsh,Linux下不超过5毫秒,并且在WSL下不超过20毫秒。如果写一个简单的脚本,运行时会卡顿,非常影响体验。最后,如果平时使用Powershell作为交互式shell,虽然脚本启动时间的问题有所缓解,但是用户体验会差很多,以后也很难改进,很容易亏大了增益。zshscriptsample你可以通过一个例子直观的感受一下用zsh写的脚本。这是一个删除当前目录及所有子目录中重复文件,并使用md5判断文件是否相同的脚本(不严谨)。熟悉bash的读者可以尝试用bash来完成同样的功能,然后对比代码(之前写过bash版的,这里就不贴了),就能感受到bash和zsh的区别了更直观。#!/bin/zshlocalfiles=("${(f)$(md5sum**/*(.D))}")localfiles_to_delete=()local-Amd5sfori($files){localmd5=$i[1,32]if(($+md5s[$md5])){files_to_delete+=($i[35,-1])}else{md5s[$md5]=1}}(($#files_to_delete))&&rm-v$files_to_delete为什么要使用shell脚本语言对于从未接触过shell脚本的开发者或者用户来说,还有一个比较重要的问题,为什么要学习和使用shell脚本?那么我们先从shell脚本的使用场景说起。Shell是与计算机系统交互的文本界面(CLI)。简单的说就是输入命令后返回结果(还有更复杂的操作)。在某些场景下,CLI比图形界面(GUI)更方便、更高效,而且是不可替代的(即使有一天语音识别取代了文本输入,CLI也会继续存在,而不换药)。那么要使用CLI,就必须约定好命令格式,shell脚本就是一种CLI交互的命令格式。由于这种相当特殊的情况,shell脚本具有一些不同于其他编程语言的特征。一个很重要的特点就是shell脚本应该比较简洁,容易输入。一个简单的命令,要是几十个字,根本就没有人能够接受。为了达到可接受的简洁程度,shell脚本的语法通常比其他编程语言的语法更怪异。有人可能会争辩说这混淆了两件事。在CLI中输入命令和编写脚本文件然后执行命令是两件不同的事情。您不需要使用相同的语言,而只是在CLI交互中。通常不需要写复杂的逻辑,也就是说基本不需要学习shell脚本。诚然,它们是两个不同的事物,但它们并非毫无关联。例如,有人这样想后,他决定只使用shell中最简单的命令,而不去学习更复杂的语法。如果他需要写脚本,他可以用Python之类的语言来写。所以有什么问题?Python专为通用场景而设计。虽然它也可以处理shell脚本所做的事情,但它往往需要多写几倍甚至几十倍的代码(如果你对Python不太了解的话)。在许多情况下,shell脚本做的是一次性工作。运行后直接删除,也可以直接在一行输入回车。在Python中编写这样的场景的成本要高得多。而且,能用Python实现shell脚本功能的也不是Python初学者。即使是熟练的Python开发人员也可能无法弄清楚如何实现一个可以用shell脚本轻松实现的功能。shell脚本的大部分工作是处理字符串和目录文件。特点是要实现的功能复杂多样,没有固定的模式。不管用什么语言,都不容易。Python内置的字符串、目录文件等库函数非常基础,基本上只能用单一函数实现操作,稍复杂的函数需要自己编写。如果你去寻找一些功能复杂的第三方库,会牵涉到很多问题,比如学习和部署成本,可能有因为使用人数少而没有发现的bug,可能没人维护他们。不管库怎么写,语法不能太简洁等等。但是,一个shell脚本的初步熟悉只需要几十分钟,用多了就会熟悉,成本收益不言而喻。格式约定文中行首的%表示zsh的命令提示符(类似于bash的$,这个可以自由定义,具体不重要),行首的>表示换行后输入内容,#开头为注释(非root用户的命令提示符,本系列文章不需要root用户),其余为命令输出。另外,有些地方会把zsh代码贴成一段,所以开头的%会省略,这样更容易区分。例子:#前两行是输入内容,第三行是输出内容%echo"Hello\>World"HelloWorld本系列文章使用的zsh版本是5.4.1(最新版本在写这篇文章的时候),旧版本的代码可能无法运行或者结果可能不同,请尝试使用最新版本。让我们直接进入主题。变量接触一种新的编程语言。运行完HelloWorld,首先需要了解的是如何定义和使用变量。有了变量之后,就可以比较变量的内容,然后就可以接触条件、循环、分支等语句,进而了解函数的用法,更高级的数据结构的使用,更多的库函数,等等。这样就可以大致了解一个面向过程的语言的基本用法,剩下的可以在使用的时候查一下手册。所以这篇文章讲的是最基本的变量和语句。zsh有5个变量:整数、浮点数(bash不支持)、字符串、数组、哈希表(或关联数组或字典,本系列文章统一使用术语“哈希表”),以及一些很少见的东西在其他语言中,比如alias(但主要用于交互,编程中基本不用)。本文只涉及整数、浮点数和字符串,不涉及数值计算和字符串处理。变量定义大多数情况下,Zsh变量不需要事先声明或指定,可以直接赋值使用(但哈希表除外)。#等号两头不能有空格%num1=123%num2=123.456%str1=abcde#如果字符串中包含空格等特殊字符,需要引号%str2='abcdef'#也可以使用双引号,但是和单引号是有区别的,比如变量可以用双引号,不能用单引号%str3="abcdef$num1"#字符串中可以使用转义字符,都是单引号和双引号%str4="abc\tdef\ng"#输出变量,也可以使用print%echo$str1abcde#简单的数值计算%num3=$(($num1+$num2))#((中的变量名可以不用$%num3=$((num1+num2))#简单的字符串操作%str=abcdef#2和4是字符在数组中的位置,从1开始数,两边不能有空格逗号%echo$str[2,4]bcd#-1是最后一个字符%echo$str[4,-1]defvariablecomparison#comparisonvalue%num=123#(())用于数值比较和其他操作,如果为真则返回0,否则返回1#&&下面的语句是在前面的语句为真时才执行#注意只能用双等号来比较%((num==123))&&echogoodgood#((里面可以使用and(&&)or(||)not(!)运算符,同c系列语言%((num==1||num==2))&&echogood#比较字符串%str=abc#用[[比较字符串,里面一定要有空格,[[具体用法后面会说#这里的双等号可以换成a单等号,大家可以根据自己的习惯选择#本系列文章统一使用双等号,因为与(())一致,使用双等号的通用编程语言比较多#双引号是$str两边都不需要,即使str未定义或者$str包含空格和特殊符号在未定义字符串和空字符串之间为真#[[也可以用在&&||中!%[[$str==""||$str==123]]&&回显良好Statement在稍微了解了简单变量的使用之后,快速进入语句部分。zsh支持多种风格的语法,包括经典的posixshell(bash的语法与其类似,但有一些扩展,可以归为一类),csh风格等。但是posix的语法shell不好用,我们不用这个。我只选择一个我认为最方便简洁的语法,没有fi,then,do,done,esac,in等关键字(虽然其中一些关键字在其他编程语言中也有,基本用法不同,容易混淆),不需要额外的分号。如果您不确定语法是否符合预期,您可以定义一个函数并使用它来查看,内容将转换为原始(posixshell样式)的外观。熟悉bash和喜欢使用bash语法的读者可以跳过这部分。句法上的差异不影响后续内容的阅读。继续用bash风格的语法写zsh是没有问题的。条件语句#格式if[[]]{}elif{}else{}大括号也可以换行,本系列文章统一采用这种风格,缩进到4个空格。注意elif不能写成elseif。[[]]用于比较字符串、判断文件等,功能比较复杂多样,这里先用最基本的用法。注意不要用[[]]来比较值,因为一不小心,值会被转成字符串进行比较,不会有任何错误提示,但结果可能不符合预期,造成不必要的麻烦。#示例if[["$str"=="name"||"$str"=="value"]]{echo"$str"}(())用于比较值,内部可以调用各种值相关的函数,格式类似于C语言,而$变量前可以省略。#格式if(()){}#示例if((num>3&&num+3<10)){echo$num}{}用于在当前shell中运行命令,判断运行结果。#Formatif{}{}#示例if{grepsd1/etc/fstab}{echogood}()用于在子shell中运行命令,判断运行结果。用法与{}类似,不再举例。#格式if(){}这几种括号可以一起使用,这样可以同时判断字符串、值、文件、命令结果等。&&||最好不要混用,可读性差,容易出错。#格式if[[]]&&(())&&{}{}循环语句#格式while[[]]{break/continue}和if一样,其中[[]]可以用其他种类的括号代替,function同理,顺序不再举例。break用于结束循环,continue用于直接进入下一个循环。所有循环语句都可以使用break和continue,下面不再赘述。#例子无限循环while((1)){echogood}until与while相反,不满足条件时运行,满足时停止。其他用法同while,不再举例。#格式until[[]]{}for循环主要用于枚举。这里的括号是for的特殊用法,不在子shell中执行。括号内是字符串(可以放多个,用空格隔开),数组(可以放多个)或者哈希表(可以放多个,哈希表是枚举值而不是键)。i是用来枚举内容的变量名,变量名是任意的。#formatfori(){}#examplefori(aabbcc){echo$i}#枚举当前目录下的txt文件fori(*.txt){echo$i}#枚举数组array=(aabbcc)fori($array){echo$i}经典的c风格for循环。#Formatfor((;;)){}#Examplefor((i=0;i<10;i++)){echo$i}这个例子只是一个例子,其实大部分情况不需要用这个有点forloop,它可以是这样的。#例如,{1..10}可以生成一个从1到10的数组fori({1..10}){echo$i}repeat语句用于循环固定次数,n为整数或内容是一个整型变量。#Formatrepeatn{}#Samplerepeat5{echogood}分支语句分支逻辑也可以用if实现,但是case更适合这种场景,功能更强大。#格式+示例$i{(a)echo1;;(b)echo2#继续执行next;&(c)echo3#继续往下匹配;|(c)回声33;;(d)回声4;;(*)回声其他;;};;表示结束case语句,;&表示继续执行下一条匹配语句(不再匹配),;|意思是继续匹配看是否满足条件分支。用户输入选择语句select语句用于根据用户的选择确定分支。语法类似于for语句的语法。如果没有break,用户会循环选择。#formatselecti(){}#sampleselecti(aabbcc){echo$i}输出是这样的。1)aa2)bb3)cc?#按上面的数字回车选择。异常处理语句#格式{statement1}always{statement2}如果语句1执行失败,则执行语句2。简化条件语句if语句的简化版本,只有一个分支时更简洁。格式:[[]]||{}[[]]&&{}最好不要混用&&||例如,连续。aa&&bb||cc&&dd容易导致逻辑错误或误解,可以用{}把语句括起来。aa&&{bb||{cc&&dd}}对于复杂的判断,最好用if来读写。&&||通常只适用于简单的场景。小结本文简要介绍了变量和语句的使用。变量部分只涉及到最基本和常用的部分,后续文章会详细介绍。语句部分已经涵盖了所有需要用到的语句。事实上,这些语句并不只有这种语法,但本系列文章统一使用这种语法。不过涉及到的几个括号的用法比较复杂,在后续文章中会详细介绍。本文不再更新,全系列文章更新维护在这里:github.com/goreliu/zshguideWindows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua等相关问题的付费解决方案,定价灵活,欢迎咨询,微信ly50247。
