Zsh 开发指南(第一篇 变量和语句)

导读

网上关于 zsh 的文章有不少,但其中超过 95% 的文章讲如何使用和配置,写如何用 zsh 编程的文章不多,能找到的多数也是只言片语,不成系统。国外有几本讲 zsh 的书,其中也有不少内容是配置、使用、编写补全脚本等等,对编程有用的篇幅占比并很少,并且比较零散不便于查询。至于官方文档?那是让即便有多年编程经验的开发者也会抓狂的神奇存在。可读性极差,并且基本没有例子,不熟悉文档结构和内容的话,很难找到本身想要的东西。但内容覆盖很全面,洋洋洒洒近 500 页,耐心去看总会找到的。还有一份官方“入门”文档,上次更新时间是 2002 年,也要 300 多页,至于可读性,比官网文档要稍微好一些吧,仍是有必定的参考价值的。官网上还有一些连接,里边内容比较零散,也能够看看。git

不少人在 zsh 中用 bash 语法写脚本,虽然也能够正常运行,但这样没法利用 zsh 的众多优秀特性,仍是很是遗憾的。熟悉下 zsh 下独有的特性,对写脚本的帮助是很大的。github

本系列文章无关 zsh 的安装、使用、配置(若是须要配置文件,能够参考个人 .zshrc,里边有比较详细的注释),更无 oh-my-zsh 相关内容,安装 zsh 后无需配置便可开始学习编写脚本。读者不须要有 bash 的基础(最好了解一些),但须要接触过任何一门编程语言,对编程的一些基础概念要有了解。shell

为何用 zsh 写脚本

不少人对 zsh 的了解停留在界面漂亮、主题多、插件多、补全强等等,而对 zsh 的语言特性了解并很少。由于 zsh 基本兼容 bash,很多人使用 bash 语法写 zsh 脚本,或者偶尔使用一些 zsh 特有的小技巧,很难体会出 zsh 做为一门编程语言的强大之处。编程

另外有些人认为 bash 几乎在全部类 Unix 系统都有默认安装,而 zsh 每每要本身安装,为了通用性而用 bash 写脚本比较好。这个说法也有必定的道理,但并非对全部开发者来讲都有影响。若是是开源软件的开发者,为了不洁癖用户由于不想安装他用不到的 zsh 而不使用本身的软件,而避免使用 zsh,是有必定道理的(但如今 zsh 的用户量也有必定的积累了)。除此以外,本身平时写脚本、公司内部使用等多数场景,都是不须要考虑这个因素的。数组

若是在公司使用,还涉及其余因素。bash

第一个是 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 脚本是一件多么特别的事情。蹩脚的语法(几乎全部从任何其余语言迁移过来的开发者,都要从新熟悉和习惯它的语法)、严重依赖外部命令(由于文件系统错误等问题,挂掉一个外部命令,脚本就休克了。命令版本不一样会有用法上的微秒差异,调试测试困难。频繁起新进程性能低下)、功能孱弱蹩脚(不少须要频繁使用的功能不全面或者很差用,好比字符串处理和数组的用法)等等,让不少开发者很是头疼,其中有些人甚至主张禁止使用 shell 脚本,一概改用 Python 等等,但 Python 并不是适用全部场景,并且也有另外的一些问题,这样作也是因噎废食。Zsh 并不是将这些问题所有解决了,但和 bash 相比,有很大的改善。好比 zsh 支持多种风格的语法,开发者很容易找到亲切感;对外部命令的依赖比 bash 要轻不少,多数经常使用的功能不须要使用外部命令,性能更好,调试也更加方便;功能上和 bash 相比也有比较大的提高,处理不那么复杂的场景已经比较够用了。编程语言

有人可能会说,不如“一步到位”,使用 Powershell。Powershell 的确比 Python 更适合做为一种 shell 脚本语言,但使用它的话会有其余问题。ide

首先 Powershell 的学习成本是绝对要比 zsh 高的,若是想省点事,这并非好的选择。

其次 Linux 下的 Powershell 目前仍是 beta 版,之后会不会有不少人用也很难说,若是不多有人用,那么生态环境就成问题。好比遇到问题后找不到解决办法,配套的软件和库不完善等等。

再次 Powershell 解释器的启动速度很是感人,在个人机器上,Windows 下的 Powershell 空脚本要执行将近 200 毫秒,Linux 下的要更长一些(我只在 WSL 里安装试用过,时间翻了几倍),而 zsh 的话,在 Linux 下不超过 5 毫秒,在 WSL 下也不超过 20 毫秒。若是写一个简单的脚本,运行时都要卡一下,是很是影响体验的。

最后若是平时就使用 Powershell 做为交互 shell,那么虽然脚本的启动时间问题有所缓解,但用户体验会差不少,并且之后也很难提高上来,很容易得不偿失。

Zsh 脚本样例

能够经过一个例子直观感觉下用 zsh 写的脚本。这是一个删除当前目录以及全部子目录下重复文件的脚本,经过 md5 判断文件是否相同(不严谨)。熟悉 bash 的读者能够尝试用 bash 完成相同的功能,而后对比一下代码(我以前写过一个 bash 版本的,不贴上来了),就能比较直观地感觉到 bash 和 zsh 的区别了。

#!/bin/zsh

local files=("${(f)$(md5sum **/*(.D))}")
local files_to_delete=()
local -A md5s

for i ($files) {
    local md5=$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 初学者就能用 Python 实现 shell 脚本的功能的,甚至熟练的 Python 开发者也极可能一时想很差怎么实现某个用 shell 脚本能很容易实现的功能。Shell 脚本的不少工做是和字符串和目录文件打交道,特色是要实现的功能复杂多样,没有固定模式,不管用什么语言写,都不容易。Python 自带的字符串和目录文件等类库功能很是基础,基本只能实现功能很单一的操做,稍微复杂点的功能都须要本身写。若是去找某些功能复杂的第三方库,那就会涉及一堆问题,好比一样有学习和部署成本,可能由于用户少因此有 bug 未被发现,可能已经没有人维护了,Python 的语法决定库怎么写都不能让语法太简洁等等。

而初步熟悉一门 shell 脚本只须要几十分钟,用多了天然就熟悉了,成本收益的权衡不言而喻。

格式约定

文中行首的 % 表明 zsh 的命令提示符(相似 bash 的 $,这个是能够自由定义的,具体是什么不重要),行首的 > 表明此行是换行后的输入内容,以 # 开头的为注释(非 root 用户的命令提示符,本系列文章不须要 root 用户),其他的是命令的输出内容。另外某些地方会贴成段的 zsh 代码,那样就省略开头的 %,比较容易分辨。

一个样例:

# 前两行是输入内容,第三行是输出内容
% echo "Hello \
> World"
Hello World

本系列文章使用的 zsh 版本是 5.4.1(写这篇文章时的最新版本),代码在老版本中可能运行不了或者结果有出入,尽可能使用最新版本。

下面直接进入正题。

变量

接触一门新的编程语言,运行完 Hello World 后,首先要了解的基本就是如何定义和使用变量了。有了变量后能够比较变量内容,进而能够接触条件、循环、分支等语句,继而了解函数的用法,更高级的数据结构的使用,更多库函数,等等。这样就大概了解了一门面向过程的语言的基本用法,剩下的能够等到用的时候再查手册。

因此这一篇讲最基本的变量和语句。

zsh 有 5 种变量:整数、浮点数(bash 不支持)、字符串、数组、哈希表(或者叫关联数组或者字典,本系列文章统一使用“哈希表”这一名词),另外还有一些其余语言少有的东西,好比 alias(但主要是交互时使用,编程时基本用不到)。此篇只涉及整数、浮点数、字符串,而且不涉及数值计算和字符串处理等内容。

变量定义

Zsh 的变量多数状况不须要提早声明或者指定类型,能够直接赋值和使用(但哈希表是一个例外)。

# 等号两端不能有空格
% num1=123
% num2=123.456
% str1=abcde
# 若是字符串中包含空格等特殊字符,须要加引号
% str2='abc def'
# 也能够用双引号,但和单引号有区别,好比双引号里可使用变量,而单引号不能够
% str3="abc def $num1"
# 在字符串中可使用转义字符,单双引号都可
% str4="abc\tdef\ng"

# 输出变量,也可使用 print
% echo $str1
abcde

# 简单的数值计算
% num3=$(($num1 + $num2))
# (( 中的变量名能够不用 $
% num3=$((num1 + num2))

# 简单的字符串操做
% str=abcdef
# 2 和 4 都是字符在数组的位置,从 1 开始数,逗号两边不能有空格
% echo $str[2,4]
bcd
# -1 是最后一个字符
% echo $str[4,-1]
def

变量比较

# 比较数值
% num=123
# (( )) 用于数值比较等操做,若是为真返回 0,不然返回 1
# && 后边的语句在前边的语句为真时才执行
# 注意这里只能使用双等号来比较
% ((num == 123)) && echo good
good
# (( 里边可使用与(&&)或(||)非(!)操做符,同 c 系列语言
% ((num == 1 || num == 2)) && echo good

# 比较字符串
% str=abc
# 比较字符串要用 [[,内侧要有空格,[[ 的具体用法以后会讲到
# 这里双等号能够替换成单等号,能够根据本身的习惯选用
# 本系列文章统一使用双等号,由于和 (( )) 一致,而且使用双等号的经常使用编程语言更多些
# $str 两侧不须要加双引号,即便 str 未定义或者 $str 中含空格和特殊符号
% [[ $str == abc ]] && echo good
good
# 能够和空字符串 "" 比较,未定义的字符串和空字符串比较结果为真
# [[ 里也能够用 && || !
% [[ $str == "" || $str == 123 ]] && echo good

语句

稍微了解下简单变量的使用后,快速进入语句部分。

zsh 支持多种风格的语法,包括经典的 posix shell (bash 的语法和它相似,但有一些扩展,能够归为一类)的,以及 csh 风格的等等。但 posix shell 的语法并很差用,咱们不必必定使用这个。我只选用一种我认为最方便简洁的语法,没有 fithendodoneesacin 等的关键字(虽然其中某些关键字其余编程语言也有,但基本用法都各异,并且容易混淆),也不须要多余的分号。若是不肯定语法是否符合预期,能够定义一个函数而后使用 which 查看,内容会被转化成原始(posix shell 风格)的样子。熟悉 bash 而且喜欢使用 bash 语法的读者能够跳过这部份内容,语法的不一样并不影响后续内容的阅读,继续使用 bash 风格语法写 zsh 也是没有问题的。

条件语句

# 格式
if [[ ]] {
} elif {
} else {
}

大括号也能够另起一行,本系列文章统一使用这种风格,缩进为 4 个空格。注意 elif 不可写做 else if

[[ ]] 用于比较字符串、判断文件等,功能比较复杂多样,这里先使用最基础的用法。注意尽可能不要用 [[ ]] 比较数值,由于不留神的话,数值会被转化成字符串来比较,没有任何错误提示,但结果可能不符合预期,致使没必要要的麻烦。

# 样例
if [[ "$str" == "name" || "$str" == "value" ]] {
    echo "$str"
}

(( )) 用于比较数值,里边能够调用各类数值相关的函数,格式相似 c 语言,变量前的 $ 可省略。

# 格式
if (( )) {
}
# 样例
if ((num > 3 && num + 3 < 10)) {
    echo $num
}

{ } 用于在当前 shell 运行命令而且判断运行结果。

# 格式
if { } {
}
# 样例
if {grep sd1 /etc/fstab} {
    echo good
}

( ) 用于在子 shell 运行命令而且判断运行结果,用法和 {} 相似,再也不举例。

# 格式
if ( ) {
}

这几种括号能够一块儿使用,这样能够同时判断字符串、数值、文件、命令结果等等。最好不要混合使用 && ||,会致使可读性变差和容易出错。

# 格式
if [[ ]] && (( )) && { } {
}

循环语句

# 格式
while [[ ]] {
    break/continue
}

if 同样,这里的 [[ ]] 能够替换成其余几种括号,功能也是同样的,再也不依次举例。break 用于结束循环,continue 用于直接进入下一次循环。全部的循环语句中均可以使用 breakcontinue,下边再也不赘述。

# 样例 死循环
 while (( 1 )) {
    echo good
}

untilwhile 相反,不知足条件时运行,一旦知足则中止,其余的用法和 while 相同,再也不举例。

# 格式
until [[ ]] {
}

for 循环主要用于枚举,这里的括号是 for 的特有用法,不是在子 shell 执行。括号内是字符串(可放多个,空格隔开)、数组(可放多个)或者哈希表(可放多个,哈希表是枚举值而不是键)。i 是用于枚举内容的变量名,变量名随意。

# 格式
for i ( ) {
}
# 样例
for i (aa bb cc) {
    echo $i
}

# 枚举当前目录的 txt 文件
for i (*.txt) { 
    echo $i
}

# 枚举数组
array=(aa bb cc)
for i ($array) {
    echo $i
}

经典的 c 风格 for 循环。

# 格式
for (( ; ; )) {
}
# 样例
for ((i=0; i < 10; i++)) {
    echo $i
}

这个样例只是举例,实际上多数状况不须要使用这种 for 循环,能够这样。

# 样例,{1..10} 能够生成一个 1 到 10 的数组
for i ({1..10}) {
    echo $i
}

repeat 语句用于循环固定次数,n 是一个整数或者内容为整数的变量。

# 格式
repeat n {
}
# 样例
repeat 5 {
    echo good
}

分支语句

分支逻辑用 if 也能够实现,但 case 更适合这种场景,而且功能更强大。

# 格式 + 样例
case $i {
    (a)
    echo 1
    ;;

    (b)
    echo 2
    # 继续执行下一个
    ;&

    (c)
    echo 3
    # 继续向下匹配
    ;|

    (c)
    echo 33
    ;;

    (d)
    echo 4
    ;;

    (*)
    echo other
    ;;
}

;; 表明结束 case 语句,;& 表明继续执行紧接着的下一个匹配的语句(再也不进行匹配),;| 表明继续往下匹配看是否有知足条件的分支。

用户输入选择语句

select 语句是用于根据用户的选择决定分支的语句,语法和 for 语句差很少,若是不 break,会循环让用户选择。

# 格式
select i ( ) {
}
# 样例
select i (aa bb cc) {
    echo $i
}

输出是这样的。

1) aa  2) bb  3) cc
?#

按上边的数字加回车来选择。

异常处理语句

# 格式
{
    语句 1
} always {
    语句 2
}

若是语句 1 执行出错,则执行语句 2。

简化的条件语句

if 语句的简化版,在只有一个分支的状况下更简洁,功能和 if 语句相似,不赘述。

格式:
[[ ]] || {
}

[[ ]] && {
}

最好不要连续混合使用 && ||,好比。

aa && bb || cc && dd

容易致使逻辑错误或者误解,能够用 { } 把语句包含起来。

aa && { bb || { cc && dd } }

比较复杂的判断仍是用 if 可读写更好,&& || 一般只适用于简单的场景。

总结

本篇简单介绍了变量和语句的使用方法。变量部分只涉及了最基础经常使用的部分,后续文章会详细介绍。语句部分已经覆盖了全部须要使用的语句,实际上这些语句都不仅有这一种语法,但本系列文章统一使用这个语法。但涉及到的几种括号的用法比较复杂,以后的文章也会详细介绍。

本文再也不更新,全系列文章在此更新维护:github.com/goreliu/zshguide

付费解决 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等领域相关问题,灵活订价,欢迎咨询,微信 ly50247。

相关文章
相关标签/搜索