ZSH-脚本编程学习指南-全-
ZSH 脚本编程学习指南(全)
原文:
annas-archive.org/md5/7a1ce16504dba808ae1d760a25ed80ef译者:飞龙
前言
如果我不得不猜测,我会说你之所以读到这些内容,是因为你像我一样,花了不少时间在处理 Unix 系统上。不管是因为你的工作需要,还是因为你喜欢探索操作系统的内部,shell 无疑是你处理大多数活动的方式。
历史上,shell 的设计初衷是加速我们的工作,但我们都知道,在某个阶段,原本应该是更精简的工作方式,却变成了充满神秘符号和令人难以记住的冗长代码的拉锯战。
那么,如果我们能从系统中挤出更多的功能,不是会很棒吗?想象一下,你目前正在做的事情,能够以一种更高效、更优雅的方式来完成,甚至是那些你认为只有经历了数百年经验的 Linux 高手才能做到的“魔法”。
如果我告诉你,像了解某个程序可以使用哪些选项标志这样的功能,现在不再需要你扫描无尽的 man 页面,你会怎么想?想象一下,不再需要面对无尽横向字符行的困扰。而且,如果你只需要看一下命令提示符,就能知道你当前所在的目录,那该有多好?现在,想象一下,所有这一切只需要你切换到一个新的 shell 就能开始。
本书内容概览
第一章,入门,从头开始,解释如何安装和设置 zsh。了解启动文件和定制 shell 提示符。
第二章,别名与历史,解释了别名是如何工作的,如何在启动文件中定义别名,并教你如何处理 shell 的历史日志。
第三章,高级编辑,介绍了 zsh 的行编辑器以及如何使用命令行上的各种快捷键和键绑定。
第四章,通配符,介绍了通过应用参数替换和修饰符来处理各种任务,从而以新的方式操作系统的文件和目录。
第五章,补全,向你介绍了 zsh 的一大特色,并展示了如何通过定义自己的样式和函数来调整“全新”的补全系统。
第六章,技巧与窍门,解释了各种杂项设置和配置选项,绝对值得尝试,还介绍了一些值得关注的社区项目。
本书所需内容
在开始之前,你应该熟悉如何使用终端仿真器。大多数操作系统都会将这类软件打包在其标准应用程序集中,但像任何其他应用程序一样,市场上还有很多替代软件等待你去发现。这些替代软件可能更适合手头的任务,所以在开始这本书之前,请确保了解你选择工具的优缺点及其怪癖。
要跟随本书中的内容并查看提供的示例,还需要使用 Git 源代码管理系统。你可以通过访问git-scm.com并按照提供的说明轻松获取并安装它,它是使用书中提到的各种软件项目和源代码时不可或缺的工具。
本书适合谁阅读
本书非常适合系统管理员、开发人员以及其他从事 Unix 工作的计算机专业人员,他们希望改进与 Unix shell 相关的日常任务。假设你对 Unix 命令行界面有一定了解,并且能熟练使用像 Emacs 或 vi 这样的编辑器。使用 Web 浏览器来阅读一些在线文档是可选的。
约定
在本书中,你会看到多种文本样式,用来区分不同种类的信息。以下是这些样式的示例,并解释它们的含义。
文本中的代码词汇如下所示:“此别名通过每次输入时加上color标志来改变ls的行为,而不是使用其更原始的版本。”
一段代码块的设置如下:
zstyle ':completion:*:descriptions' format '%B%d%b'
zstyle ':completion:*:messages' format %d
zstyle ':completion:*:warnings' format 'No matches for: %d'
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会以粗体显示:
autoload -Uz compinit
compinit
任何命令行输入或输出都按如下方式书写:
$ zsh --version
zsh 5.0.2 (x86_64-apple-darwin12.3.0)
新术语和重要词汇以粗体显示。你在屏幕上看到的词汇,例如在菜单或对话框中的词汇,会以这样的形式出现在文本中:“如果你的操作系统向你显示一个礼貌的zsh not found消息,那没关系;否则,你不会读到这些内容。”
注意
警告或重要提示会显示在像这样的框中。
提示
提示和技巧会以这样的形式出现。
读者反馈
我们始终欢迎读者的反馈。让我们知道你对本书的看法——你喜欢什么,或者不喜欢什么。读者反馈对我们开发能让你充分受益的书籍至关重要。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并通过邮件主题提到书名。
如果你在某个领域有专业知识,并且有兴趣撰写或参与编写书籍,请查看我们的作者指南,网址是www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲拥有者,我们有许多方法帮助您从购买中获得最大收益。
下载示例代码
您可以从您的账户中下载所有已购买 Packt 书籍的示例代码文件,网址为 www.packtpub.com。如果您是在其他地方购买的此书,可以访问 www.packtpub.com/support,注册后我们会直接将文件通过电子邮件发送给您。
勘误
尽管我们已尽最大努力确保内容的准确性,但难免会出现错误。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们将非常感激您向我们报告。通过这样做,您可以帮助其他读者避免困惑,并帮助我们改进该书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/support 报告,选择您的书籍,点击 勘误提交表单 链接,输入错误的详细信息。一旦您的勘误经过验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该书名下现有勘误的列表中。
盗版
网络上对版权材料的盗版问题是一个跨所有媒体的持续性问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上发现我们的作品的任何非法复制品(无论以何种形式),请立即向我们提供位置地址或网站名称,以便我们采取相应的措施。
请通过 <copyright@packtpub.com> 与我们联系,并附上涉嫌盗版材料的链接。
我们感谢您帮助保护我们的作者,并支持我们为您提供有价值的内容。
问题
如果您在使用本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 与我们联系,我们会尽力解决问题。
第一章. 入门
那么,Z shell 到底有什么特别之处呢?你大概已经对现代 shell 有了一个清晰的概念,因此像命令历史、补全和自动修正这样的功能可能不会像那些刚刚发现 Bash 的人那样让你惊讶。然而,与其他可用的 shell 相比,Z shell(zsh)拥有一个非常强大的脚本语言和令人惊叹的补全系统。其实,用“惊叹”都无法形容它的强大,“快速而轻松”可能更为贴切。zsh 还融合了 Bash、ksh 和 csh 中的许多有用功能,甚至可以在脚本中模拟这些 shell,以增加额外的兼容性。
一旦你发现了多行编辑或开始依赖自动拼写修正,我保证你会回头看看自己以前用键盘乱按按钮的日子,想知道为什么没有早点切换到 zsh。所以,让我们开始吧,好吗?
本章将从了解 zsh 开始,我们会快速浏览一些使其与众不同的功能。不过,在我们开始冒险之前,我们需要安装并配置新的 shell,确保一切顺利运行。接下来,我们将进入配置部分——启动文件是什么,如何使用不同的样式、转义序列和条件表达式来自定义提示符。
安装 zsh
和你系统中的大多数东西一样,zsh 也需要安装和维护;因此,在本节中,我们将学习如何进行安装。不过需要注意,为了避免引入不一致性或不兼容问题,安装 zsh 的推荐方式是直接从你的软件包维护者提供的源进行安装。可以参考系统文档,或者访问 zsh 的主页(zsh.sourceforge.net)了解整个安装过程。
在开始之前,最好检查一下是否需要安装或更新当前的 zsh 安装,因为某些 Unix 系统可能已经安装了该软件包。打开你喜欢的终端模拟器,并输入以下命令:
$ echo $SHELL
这应该会在大多数系统上输出类似/bin/sh或/bin/bash的内容,这意味着你当前的登录 shell 不是 zsh。如果结果中显示的是zsh,那么可以继续执行以下命令:
$ zsh --version
zsh 5.0.2 (x86_64-apple-darwin12.3.0)
如果运气好(当然,你的系统更新工作也得做好),你应该会看到 zsh 的版本信息,类似于之前的代码片段。如果是这种情况,你可以跳过本节内容。如果你的操作系统提示你未找到 zsh,也没关系,这样的话你才会看到这些内容。让我们进入安装部分吧,好吗?
注意
本书中将以最新的稳定版——截至本书写作时为版本 5.0.2——作为参考。因此,如果你运行的是早期版本,建议尝试更新你的现有安装。请参阅你的包管理器文档以更新 zsh。
在 Linux 上安装
根据你当前使用的 Linux 发行版,zsh 可能(或可能不会)在其软件仓库中,或者更好的是,已经安装在你的操作系统中。在极少数情况下,如果 zsh 不可用,你应始终查阅操作系统的包列表。
在 Debian 及其众多衍生发行版(如 Ubuntu 和 Linux Mint)上,你可以通过打开终端并运行以下命令来完成整个安装过程:
$ sudo apt-get update
$ sudo apt-get install zsh
根据你的 Debian 版本及其软件仓库,你可以获得从 4.3.x 到 5.0.0 及更高版本的 zsh(至少在使用当前发行版时是如此)。再次强调,尽可能使用最新版本。
提示
你可以通过在终端中运行 zsh --version 来检查 zsh 的版本。
基于 Red Hat 的发行版,如 Fedora,你需要输入以下命令:
$ sudo yum check-update
$ sudo yum install zsh
然后是 openSuSE 用户:
$ sudo zypper refresh
$ sudo zypper install zsh
让我们也不要忘记 Arch 用户:
$ sudo pacman -S zsh
等待下载和安装脚本/触发器完成后,可以跳到下一节。
在 OS X 上安装
可以说,在 OS X 上获取 zsh 的最简单方法是通过 Homebrew (www.brew.sh) 或 MacPorts (www.macports.org),这两款包管理器旨在扩展 OS X 用户可用的默认选项。不幸的是,这两个选项都没有捆绑在 OS X 中。你需要先安装其中一个解决方案,然后才能使用最新版本的 zsh(截至本书写作时仍为 5.0.2)。所以,打开你喜欢的终端模拟器,输入以下命令:
$ brew install zsh
或
$ sudo port install zsh
等待下载和安装脚本完成后,直接跳到下一节。同时,请参阅每个应用程序的文档,以排查安装过程中可能出现的任何问题。
从源代码编译
zsh 的官方网站位于zsh.sourceforge.net,你应该在浏览器中访问该网址,开始你的构建之旅。不过请记住,获取系统 zsh 二进制文件的推荐方法是通过已编译的二进制包。如果因为某些原因你想要获得最新的版本,并且不介意处理比稳定版本更多的 bug,你很可能需要使用 Git 版本控制软件克隆仓库:
$ git clone git://git.code.sf.net/p/zsh/code zsh
确保你检出了并跟踪了 master 分支,因为最新的更改都已经提交到该分支。还要记住,在你构建本地的 zsh 副本之前,有一些依赖项需要满足。这些依赖项都在已克隆到你磁盘上的多个配置文件中有详细文档说明,因此在尝试构建配置脚本等操作之前,最好仔细查看 README 文件。
注意
在你的平台上安装 Git 超出了本书的范围,但请放心,你按照www.git-scm.com上的说明操作时不会遇到任何问题。
首次运行
现在 zsh 已经安装在你的系统上了,我们来试试它怎么样?打开你喜欢的终端模拟器,输入以下命令:
$ zsh
和许多其他应用程序一样,zsh 也有一个首次运行向导(请耐心点,它几乎像是一个向导)。它是一种神奇的工具,唯一的目的就是帮助我们通过一连串的问题和决策快速配置我们的工具。我们这次跳过新用户配置,但你可以根据自己的需要选择适合你的方法,要么一项一项地回答问题,要么直接按 Q 来中止操作。只需记住,newuser 模块在 <zshInstallFolder>/Functions/Newuser/zsh-newuser-install 或 <zshInstallFolder>/functions/zsh-newuser-install 中可以找到——如果将来需要它的帮助,随时可以使用。
为了避免每次运行时都跳过配置选项,你可以创建一个所谓的 启动文件:
% touch ~/.zshrc
我们刚刚创建了主偏好文件;问题是,它目前是空的。我们接下来加一些偏好设置,好吗?
注意
在文中会多次提到 zsh 的选项——这些设置会改变 shell 的行为——因此,现在是时候为它们制定一些约定了。首先,命名规则有些过于宽松——它是区分大小写的,并且忽略下划线。所以,以下两个选项名称是等同的。
SOME_OPTION 和 SOMEOPTION
其次,试着将选项看作是 开关。顾名思义,它们可以被 开启 或 关闭。在 zsh 提供的众多切换选项的方式中,最容易记住的可能就是 setopt/unsetopt 组合。
setopt SOME_OPTION # enables any option.
unsetopt SOME_OPTION # use this to disable an option.
相反,你可以通过在选项名前加上 NO 来否定该选项的行为,从而使 unsetopt SOME_OPTION 与 setopt NO_SOME_OPTION 或者考虑到下划线仅仅是为了便于人类阅读,等同于 setopt NOSOMEOPTION。
为了避免混乱,并且因为我喜欢标准化,我们将在本书中使用 ALL_CAPS_SNAKE_CASE 来表示选项。
用你喜欢的编辑器打开 ~/.zshrc;你可以使用 vim、Emacs、nano 或者现在孩子们喜欢的任何编辑器,添加以下一行:
autoload -U promptinit # initialize the prompt system promptinit
让我们回顾一下刚才输入的内容:代码的第一行是我们告诉 shell 启动其 promptinit 模块的方式——这是一系列处理 shell 各种提示符和功能的函数。井号后面的内容仅是一个注释,用来提醒你该命令的作用和它为什么在这里。最后一行才是实际调用和初始化提示符模块的代码。看起来可能不算什么,但当你处理提示符时,它会派上用场,我保证。
随时可以省略注释,并确保保存更改。
注意
Zsh 会忽略每一行以井号(#)——或称为磅符——开头的内容。这对于调试首选项非常有用,更好的是,它可以帮助你记录功能。请看下一个例子,其中的注释以粗体显示:
# This is a comment and will be ignored by the shell.
HISTFILE=~/.zsh_history # sets the location of the history file
将 zsh 设置为登录 shell
如果有一件事 shell 会认真对待,那就是它们的角色。你看,shell 的问题是它们喜欢被分为非常特定的类别——它们要么是交互式的,要么是非交互式的,然后还有登录 shell。
正如你从它们的名字中可能猜到的,交互式 shell 允许你与它们进行交互;也就是说,它们会显示一个提示符,你输入一个命令,然后它们会返回一个答案,并准备好接受新的输入。另一方面,应用交互式 shell 会在执行脚本时被调用,并在完成任务后自行退出。
注意
简单来说,提示符通常是闪烁的光标,告诉你 shell 已准备好接受输入。
那么,登录 shell 怎么样呢?好吧,不像交互式 shell,登录 shell 通常在用户登录时被调用——无论是在本地计算机上,还是使用像 SSH 这样的工具——并且会仔细读取你的启动文件和配置项。更重要的是,登录 shell 不一定需要是交互式的。
在上一节中,我们使用了直接调用二进制文件 zsh 来启动 zsh。正如你可以想象的那样,这只是一个临时的解决方法,因为每次我们想使用它时都输入 shell 的名字,显得有点不切实际。更糟糕的是,你会想到前一个 shell 可能还在后台等待,一旦你结束使用 zsh,它就会准备好重新跳出来。如果你不相信我,可以输入 exit;我等着你。看到屏幕上显示的那个吗?那就是你以前的命令行伴侣。和它说再见后,输入 zsh 并按 回车 重新进入 zsh。
所以接下来要做的就是——你猜对了——摆脱你那个旧的 shell,省去每次想用 zsh 时都要记得调用它的麻烦。
注意
你可以通过在启动 zsh 时使用 -l 或 --login 标志,欺骗 zsh 和许多其他 shell,使它认为自己是一个登录 shell。打开终端并输入以下命令之一:
$ zsh -l
或
$ zsh --login
Voilà!一个带有登录功能的 shell。
幸运的是,Unix 的 chsh 命令似乎正是医生推荐的那个,所以请在终端中键入以下命令:
$ chsh -s $(which zsh)
在上一段代码中,我们告诉系统为当前用户更改 shell。这里使用的 -s 选项是用来指定 shell 二进制文件的位置的。你看到的那个复杂的 $() 构造是我们告诉 shell 展开括号内命令的结果,这个命令就是 which zsh。
你可能还记得上一节中提到的 which,我们用它来找出现有 zsh 安装的位置。which 的作用是大声宣布用户 $PATH 环境变量中任何程序文件的位置。因此,我们可以安全地假设,如果 zsh 不在其中,说明某个地方出了问题,或许我们应该回头再检查一下。
很可能,更改登录 shell 会要求以提升的权限运行该命令,因此确保你使用的是具有适当权限的账户。
从现在开始,每次启动你选择的终端模拟器时,系统都会默认启动 zsh。很可能,你已经安装并将 zsh 设置为登录 shell。接下来就是对其进行调整。
Shell 选项
除了通过 -l 标志欺骗 zsh 使其认为它是一个登录 shell 外,还有许多其他有用的选项可以在启动时设置。例如,zsh -v 将开启详细模式,这会让 shell 在执行任何命令前先打印出该命令的内容。然后,还有 zsh -x —— 即 xtrace —— 在调试脚本时非常有用,或者 zsh -f 会使用默认设置启动一个干净的 zsh 实例。
这些选项中的任何一个也可以在 shell 启动后设置;你只需要通过 set 命令调用所需的选项标志。下面的示例在一个正在运行的会话中启用详细模式:
% set -v
% echo 'quite the echo in here'
> echo 'quite the echo in here'
> 'quite the echo in here'
提示
下载示例代码
你可以从 www.packtpub.com 下载所有你购买的 Packt 书籍的示例代码文件。如果你是在其他地方购买的这本书,可以访问 www.packtpub.com/support 并注册,以便直接通过电子邮件接收文件。
此外,你还可以使用相同的 set 命令禁用任何选项,只需将减号/负号替换为加号,如下所示:
# disables verbose mode
% set +v
有关各种 shell 选项及其使用方式的更多信息,请参阅 zshoptions(1) 手册页(man zshoptions)。
启动文件
和大多数登录 shell 一样,zsh 依赖一系列配置文件,称为 启动 文件,这些文件包含了在 shell 启动过程中需要执行和设置的命令和偏好设置。我们在前面章节使用了 .zshrc 文件,以避免被 newuser 函数打扰,但既然现在我们已经把 zsh 设置为登录 shell,是时候仔细看看我们可以用这些文件做些什么了。
注意
默认情况下,zsh 会在用户的主目录($HOME,或其别名,更常见的是波浪号~,在本文本中我们交替使用它们作为当前用户主文件夹的路径)下查找启动文件。你可以通过在$HOME下的.zshenv文件中设置参数ZDOTDIR来告诉 zsh 去另一个目录查找配置文件:
ZDOTDIR=/etc/my_kewl_folder/.zshrc
在启动过程中,zsh 会在/etc/下查找,或称为源文件,一组特定的系统和用户文件名。紧接着,每个文件都有一个用户可编辑的副本,通常位于$HOME下,会被读取。然而,有一些规则可能会让 zsh 完全跳过这些文件。文件的顺序非常重要,因为在错误的文件中设置选项可能导致命令在错误的时间执行,产生一些奇怪的行为。因此,在设置文件偏好时,请记住以下顺序:
-
zshenv -
zprofile -
zshrc -
zlogin
如果 zsh 没有作为交互式 shell 调用,zprofile和zshrc以及它们在$HOME中的对应文件(~/.zprofile和~/.zshrc)将不会被源文件。除此之外,如果 zsh 没有作为登录 shell 调用,zlogin和$HOME/.zlogin也会被跳过。
注意
根据你安装 zsh 的方式,在查找全局文件时,除了/etc/目录外,可能还会使用其他目录。
通常,你只希望修改自己用户的偏好设置,所以我们将重点关注位于$HOME下的启动文件,具体如下:
-
~/.zshenv:这将在/etc/zshenv之后立即被调用。你应该仅添加诸如PATH设置和任何你希望对任何类型的 shell(无论是交互式的还是非交互式的)都可用的内容。 -
~/.zprofile:这是/etc/zprofile的配套文件,是启动文件组中比较“无聊”的一个。你应该在这里放置任何希望在~/.zshrc之前执行的脚本。 -
~/.zshrc:这是你的“主力军”。大部分的用户设置和 shell 偏好会放在这里。请记住,它只会在交互式 shell 中生效。正如我们稍后所看到的,你可以通过调用多个文件来简化并扩展它的作用。 -
~/.zlogin:这将在~/.zshrc之后执行,基本上与~/.zprofile类似,所以你应该把希望在主启动文件后调用的脚本放在这里。
在启动文件的对立面,还有关闭文件。正如你所想的,这组相对较小的文件不仅按照特定的顺序调用,而且会在登录 shell 的注销序列中调用。关闭文件可以视为启动文件的子集,所以不必为此失眠。需要记住的重要一点是,当你在命令行输入logout时,用户可配置的~/.zlogout文件中的设置会被读取,然后是安装文件/etc/zlogout。
你可以使用 RCS 和 GLOBAL_RCS 选项来禁用启动文件的加载机制。这个偏好必须在系统文件 /etc/zshenv 中取消设置,如下所示:
unset RCS # disables loading of files other than zshenv
unset GLOBAL_RCS # disables loading of files under /etc/
例如,如果 zshenv 中取消设置了 RCS 选项(这是第一个被读取的文件),~/.zshenv 和所有其他文件将被跳过。但请记住,这两个选项可以被你加载的任何后续文件重新启用。
例如,如果你在 /etc/shenv 中有以下内容:
unset RCS
source my_options_file.zsh
然后,在 my_options_file.zsh 中添加:
# some more options here
set RCS
然后,shell 会继续加载 .zshenv 文件,仿佛什么都没发生过。所以,务必小心!
我们已经查看了启动文件及其严格的加载顺序;现在,到了深入了解提示符的时刻。
shell 提示符
给任何人足够的时间使用 shell,"如何给它添加颜色?"这个问题迟早会出现。幸运的是,zsh 提供了大量的配置选项和转义序列,让你能够实现这一目标,甚至更多。在本节中,我们将深入探讨你可以用来定制提示符的选项。
提示符命令
Zsh 提供了丰富的预定义提示符配置,可以作为构建更符合你需求的主题的基础。除了其他内容,prompt 工具允许你选择你喜欢的主题。在默认安装中,各种主题和用户贡献位于 <zshFolder>/Functions/Prompts(或在 OS X 中的 <zshFolder>/functions)目录下,并遵循命名规则 prompt_<theme>_setup。要查看默认包中包含的内容,只需输入以下命令:
$ prompt –p
然后,你将看到一个包含所有 zsh 提供的可用提示符主题的列表。你可以使用 -p 选项和主题名称一起,仔细查看任何主题:
$ prompt -p
为了使用 prompt 函数,你需要在 shell 中设置 promptinit 模块。最简单的方式是将其添加到 .zshrc 文件中。如果你还没有设置,请查看 第一次运行 部分。
注意
你可以参考 zshcontrib(1) 手册页中的 PROMPT THEMES 部分,获取关于 zsh 提示符的更多详细信息。在终端中输入 man zshcontrib 来开始。
你可以试用任何你喜欢的主题,通过输入以下命令将其暂时应用到当前的 shell 中:
$ prompt <theme_name>
一些主题,如 adam1,甚至可以接受一些额外的配置参数,如下所示:
$ prompt adam1 red yellow magenta # sets the 'adam1' theme
默认情况下,zsh 不太喜欢在命令行中输入注释。幸运的是,你可以通过在 .zshrc 文件中设置以下选项来改变这一行为:
setopt INTERACTIVE_COMMENTS # allow inline comments like this one
在前面的代码段中,我们向主题传递了一系列选项,特别是颜色 red、yellow 和 magenta。你可以通过调用任何给定主题的内置帮助,获得对每个提示符主题的更详细描述:
$ prompt -h <theme_name>
尝试在你喜欢的主题上使用这些设置,看看能调整出什么其他效果。
一旦你找到一个适合你的组合,你就可以保存这些更改。只需用编辑器打开 .zshrc 文件,并添加以下行:
autoload -U promptinit
promptinit
prompt adam1 red yellow magenta
我们拿到了之前的偏好设置文件,并在默认提示符adam1中加入了一些颜色。那么,如何调整它,让它更像家一样呢?
注意
如果你在之前的 shell 中已经投入了大量时间来定制你的提示符,那么在将其迁移到 zsh 时,弄清楚不同的规则集可能会让你感到头痛。幸运的是,zsh 提供了一系列工具,使得这个转换过程更为顺利。你可以在 <zshFolder>/Misc 文件夹下找到 bash2zshprompt 或 c2z 脚本,分别用于迁移 Bash 或 csh 的偏好设置。不过需要注意的是,一些发行版可能没有包含这些脚本,在这种情况下,你可以直接前往官方仓库,获取本地副本。关于如何获取 zsh 源代码,请查看从源代码编译部分的详细信息。
自定义提示符
Zsh 提供了五种不同的提示符,你可以根据需要进行调整,每种都有其特定的用途。虽然在大多数使用场景下你可能不需要担心这些提示符,但了解它们的作用仍然是很重要的。关于每个提示符的详细描述,我建议你查看 man zshmisc。
Zsh 喜欢将其主要提示符变量称为 $PS1 或其别名 $PROMPT(也可以是 $prompt)。不过可以放心,这三者实际上是一样的,zsh 对它们的处理是相同的。然后还有 $RPS1,它会在屏幕右侧打印提示符。与其他提示符不同的是,它会在需要行宽时自动消失。
$PS2 会在 shell 等待更多输入时显示,例如在某些未完成的语法结构开始时,或者在命令行中添加内联注释时。$PS3 用于在 select 循环控制机制中进行选择。最后但同样重要的是,$PS4 在调试脚本时非常有用。
总的来说,这些就是我们将要使用的工具集,通过一组巧妙的工具——转义序列,扩展其功能,超越基本设置。
提示
你可以随时使用 source 命令重新加载你的 zsh 配置文件。只需保存更改并运行以下命令:
$ source file_path/file_name
如果你的文件路径中包含空格,记得使用双引号。
$ source "random folder/.zshenv"
使用转义序列
转义序列是一组预定义的信息快捷方式,可以添加到 zsh 的提示符设置中。它们可以显示信息,例如你登录的机器名称、系统的当前日期和时间,甚至是当前工作目录。大多数转义序列是通过模组或百分号(%)操作符来定义的,有些甚至接受可选参数,进一步扩展其功能。
要使魔法生效,我们首先需要在配置文件中添加一个新设置。打开.zshrc并添加以下行:
setopt PROMPT_SUBST
通过这样做,我们启用了PROMPT_SUBST选项。这样,zsh 会将$PROMPT视为普通的 shell 变量,并在命令替换、参数和算术扩展时进行检查。
接下来,我们将介绍许多可用的转义序列及其含义。请记住,这绝不是所有可用选项的完整列表;因此,如果您需要更全面的可用选项列表,随时可以查阅zshmisc(1)手册页,特别是标题为Prompt Expansion的部分。
Shell 状态选项
以下选项作为当前 shell 状态的一些指示符:
-
%#:如果 shell 正在以提升权限运行,则显示#,否则显示%。 -
%?:显示最后执行的命令的退出状态代码。 -
%h或%!:显示当前历史事件编号。 -
%L:显示当前$SHLVL变量的值。 -
%j:显示当前正在执行的作业数量。
登录信息选项
以下选项显示有关当前 shell 正在运行的主机和机器的更多有用信息:
-
%M:显示机器的主机名。 -
%m:与前者相同。主机名会显示到第一个点(.)分隔符为止。它接受一个可选的整数,表示要显示的组件数量。 -
%n:与打印环境变量$USERNAME的效果相同。
目录选项
以下选项提供关于当前工作目录($PWD)和文件系统目录的信息:
-
%d或%/:显示当前目录。与打印$PWD环境变量一样。 -
%~:与前者相同,但如果当前目录是$HOME,则显示~。 -
%c或%.:列出与$PWD相对的目录数量。它接受一个整数作为参数。因此,%2c会显示$PWD前面的两个目录。 -
%C:与前者相同,但目录名不会被任何符号替代。
日期和时间选项
以下选项提供杂项的日期和时间信息:
-
%D:以yy-mm-dd格式打印当前系统日期。 -
%W:与前者相同,但以mm/dd/yy格式显示。 -
%w:显示day-dd格式的日期。 -
%T:显示当前时间,24 小时格式。 -
%t或%@:与前者相同,使用 12 小时制,am/pm 格式。 -
%*:与前者相同,也显示秒数。
文本格式选项
与之前的转义序列不同,这些需要围绕提示符的目标部分打开和关闭。也就是说,为了给word加下划线,你需要输入%Uword%u。特别注意开头(大写字母)和结尾(小写字母)转义序列的大小写差异,如下所示:
-
%U %u:这启用下划线模式。 -
%B %b:这启用粗体模式。 -
%K %k:这设置背景颜色。使用方式如%K{red}%k。 -
%F %f:与前面的相似,但应用于前景颜色。 -
%S %s:这启用突出显示(高亮)模式。
处理转义序列时,%和)在 zsh 中都是有些特殊的;因此,如果你需要在提示符中显示字面上的%,记得输入%%。同样,字面上的)应该输入%)。这种技术通常被称为转义字符。
注意
你可以在 zsh 配置中启用PROMPT_BANG选项,以便在提示符中使用感叹号(!),从而显示当前的历史事件编号,而无需转义(%!)。只要记住,当你需要一个字面上的!时,输入!!。
setopt PROMPT_BANG # enables '!' substitution on prompt
条件表达式
我们将通过查看可用于条件扩展的转义序列来结束我们的转义序列之旅。不过幸运的是,大部分内容可以总结为以下的三元表达式:
%(X.true-text.false-text)
基本上,这意味着如果条件X为真,执行true-text中的内容,否则执行false-text中的内容。需要记住的重要一点是,你应该用%()包裹你的表达式,而且你看到的点(.)是完全随意的,意味着你可以将它们替换成任何字符。
关于true-text/false-text表达式,手册页(例如当你访问man zshmisc时)告诉我们,它们可以替换为类似!的符号。如果 shell 在有权限的情况下运行,那么它将评估为 true,或者是?,而?前可以跟一个整数n,只有在最后一个命令的退出状态匹配时才会评估为true。因此,为了将#作为你的主提示符,显示是否正在以提升的权限运行,你可以发挥想象,得到如下内容:
PS1=%(!.#.>)
同样,你可以使用以下行来包装最后运行命令的退出状态,如果它不等于0,也就是说:
PS1=%(?..(%?%))
把所有内容组合起来
正如你现在已经非常清楚的,zsh 的提示主题内置了许多强大的功能。实际上,它们如此之多,以至于大多数情况下我们自己定制的解决方案可能会让人觉得是在重新发明轮子。不过,我们仍然需要尝试自己构建一个提示符;那么,如何利用其中一个现有的主题作为起点呢?
导航到你的 zsh 安装文件夹或仓库克隆,进入 Functions 下的 Prompts 文件夹。正如我们之前所看到的,所有提示符都附带一个名为 prompt_<theme_name>_setup 的设置函数。找到 SuSE 主题的设置文件并打开它,它很可能位于 prompt_suse_setup 下。
你看到的其实是一个与文件同名的 shell 函数。只需调用一次这个 prompt_suse_setup 函数,并且不传递任何参数,就能完成两个赋值操作——一个是为 PS1 提示符赋值,另一个是为 PS2 提示符赋值。请查看下面这个为示例格式化的代码:
PS1="%n@%m:%~/ > "
PS2="> "
那么我们开始动手修改这个提示符吧!打开你的 .zshrc 文件,并记住,你将在 promptinit 调用后添加以下这一行。我们可以从突出显示用户名开始,就像在 adam1 提示符中一样:
PS1="%K{yellow}%n%k@%m:%~/ > "
如果你还记得前一部分,%K%k 转义序列定义了背景色。代码中高亮显示的部分,我们将转义序列 %n 包装起来,给当前会话 $USERNAME 添加背景色。在 @ 符号右侧仍然是机器名称的简短版本,当然,还有一些炫酷的行指示符。
我们来为右侧添加一个错误标志,这样就可以立即检查是否有异常的命令退出码:
RPS1="%(?..(%?%))"
如果你愿意的话,可以通过调用一个会异常退出的程序来测试我们全新的右侧提示符。记住,退出状态为 0 是可以的;其他任何状态都会触发我们的提示符。像 ls some_nonexistent_folder 这样的命令应该就足够了:
gfestari@machine:~/ > ls nonexistent_folder
ls: cannot access nonexistent_folder: No such file or directory
gfestari@machine:~/ > (2)
你可以像我们为 PS1 所做的那样,在右侧提示符中添加一些颜色。当你完成调整后,尝试使 .zshrc 文件尽可能与以下代码相似:
autoload -U promptinit
promptinit
PS1="%K{yellow}%n%k@%m:%~/ > "
PS2="> "
RPS1="%(?..(%?%))"
在前面的示例中,我们保留了 autoload -U promptinit 和 promptinit 调用,因此当你最终需要使用它时,提示符模块会被加载并准备好。需要注意的是,除非你打算使用 prompt 模块,否则不需要同时调用这两个命令。
保存文件并重新加载 zsh 配置。我们通过再次加载 .zshrc 文件来完成此操作。不过要小心,这可能需要一些时间,具体取决于你可能添加的其他文件链接:
% source ~/.zshrc
提示
source 有一个更加简洁的兄弟:点(.)别名。现在你已经认识了他,可以自由地执行类似以下操作:
% . ~/.zshrc
我们不妨利用终端模拟器窗口的整个宽度?你知道的,因为是宽屏显示。
一个特别有用的屏幕帮助是当前目录快捷方式,回想一下,它可以是 %~ 或 %d。那么,如何在懒惰的右侧提示符中添加更多上下文信息呢?
RPS1=%~
来吧,我知道你不会以为它就这么简单吧?我们在这里添加了功能,所以不只是简单地丢掉退出状态指示符。想一想,我们还需要将当前工作目录添加到右侧的提示符中。你最初的猜测可能是类似以下命令:
# this won't work!
RPS1=%(?..(%?%)) %~
这几乎是完美的,除了它不会立刻生效。
% source .zshrc
> job not found: ~
真糟糕!不过,有个小细节缺失了,那就是双引号的使用。没错,我们可以通过 shell 的字符串处理,巧妙地让空格通过,而不报错,只需要使用双引号,像这样:
RPS1="%(?..(%?%)) %~"
这将告诉提示符函数直接使用 RPS1 变量,而不必担心解析多个参数。
就这样,你已经在全新的 zsh 安装中拥有了自己的提示符版本。不过,你可能会想知道我们留在那里的第二个提示符是怎么回事。我会留给你自己决定它的命运,因为我真的很喜欢现在这个老派的 > 指示符。
然而,在我们完成这一章之前,我想指引你查看 zshcontrib(1) 手册页中的 PROMPT THEMES 部分。若想获取更多关于创建自定义提示符主题的详细信息,可以在你喜欢的终端模拟器中输入 man zshcontrib。
总结
在这一章中,我们深入了解了 zsh,学习了它的基础特性,并将你的前一个登录 shell 替换成了新的 zsh 安装。我们甚至还更进一步,通过使用各种转义序列和配置选项定制了提示符,加入了一点自家风味。由于我的记性真的很差,这里列出了到目前为止涵盖的内容:
-
我们学习了如何配置和设置 zsh,因此可以抛弃当前的 shell,用全新的 zsh 安装替换它。
-
我们了解了启动文件,现在我们清楚了在终端模拟器窗口出现在屏幕上之前幕后发生的事情。
-
我们熟悉了 shell 提示符,发现 zsh 提供了远超表面功能的内容。
-
我们更进一步,学习了转义序列和条件表达式后,定制了提示符。
现在,你的系统应该已经完全准备好迎接接下来的冒险了。不过,我们还有很多内容要覆盖,所以最好开始下一章 别名与历史记录,在这一章中,我们将学习 alias 机制,如何为功能创建自己的快捷方式,还会开始使用 shell 的历史记录日志。
第二章:别名与历史
在本章中,我们将扩展 zsh 的基础知识,并重点介绍别名,这是最节省时间的功能之一。我们将仔细研究别名如何工作,学习如何用简短的版本替代冗长且无聊的命令,并在启动文件中自动化整个过程。接下来,我们将学习大括号展开,以便在可以避免时减少输入的按键。我们将学会如何使用 zsh 的历史记录和历史展开机制,并将这些新功能融入到工作流中。
使用别名
别名 是一种替代说法,表示同样的意思。可以把它看作是命令的昵称。尽管与聚会后可能获得的尴尬绰号不同,shell 提供的别名机制是一个快捷方式,可以用更友好的名称执行一系列命令和选项。别名的核心目的就是做得更多,最好是输入更少。
我敢打赌我刚刚用那个“输入更少”部分吸引了你的注意。让我解释一下:
ls 命令用于列出目录的内容。快速查看它的手册页(man ls)告诉我们,这里有不少选项:
ls -a # lists all files, even those hidden that start with a dot
ls -l # shows more information for each file, like size and permissions
使用别名,我们可以像下面这样操作:
% alias la='ls -a'
注意
等号(=)两侧不允许有空格。如果赋值的右侧(即等号后面的部分)包含空格或制表符,请确保使用引号将其括起来,如下所示:
% alias talk='echo "quack!"'
% talk
> quack!
现在猜猜如果你输入 la 会发生什么?试试看。Shell 会读取你的别名——在这个例子中是 la——并展开它。整个过程类似于查字典一样查找单词的含义。只不过在这里,一旦找到,意义就会被执行。
我们也可以对 -l 选项做类似的操作:
% alias ll='ls -l'
或者,甚至可以像下面这样混合搭配:
% alias lla='ls -laF'
最后一段代码同时使用了 l 和 a 标志与 F,意味着它的行为与 –la 开关相同,但增加了一个格式化输出的选项,可以轻松区分文件和文件夹。
提示
别名仅适用于交互式 shell。如果你的 shell 以非交互模式运行,它会禁用所有现有的别名。创建脚本时请牢记这一点。
你有两种声明别名的方法。第一种是直接在命令行中声明,正如我们到目前为止所做的那样。这会创建一个可以立即使用的别名;但缺点是,修改的别名只会在当前会话期间有效。一旦关闭终端模拟器或注销系统,它就会消失。声明别名的基本语法如下:
alias [shortname]=<longname or command(s)>
你可以使用这种方法来处理一些你会频繁输入但之后不会再用到的内容。不过,大多数时候,我们需要一些更加持久的东西。需要的是每次在使用命令行时都能用到的东西。
进入启动文件;如果你还记得上一章的内容,启动文件会在每次 shell 启动时读取,并为当前会话加载配置。就像医生推荐的一样。
现在打开你的.zshrc文件,并添加我们到目前为止所做的别名:
# put this on your .zshrc
alias la='ls -aF'
alias ll='ls -lF'
alias lla='ls -laF'
保存你的更改,source(或者使用它的别名点(.))你的文件,别名将在每次未来的 shell 会话中设置并可用。尽管它们的行为不同,所有的别名声明方法使用的语法是相同的。
引号字符
任何给定字符都可以通过在其前面加上反斜杠(\)字符来引用。这个方法在处理具有额外含义的“特殊字符”时特别有用,比如$,甚至是实际的\字符。举个例子,考虑以下echo语句:
# this is wrong!
% echo 'that's a quoted sentence for you'
quote>
shell 提示符表示它在等待一个引号字符(正确)闭合。这里的问题是我们没有正确地转义“that's”中的撇号:
% echo 'that\'s a quoted sentence for you'
> that's a quoted sentence for you
这很好,且工作正常,但当我们需要做大量转义时,会发生什么呢?
% echo 'Escaping single quotes like this \' with backslashes \\ is really tedious'
> Escaping single quotes like this ' with backslashes \ is really tedious
幸运的是,zsh 提供了RCQUOTES选项作为解决方法,它允许你使用双单引号('')进行转义:
% setopt rcquotes
% echo 'Look ma'' I''m escapin'' single quotes'
> Look ma' I'm escapin' single quotes
那么双引号呢?嗯,这些确实很特别,跟其他字符不同,因为它们允许你进行参数和命令替换,正如我们很快会看到的那样。使用双引号时你需要记住,"、\、$和`这些字符需要用反斜杠转义。
让我们试试双引号:
% echo "'echo \"\$HOME\"' will print out '$HOME'"
> 'echo "$HOME"' will print out '/Users/gfestari'
在前面的示例中,当$字符没有被引用时,$HOME环境变量会被实际值(/Users/gfestari)替换。
你也可以在双引号内使用反引号来执行程序:
% echo "zshenv is located at: `locate zshenv`"
> zshenv is located at: /etc/zshenv
shell 会首先执行locate zshenv,就像执行任何其他命令一样,并将其输出替换到传递给echo的参数中。
如你所见,你可以在大多数日常使用中绕过单引号的限制,转而使用双引号、转义序列和参数扩展,只要在特定情况下需要这样做。
单引号和双引号的别名
当你在别名赋值中使用空格时,需要使用单引号(');然而,通常建议无论右侧是否有空格,都使用单引号。确实,这是一个“为了安全起见”的方法,但相信我,这会帮助你避免在声明别名时遇到一些不必要的麻烦。
另一方面,如果你希望在赋值表达式中使用像环境变量或参数替换这样的内容(想想我们在上一章看到的提示符转义序列),则需要使用双引号(")。假设你想使用别名输出当前用户的名字。如我们之前所见,直接访问的方法是通过环境变量$USERNAME。那时,第一个想到的就是使用以下别名:
# This is wrong!
alias saymyname='echo $USERNAME'
不幸的是,这在单引号中不起作用。正确的方法是使用双引号,示例如下:
% alias saymyname="echo $USERNAME"
% saymyname
> gfestari
带有变量的复杂表达式通常需要用引号括起来,我们使用单引号来实现这一点。如果你的别名需要在使用之前扩展变量,则使用双引号。
如你所见,alias机制确实是一个非常强大的功能。如果使用得当,它甚至可以让你重新定义命令的意义:
% alias ls='ls --color=auto'
或者在 OS X 上使用其等效命令:
% alias ls='ls –G'
提示
你可以从另一个别名中定义别名。按照之前的示例,如果你执行以下操作:
alias ls='ls --color=auto'
alias la='ls -a'
la别名的行为就像你输入了ls --color=auto -a一样,你不需要在定义时再次输入--color=auto。
这个别名通过每次调用时添加color标志来改变ls的行为,而不是使用它的更普通版本。虽然这对于特定场景很有用,但如果在像rm这样的命令中使用时,若不小心,可能会带来非常危险的后果。
例如,假设你将文件强制删除的操作别名为rm:
# Be careful when doing things like this!
alias rm='rm -f'
在这里,你正在强制删除文件而没有任何警告。结果是,有人可能在不知情的情况下执行了错误的命令,导致误删后碰撞键盘。这里的重点是,越是避免用你的“l33t 别名”覆盖现有命令,越好。想想那些被砸坏的键盘,别做那个人。
那么,如果你不确定自己是否绕过了当前会话中的任何别名设置怎么办?别担心,有一个命令可以查看。输入alias会列出当前会话中的所有别名:
% alias
> la='ls -aF'
ll='ls -lF'
lla='ls -laF'
saymyname="echo $USERNAME"
你也可以通过简单地指定别名的名称来获取该别名的信息,示例如下:
% alias la
> la='ls -aF'
你可以通过输入以下命令来禁用任何现有的别名,尽管是暂时禁用:
% unalias <aliasname>
只需将aliasname替换为你希望关闭的别名的名称。当你在使用某些特别严格的程序时,别名的选项,甚至是命令行语法,都可能会被覆盖,这时这个功能就非常有用了。
提示
你有几种方法可以防止 shell 执行仅作为其他命令调用的别名。单引号括起来的命令和以反斜杠(\)开头的命令,以及作为相对路径或绝对路径输入的命令,都不会被 shell 视为别名。
例如,如果你希望避免调用别名,可以使用以下任意一种方式:
% 'ls'
% \ls
甚至可以这样:
% /usr/bin/ls
Zsh 还提供了command,它会将任何参数作为外部命令执行,而不是作为函数或内置命令。因此,你也可以使用它来避免使用别名。这将让我们得到如下的示例:
% command ls
你可以通过man zshbuiltins获取更多信息。
和本书中许多其他内容一样,别名并不是万金油,因此,你不应该在终端会话中乱用别名。在你踏上“少输入”冒险之旅之前,以下是几个简单的考虑因素:
-
我的别名更容易记住吗?
echo -n比类似echodontprinttrail的别名要简单得多。保持简单,不要为了别名而设置别名。两个月后,“未来的你”会非常感激。 -
我的别名更容易输入吗?一个别名如果让人觉得尴尬,那就是个糟糕的别名。比如用
alias grepcola='grep --color=auto'替代一个简单的grep,真的吗?记住:简洁明了的名字很棒,但如果你连ping都记不住的名字就不酷了。 -
我的别名是否仅仅为了某种原因覆盖了一些行为?想想之前的
rm -f例子。大多数时候我们希望避免类似的情况;然而,每次都提示用户似乎是一个值得添加到工具箱中的合理功能。将rm='rm -i'别名化,使得删除文件前需要确认,似乎更...好一些。不过要小心这些技巧,过度依赖这种别名可能会导致错误的安全感。试想,如果你习惯了rm总是等待确认,然后在不同的环境中不加思索地使用它会怎样?
全局别名
如果你喜欢别名带来的简便,那么全局别名就是锦上添花。顾名思义,全局别名就是可以在任何地方使用的别名,允许你将过滤器或某些命令作为简单的后缀处理。
让我们看几个例子:
alias -g L='|less'
特别注意-g选项,它代表全局别名。
现在你可以通过添加L后缀将less分页器附加到任何命令的输出中:
% ls -la /etc L
另一个在实际中经常看到的做法是将标准错误输出(stderr)和标准输出(stdout)重定向到/dev/null,这样任何给定的命令都可以静默运行:
alias -g NUL="> /dev/null 2>&1"
这样你就可以调用类似command NUL的命令,而不需要让当前终端窗口充斥着成千上万的日志和信息。
为了清晰起见,别管是否符合标准,建议你像定义全局变量一样定义全局别名,全部使用大写字母。
哈希
你可以使用哈希给特定目录设置别名。这对于你的工作空间尤其方便:
% echo $GOPATH
> /Users/gfestari/workspace/go
我不想每次都输入/Users/gfestari/workspace/go来访问$GOPATH目录中的src文件夹。那么,为什么不利用哈希呢?
% hash -d gosrc=$HOME/go/src
现在我们可以像输入cd ~gosrc一样快速到达目的地(注意前导的~字符)。
这里有另一个例子,这次使用/var/www目录:
% hash -d www=/var/www
% cd ~www
/var/www
您可以开始为您最常访问的目录生成哈希值。只需记得将必要的条目添加到您的.zshrc中,这样您就不必反复输入相同的内容。
额外奖励:设置AUTO_CD选项,这样当您想要切换工作目录时,只需输入目录名称即可:
% setopt autocd
% ~www
> /var/www
现在,去展示给您的朋友们看吧,我会在这里等着。
将一切结合起来
在我们进入下一个话题之前,这里有一些事情可以尝试使用我们新发现的别名。
如果您在终端会话中发现自己多次输入cd ..,请举手。我知道,我能感同身受。那我们来简化一下吧?
我们可以尝试以下方法:
% alias ..='cd ..'
现在,只需输入.即可将当前工作目录向上移动一级. 不错吧?我们可以进一步优化:
alias ...='cd ../..'
alias ....='cd ../../..'
我认为,进行多级目录切换有些过于复杂,但可以根据需要扩展您的别名。
那么,创建目录呢?我敢打赌,像我一样,您不止一次看到过以下情况:
% mkdir dir1/dir2
> mkdir: dir1: no such file or directory
这是因为dir1不存在。所以我们做的是——你猜对了——创建一个别名,让我们能够自动创建父目录,并且更详细地(即,“在创建目录时列出目录”)显示输出:
alias mkdir='mkdir -pv'
现在尝试执行mkdir dir1/dir2并观察会发生什么。您也可以将相同的开关应用于cp和mv等命令,只需记得引用您的赋值!
提示
您可以在启动文件中使用COMPLETE_ALIASES选项,以强制 shell 将别名视为独立的命令进行补全。换句话说,别名不会在尝试补全之前被替换。
扩展
Shell 允许您在执行一行命令之前进行不同类型的操作。在接下来的部分中,我们将学习如何利用 zsh 中的各种扩展和替换形式。
参数扩展
参数扩展允许您在命令行的赋值过程中替换已知的变量。简单来说,参数替换是 shell 用来更改以下内容的机制:
% foo=Hello
它将被更改为以下内容:
% echo "${foo}, world!"
> Hello, world!
请注意,我们在前一行声明的变量foo被替换为echo参数中的实际值。您应该特别关注这个特殊的${}结构。发生的情况是,当 zsh 读取${foo}结构时,它立即知道要用它所持有的值替换其中的内容。
聪明的读者可能已经注意到围绕echo参数的双引号。重要的是要记住,就像别名和提示序列一样,参数替换对于传递给双引号中的参数有效,就像处理其他任何变量一样。
命令替换
就像参数扩展一样,命令替换允许 shell 执行命令并将其输出替换到特殊的语法中。命令替换通常采取`command`的形式,即一个被反引号包围的程序名。
在像 zsh 这样的更新版 shell 中,还有另一种形式的程序替换,它的形式为$(command)。这两种替换形式,``和$(),是一样的;不过,反引号被认为更加便携,因为几乎所有的 shell 都能识别它们。
在实际使用中,命令替换通常用于查找命令的完整路径:
% print $(which zsh)
/usr/local/bin/zsh
或者,为了让它更加便携:
% print `which zsh`
/usr/local/bin/zsh
算术扩展
不要被名字吓倒;就像参数替换一样,算术扩展是另一种替换形式,帮助我们快速穿越命令行。顾名思义,你可以将输入扩展成一系列元素,否则你需要输入很多内容。
让我们试试:
% echo $(( 5 + 4 ))
> 9
我们从一些相当简单的算术表达式开始(我知道,我知道;数学)。但是别担心,刚才发生的事情可以很容易地解释。我们已经知道,echo会将信息打印到标准输出,所以没有什么神秘的地方。接下来是一个使用$(( ))构造的算术表达式。注意,与参数替换不同,这种算术替换需要额外一对括号。这是我们告诉 zsh 它需要处理数字的方式,这就是为什么我们的5 + 4会被当作算术运算来处理。
以下情况适用相同的规则:
% echo $(( 5 + 4 * 3 ))
> 17
这使我们意识到,我们需要更多的括号来设置运算符优先级:
% echo $(( (5 + 4) * 3 ))
> 27
请记住,$(( ))构造只是一个特殊的构造,它告诉 zsh 将其内部的内容作为算术表达式来处理。
有趣的是,我们也可以邀请参数替换加入这个“派对”。看起来变量也可以在算术表达式中进行替换:
% num=5+4
% echo $(( num * 3 ))
> 27
在前面的代码片段中,我们声明了一个变量来保存我们的5 + 4表达式;这使得num成为一个容器,当询问它时,它会大喊出我们的5 + 4表达式。验证这一点的一个简单方法是:
% echo ${num}
> 5+4
然而请注意,通过在表达式中使用num,我们不需要额外的一对括号来设置运算符优先级。这是因为我们的num变量在下一行被替换成它的值,最终我们得到的表达式等同于(5 + 4) * 3。表达式在替换之前会被先计算,否则前一个调用的结果将是17。
让我们再加一把火,使用另一个实用的算术替换:
% num=5+
% echo $(( $num 4 ))
> 9
在这个例子中,我们将 num 表达式保留下来,类似于“将后续内容添加到其中”。这就是为什么在下一行进行求值时,它会被替换为你预期的内容,在这个例子中是 5 +。你看到 num 变量前面的那个 $ 吗?还记得本节开头提到的参数替换吗?这就是发生的情况。如果没有 $num,zsh 就无法处理 num 赋值。
# This is horribly wrong!
% num=5+
% echo $(( num 4 ))
> zsh: bad math expression: operator expected at `4 '
记住,如果你想替换一个参数,请使用 $:
% echo $(( $num 4 ))
提示
你总是可以通过在控制台中输入 man zshexpn 来查看所有支持的扩展类型。
花括号扩展
另一种有用的扩展类型叫做花括号扩展。顾名思义,它的语法涉及使用花括号({})——我猜“花括号扩展”在命名时有点过于冗长了。花括号扩展允许你像下面这样声明一个条目的数组:
% echo picture.jp{eg,g}
> picture.jpeg picture.jpg
发生的情况是,{eg,g} 结构被扩展为一个包含元素 eg 和 g 的数组。Shell 然后循环遍历这些元素,将两个参数传递给 echo 命令,这基本上等同于输入以下内容:
% echo picture.jpeg
% echo picture.jpg
但是,你节省了不少按键和伴随而来的无聊。让我们试试另一个例子:
% touch log_00{1,2,3}.txt
% ls
> log_001.txt log_002.txt log_003.txt
这次我们创建简单的日志文件,模式为 log_00<num>.txt。Shell 会将 {1,2,3} 元素扩展为 1、2 和 3,然后调用 touch 命令三次:
% touch log_001.txt
% touch log_002.txt
% touch log_003.txt
如果你没注意到,我们使用了逗号(,)来声明大括号内的每个元素。现在,你可能会想,“如果我们使用更长的数组会怎样?”这就变得更加有趣了;声明一个值的范围:
% touch log_{007..011}.nfo
% ls | grep .nfo
log_007.nfo log_008.nfo [...] log_010.nfo log_011.nfo
值得注意的是,前面的例子有几个要点。我自行格式化了输出的列表。但那个(…)意味着文件 007 到 011 确实存在。首先,我们现在使用花括号扩展来扩展一个范围,这次是从九到十一。接下来值得一提的是,zsh 足够聪明,能够注意到前导零并将其用作其他值的填充,而不是将它们替换为普通的整数。这就是为什么你看到序列从 log_007.nfo 开始,直到 log_011.nfo 结束。
在第二行,我们使用管道符号(|)将不同命令之间的输出连接或重定向。这种方式我们列出了文件的内容,并将输出重定向到 grep 工具,以便通过 .nfo 扩展名过滤该输出。
当我们在数组中加入一些数学运算时,它们会变得更加有趣:
% foo=(A B C)
% bar=(1 2 3)
% echo $^foo-$^bar
> A-1 A-2 A-3 B-1 B-2 B-3 C-1 C-2 C-3
在上面的代码片段中,我们声明了两个数组,一个包含元素A、B和C,另一个包含元素1、2和3。随后对echo命令的调用传递了参数${^foo}-${^bar}。注意^运算符(在前一个调用中大括号是隐式的,这里为了清晰起见我加上了它们)。再次强调,我们在告诉 zsh 扩展$后面的变量,不过这次我们得到的是笛卡尔积,而不是像A B C-1 2 3那样的结果。这是因为^运算符作为数组扩展表达式。因此,在 zsh 看来,我们在独立地使用数组中的每个元素。
关于数组扩展和^运算符的更详细描述,请访问man zshoptions(特别是RC_EXPAND_PARAM部分)和man zshexpn。
提示
与其他序列一样,某些字符被视为“特殊”字符,需要转义。逗号和单引号需要使用反斜杠进行转义:
% echo \'{\,,\'}\'' needs to be escaped'
> ',' needs to be escaped ''' needs to be escaped
使用历史记录
像大象一样,许多现代 Unix shell 倾向于详细记录在使用它们时输入的大量命令。像许多其他 shell 一样,zsh 也拥有历史日志,并提供一种更便捷的方式来访问其中的每一条记录。从工作日志的角度来看,能够查看你做过的事情不仅实用,而且也有助于提高效率。想想看;你可以使用history命令查看(并最终编辑)之前输入的命令,获得一些关于系统状态的上下文,或者避免反复输入相同的内容。能够轻松检索过去的命令听起来很棒,因为它确实是一个非常巧妙的功能。
我们现在来看看如何使用 zsh 的历史扩展来处理命令行中的以前条目。
注意
使用历史记录
更传统的回顾历史记录条目的方法是使用键盘上的上箭头和下箭头键来浏览历史记录条目。在下一章我们将深入研究如何修改这种行为,具体是在检查 zsh 行编辑器(ZLE)模块时。现在,我们假装这些是唯一可以在历史记录中移动的按键。
历史扩展
zsh 为您提供访问历史记录的方式之一是通过所谓的历史扩展。只要您的输入以感叹号!字符开头,这种方式就会生效。正如我们在上一章看到的,!字符的默认行为可以通过将histchars shell 参数设置为不同的值来覆盖:
% set histchars='@^#'
然而,与其他 shell 不同,zsh 在设置histchars时最多接受三个参数。除了扩展(更改为@)之外,另外两个分别用于替换(^)和注释(#)。
通过将默认的感叹号(!)替换为@字符,您现在可以执行类似于以下命令,调用上次执行的命令行:
% ls *.txt
> readme.txt notes.txt
% @@
% ls *.txt
> readme.txt notes.txt
通过重新定义histchars,你可以使用那些实际上需要特殊字符的命令,比如!,而不需要转义它们或担心历史替换。你可以选择任何你想要的组合,但作为经验法则,尽量选择那些不太常用的字符,这样才值得花费精力去设置。
注意
历史扩展仅在你运行交互式 Shell 并且.zshrc文件中的NO_BANG_HIST选项未设置时才会生效。
访问历史记录条目是通过我们称之为事件标识符的方式完成的。像转义序列一样,标识符是外壳扩展的构造的花哨名称,用于精确地知道从历史记录中需要检索什么。最常见和有用的事件标识符之一是双重感叹号(!!),它本身指的是最后一次输入的命令:
% sh myscript.sh
> myscript.sh: Error: you need to be root to execute this.
% sudo !!
> myscript.sh: executing myscript.sh
如你所见,!!字符对于那些忘记使用提升权限执行命令的情况非常有用。发生的情况是,zsh 立即展开对历史中最后一个命令的引用,并将其替换为包含sudo调用的行,避免你再次输入整行命令。
让 Shell 进行替换并自动执行命令比我们大多数人愿意在 Shell 中投入的“盲目信任”要多一点。幸运的是,我们可以在.zshrc中设置HIST_VERIFY选项,强制 zsh 在每次你执行命令时要求确认:
% setopt HIST_VERIFY
% echo 'Hello!'
> Hello!
% !!
% echo 'Hello!'
如你所见,Shell 会在你的提示符中使用之前的命令来完成输入,但不会执行它。这对像提升权限或使用 sudo 命令非常有用。请随意将setopt HIST_VERIFY添加到你的.zshrc文件中,因为从现在开始我们假设它会被使用。
对于我们刚才输入的命令,这真是很方便,但如果前一个命令在历史记录中更久远呢?那么,我们需要使用普通的事件感叹号:
% !cat
% cat /etc/hosts | grep 127.0.1.1
这里我最后执行的包含cat的命令是打印出我的hosts文件(cat /etc/hosts),随后调用了grep,因为我在寻找包含127.0.1.1的行。
如果你通过 SSH 连接到远程主机,你可以使用类似以下内容来检索上次运行的连接:
% !ssh
% ssh gfestari@192.168.1.10
如你所见,历史扩展的语法相当容易记住。只需将!字符与要查找的命令放在一起,让 zsh 发挥它的魔力。
提示
词设计符表示将包含在历史引用中的命令行的单词。以下是可用设计符的快速参考:
-
^:第一个参数。 -
$:最后一个参数。 -
%:给定词语的最新匹配。 -
x-y:一系列单词。负索引如-i表示0-i;因此,-1表示“倒数第二个条目”。 -
*:所有参数。如果事件只有一个词,则返回 null。
请注意,%单词指示符只有在用作!%、!:%或!?str?:%时才有效;其他任何用法都会导致错误。
要更深入地了解单词指示符和历史扩展语义,请参考man zshexpn,特别是名为“HISTORY EXPANSION”的部分。
那么我们来提高一下难度;你可以结合使用特殊字符^和$,分别访问历史记录条目的第一个和最后一个参数:
% mkdir new_folder
% cd !^
% cd new_folder
^字符扩展为mkdir命令的第一个参数,在这个特定的例子中是new folder。
% touch log1.txt log2.txt
% nano !$
% nano log2.txt
这里同样使用了$,不过这次扩展了touch命令的最后一个参数,因此我们最终可以使用nano编辑它。
如果你熟悉正则表达式,这两个指示符的行为应该不让你感到惊讶。然而,如果你需要访问的字符串既不在历史的开始(^)也不在末尾($),那么你就需要使用?指示符:
% !?etc
> cat /etc/hosts | grep 127.0.1.1
上述表达式匹配包含etc的最新命令。一般来说,使用?事件指示符的语法可以总结如下:
!?str[?]
你看到的那个可选的?,只有在命令后面跟着一些不应被视为str一部分的文本时才是必要的;例如:
% !?etc?^
> /etc/hosts
你注意到?字符是如何作为etc关键字的定界符吗?可以把它们想象成括号,包裹着你想要匹配的表达式。插入符号操作符(^)表示我们对该特定命令行的第一个参数感兴趣,而巧合的是,它正是/etc/hosts字符串。
使用历史感叹号操作符,我们还能做很多其他的事情。另一个有趣的技巧是,它可以引用历史中的特定行。就像之前一样,语法只是对我们已经知道的内容做了些微的调整:
!<hist_number>
% !103 # this retrieves the 103rd entry in your $HISTFILE.
% !4 # this retrieves the 4th entry.
那么如何确定我要使用哪一行呢?嗯,这要复杂一点,但也没有使用grep、ack或当今孩子们用来在历史文件中搜索的其他工具那么复杂:
% history | grep nano
> 2045 nano /etc/hosts
使用grep并搜索包含nano的条目,我可以看到我曾用它编辑过/etc/hosts,该记录位于我的$HISTFILE的第2045行。如果我们想再次打开 hosts 文件,只需简单地调用:
% !2045
% nano /etc/hosts
现在让我们来一些混搭:
% history | grep git
> 1571 cd ../git/dotfiles
1572 git status
1573 git diff zsh/zsh_funcs
1574 git diff zsh/zshrc
1584 history | grep git
在这个例子中,我在查找git条目。如你所见,结果中显示了我使用git做的很多事。结合我们目前学到的内容,我们能做很多事情:
% more !1573$
% more zsh/zsh_funcs
如你所见,我们将感叹号操作符与$选择器一起使用,以引用历史记录中第 1573 行的最后一个参数。
有趣的是,你还可以使用负整数来引用倒数第 n 个条目:
% !-2 # this will retrieve the 2nd to last entry in history.
% !-97 # this does the same to the 97th to last entry.
对一些程序员来说,负索引应该是非常熟悉的(我在看着你们,Python 和 Ruby 开发者)。
历史替换
zsh 历史扩展的另一个有用功能是命令替换。通过这种替换方式,你可以避免重新输入整行 shell 历史记录,仅仅为了编辑其中一个较小的部分。
如果你做过类似以下操作,请举手:
% ls
> dir1 file.txt
% mv fiel.txt dir1/
mv: rename fiel.txt to dir1/fiel.txt: No such file or directory
看来我拼错了 file.txt 文件名,那现在该怎么办?传统的历史记录使用方法会建议我们按上箭头键回忆上一行,左移光标到 fiel 错误拼写处,重新输入正确的名称,完成。可是 zsh 的方法更实用一些:
% ^fiel^file
% mv file.txt dir1/
这是什么魔法?简单来说,链式 ^ 操作符允许你匹配一个单词的第一次出现并用附加到第二个 ^ 操作符的单词进行替换。更一般的语法是:
^history-entry^word-replacement
提示
你可以通过在启动选项中设置HIST_IGNORE_SPACE来防止命令被添加到历史记录中。这样,shell 会忽略以空格开头的行。
% echo "this line will be recorded in history"
% echo "this will not"
更多有用的选项
为了总结这一部分,以下是一些值得考虑在启动文件中填充的与历史记录相关的选项,除此之外,我们在本章中已经讨论过的内容。只需将其中的任何(或全部)选项添加到 .zshrc 中,并记得在每个条目前加上 setopt。
-
EXTENDED_HISTORY:为每个历史条目保存时间戳和持续时间。对于数据分析爱好者来说是一个非常棒的补充。 -
HIST_IGNORE_ALL_DUPS:在显示结果时忽略重复的条目。 -
HIST_FIND_NO_DUPS:不显示已经找到的行的重复项。 -
HIST_REDUCE_BLANKS:移除历史记录中的多余空格和制表符。 -
INC_APPEND_HISTORY:在输入时将条目添加到历史记录中,也就是说,不等到 shell 退出后再添加。可能是 zsh 最棒的功能之一。你知道你想要这个。 -
SHARE_HISTORY:在不同的 zsh 进程之间共享历史记录。另一个非常棒的选项,可以与前一个选项相得益彰。
总结
在本章中,我们仔细研究了 zsh 的一些最显著的节省时间的功能。我们在 shell 冒险中的目标是通过减少输入来实现更多的操作。因此,本章重点介绍了理解别名、它们的工作原理,以及如何以一种不会带来更多麻烦的方式来编写我们自己的节省敲击的定义。
接着我们学习了扩展功能,了解了算术扩展和大括号扩展,目的是让与命令行相关的工作变得更加轻松。最后,我们仔细研究了如何使用历史记录,超越了键盘上下箭头疯狂按压的方法,学习了历史扩展和事件设计符,以避免重复自己进入无尽的循环。
到现在为止,你应该对以下内容有了相当清晰的了解:
-
别名:我们了解了什么是别名以及如何为我们的命令定义一个有用的快捷方式,并且掌握了一些开始构建别名集合的小技巧。
-
参数扩展、命令替换、算术和大括号扩展:如何用任意给定程序的输出、算术表达式的结果来替换命令行中的条目,甚至如何扩展数组,以便不必重复输入相同的内容。
-
历史扩展和替换:如何将以上所有内容应用到 shell 的历史记录中,以及避免无聊地重复自己,特别是双感叹号(
!!)等特定结构。
一点也不错。可以自豪地给自己一个鼓励,或者去喝杯啤酒,现在你应该对 zsh 的使用感到足够自信了。这很棒,但是我们还有更多待探索,所以不要懈怠!接下来是 ZLE,即 zsh 的行编辑器。我们将了解 zsh 的另一个很酷的特性,并发现在命令行上执行一些更高级的文本处理并不需要专门的程序。除了节省我们数百小时重复无聊的击键外,我们还将学习如何定制编辑器的快捷键和绑定,这样就不必再靠猜测了。
第三章:高级编辑
在本章中,我们将从基础的 zsh 使用方法迈出一步,深入探讨命令行的更高级功能。我们将亲密接触并了解 zsh 行编辑器,理解它是如何工作的,以及为什么 zsh 需要专门的输入编辑器。我们将探索新的方法来访问并利用 shell 的历史记录,并学习一些新的命令行编辑技巧,以便加快我们大部分常规任务的处理速度,避免重复劳动造成的乏味。最后,我们将发现,使用 zsh 时,实际上并不需要局限于单行文本。
Zsh 行编辑器
在上一章中,我们学习了如何访问 shell 的历史记录,以及如何使用一些特殊的转义序列来访问这些记录。然而,我们假设查看先前历史记录的唯一方法是使用键盘上的上箭头和下箭头键,按顺序浏览它们。嗯,正如你可以想象的那样,是时候了解 zsh 另一个强大功能了:zsh 行编辑器。
与其他 shell 不同——我在说你,Bash——zsh 并不依赖于 GNU 的 readline 库,而是使用自己的命令行编辑器版本,这个编辑器具备了你期望在一个完善应用程序中找到的大部分功能。zsh 行编辑器,简称 ZLE,允许你定义自己的键绑定(按键组合)和自定义键映射集(键绑定的集合),此外还可以扩展预定义的条目。ZLE 还是 zsh 的一个关键模块,任何交互式 shell 中都会存在它。幸运的是,zsh 足够聪明,知道在不需要 ZLE 时避免加载它,从而节省不必要的资源。
了解 ZLE
到现在为止,你已经使用 zsh 足够长的时间,开始注意到有些事情似乎有些奇怪;比如当你按下一个键时,比如 PageUp,你肯定会看到一些神秘的符号,就像使用 Ctrl + 左箭头快捷键在单词之间移动光标时一样。实际上,ZLE 就是负责理解这些符号的含义以及与之相关的行为的,我们需要通过键绑定来设置这一任务。我们甚至可以将一组键绑定按相同名称分组,并为完全不同的用途使用不同的集合,例如使用 Home 键在编辑命令时移动到行首,或者在浏览历史记录时选择第一个条目。但首先,让我们利用 zsh 默认安装和原始 ZLE 中已经定义的内容。
使用键映射
ZLE 本身提供了一些便捷的绑定,以便满足 Emacs 和 vi 用户的需求,它们是最流行的编辑器之一。ZLE 支持 vi 插入模式和 读取模式,但默认使用 Emacs,因为这对于新用户来说似乎是最友好的映射。
你可以随时通过在命令行中输入 bindkey -e 来访问它。我们将在本书中使用 Emacs 键绑定,但如果你对 vi 模式更为熟悉,也可以选择使用 vi 模式。你可以随时通过在终端中输入 bindkey -e 来返回 Emacs 模式。不论你选择哪种模式,请记住,ZLE 只在交互式 shell 会话中工作,你需要将不同的配置条目和绑定添加到 .zshrc 文件中,因为它们需要在每个会话中设置。
注意
Zsh 根据你的环境变量 $EDITOR 和 $VISUAL 来猜测——准确地说,是做出一个合理的猜测——它将默认使用哪个 ZLE 键绑定。然而,请注意,像 vile 这样的名字,它包含 vi 字符串,会触发使用 vi 键映射。你可以通过在 .zshrc 文件中添加 bindkey -e 来设置自己的安全网,以避免可能的冲突并显式设置键盘布局。
例如,为了将每个新会话默认设置为 Emacs 模式,打开你的 .zshrc 文件并附加以下行:
bindkey -e
在启动文件中设置默认值并不意味着你必须一直使用它。你可以通过输入以下命令在 vi 和 Emacs 模式之间切换:
% bindkey -e
或者
% bindkey -v
通过使用 e 或 v 选项,你是在告诉 bindkey 将提供的 emacs 或 viins 键映射链接到 main 别名,后者将在启动时默认加载。如果发生错误,ZLE 会默认使用 .safe 模式,这是一个非常受限的模式,仅提供最基本的功能。在这种情况下,你最好的办法是通过输入 bindkey -e 并按 return 键来切换键绑定。正如你可能预料的那样,使用 .safe 模式意味着你的配置出现了问题,因此,这种绑定你真的不希望频繁看到。
注意
正如 vi 用户可能预期的那样,zsh 提供了两个 vi 键映射:viins 和 vicmd。不过要小心修改这些设置,因为默认使用 vicmd 会导致你无法插入任何文本。
基本编辑
现在我们已经将默认键映射设置为 Emacs,可以开始讨论一些更有趣的功能,比如能加速任务的键盘快捷键。
以下表格包含一些有用的 Emacs 映射:
| Ctrl + A | 将光标移动到行首 |
|---|---|
| Ctrl + E | 将光标移动到行尾 |
| Ctrl + W | 删除光标位置前的整个单词 |
| Esc + B | 将光标向后移动一个单词 |
| Esc + F | 将光标向前移动一个单词 |
| Ctrl + D | 删除一个字符(向前移动)/ 列出补全项 / 登出 |
| Ctrl + U | 删除整行 |
| Ctrl + K | 删除到行尾的内容 |
| Esc + D | 删除光标右侧的一个单词 |
| Esc + Backspace | 删除光标右侧的一个单词 |
| Ctrl + Y | 拉取最后一个删除的单词 |
| Esc + Y | 切换最后一次拉取的单词 |
| Ctrl + T | 转置两个字符 |
| Esc + T | 转置两个单词 |
| Ctrl + R | 向后增量搜索 |
| Ctrl + S | 向前增量搜索(自动启用 NO_FLOW_CONTROL 选项) |
注意
根据你的键盘和输入配置,你可以将 Esc + 按键序列替换为通常所说的 Meta 键。这个键通常映射到 Alt 键;然而,本文中我们将使用 Esc + 按键序列来指代这些映射,因为它们具有相同的行为,且更具可移植性。
在单词之间来回跳转
Esc + B 和 Esc + F 绑定键与 WORDCHARS shell 变量密切相关。这是 zsh 判断给定单词开始位置的一种方式,尽管对于来自其他 shell 的用户来说,“单词”的定义可能相当特殊。特别地,WORDCHARS shell 变量默认值是……嗯,看看你自己就明白了:
% echo $WORDCHARS
> *?_-.[]~=/&;!#$%^(){}<>
看到这些符号了吗?这些也被认为是单词的一部分(除了字母数字字符)。在这里需要记住的是 shell 的二元行为;一个字符要么是单词的一部分,要么不是。在使用 Esc + B 或 Esc + F 这样的快捷键时请记住这一点,并且在那些特殊情况下,你总是可以覆盖 WORDCHARS 的定义。
拉取和转置文本
你可能注意到在快捷键表中有 yanking 和 transposing 这些术语,可能立即想到了“什么?”这种疑问。所以让我们来进一步解释一下。
转置(Ctrl + T)可能是个花哨的名字,但请放心,其功能远没有听起来那么复杂。简单来说,转置一个字符会将它与右边紧挨着的字符交换位置,使其勇敢地向行尾移动,一次交换一个位置。一旦到达行尾,它只会与前一个字符交换位置。这可能有些困惑,我们通过一个例子来说明:
% echo bca
> bca
这不对。让我们编辑一下之前的历史记录:
% echo bca
现在将光标移到 a 上——最直接的方法是按行尾快捷键 Ctrl + E——然后按下转置快捷键 Ctrl + T。
% echo bac
a 和 c 交换了位置。进展!现在再回退一个字符,光标重新定位到 a 上,然后再次按下转置快捷键。
% echo abc
成功!正如我们将在第五章中看到的,自动补全将修正这些愚蠢的错误;然而,在你输入诸如参数标志或 URL 之类的错误时,转置功能就非常有用了。
% git psuh origin master
错误输入的 git push 命令可以通过简单地导航到 psuh 中的 u 并按下转置快捷键来轻松修复。
% git push origin master
同样的规则适用于单词交换机制 (Esc + T)。唯一的不同,正如你可能已经猜到的,它作用于整个单词,而不仅仅是字符。
正如古老的谚语所说,行动胜于言辞,以下是另一个例子,这次是通过交换单词来演示:
% echo 'world hello,'
哎呀!完全弄反了,赶紧来一个 Esc + T。将光标放在 hello 上,按下交换位置的快捷键。
% echo 'hello, world'
的确,这将让 Backspace 键得到了应得的休假。
粘贴操作看起来有点难以解释,但基本上就是插入你之前通过任一 kill 快捷键删除的单词(Ctrl + W、Ctrl + U、Ctrl + K、Esc + D、Esc + Backspace)。其工作原理如下:
开始输入你的命令。
% echo world hello
意识到你犯了个错误,然后删除错误的部分。在这个例子中,我们使用 Esc + Backspace 删除 hello 字符串。
% echo world _
现在,使用 Esc + B 快捷键将光标向后移动一个单词。
% echo _world
然后通过按 Ctrl + Y 将 hello 字符串粘贴到当前行(请注意,在此情况下,你需要在单词之间加一个额外的空格,_ 字符表示光标应放置的位置)。
% echo hello_world
在使用 Ctrl + Y 快捷键进行粘贴后,你可以通过 Esc + Y 快捷键来在之前删除的单词之间切换。你所看到的 shell 会保留最多 10 个删除的单词,以防你需要再次使用它们。这种“删除单词剪贴板”因其行为被称为 kill ring —— 你会交换每个已删除的单词直到最后一个,然后通过反复按 Esc + Y 从第一个开始重新循环。然而,注意再次按 Ctrl + Y 只会插入一个新的先前粘贴的单词。
回顾历史
正如你可能在 Emacs 快捷键表中注意到的那样,我们可以使用相当多的快捷键来操作历史记录。那么,让我们更好地利用 ZLE,并结合我们新学到的绑定,进一步扩展 第二章 中的 历史扩展 部分。
原来我们可以使用 Esc + < 跳到历史文件的最开始,也就是日志的第一个条目。同样,按 Esc + > 可以将我们送到历史文件的末尾。不过,这对于较大的历史日志并不方便。我们真正需要的是执行增量搜索。Ctrl + R 是 zsh 提供的默认机制,它会显示一个提示框,你可以在其中输入以作为即时搜索过滤器。你输入的越多,匹配就越精确。
% # press Ctrl + R
bck-i-search: _
开始输入,一旦找到你想要的历史记录条目,你可以按 return 执行它,或者按左箭头/右箭头键编辑选中的条目。你可以随时通过按 Ctrl + G 退出此模式。
提示
增量搜索模式有自己的键映射,称为 isearch。
很可能你的终端设置了使用Ctrl + Q和Ctrl + S组合键来进行流控,分别用于停止和恢复终端输出。为了避免与默认的history-search-forward绑定(也就是Ctrl + S)冲突,zsh 提供了NO_FLOW_CONTROL选项,可以在启动文件中进行设置。
setopt NO_FLOW_CONTROL
这将安全地禁用终端内的该行为(其他程序通常依赖于流控),因此,使用Ctrl + S时建议采用这种方式。
高级编辑
到目前为止,我们已经掌握了命令行的基本操作,并且开始习惯 ZLE 的使用。现在是时候更进一步,看看行编辑器到底能做什么了。
ZLE 相关选项
如果没有一些选项让我们现在来调试一下,这一章怎么能算完呢?以下是一些可以尝试的内容,如果你想修改 ZLE 的默认行为:
-
NO_BEEP: 该选项可以跳过错误时的蜂鸣提示。 -
OVERSTRIKE: 该选项将编辑器默认设置为插入模式。其工作原理是每输入一个新字符,它会替换当前光标右侧的字符,而不是像默认设置那样将其推移到右边一个位置。 -
SINGLELINEZLE: 关闭多行编辑。不,我没有吸毒。这可以作为一种提醒,提醒我们曾经度过的黑暗时光。
将这些选项添加到你的启动文件(特别是.zshrc)中,你就完成了设置。
定义你自己的键映射
除了 Emacs 和 vi 模式设置选项外,bindkey内置命令还允许你创建自己的键映射,并使用几个简单的选项将它们定义为别名。特别是,-N标志让你可以即时定义一个新的键映射。
% bindkey -N newmap # this creates a keybind named 'newmap'
或者基于现有的配置创建一个新的。
% bindkey -N mycoolmap emacs # this creates a new keymap based off the existing 'emacs'
然后你可以通过简单地输入以下命令,使用-A选项将新的键映射定义为别名:
% bindkey -A mycoolmap mymacs # this creates an alias 'mymacs' for 'mycoolmap'
为现有的mycoolmap键绑定创建别名mymacs,可以让你在以后使用bindkey -D mycoolmap删除它,而无需担心丢失设置。事实证明,这两个别名被视为独立的键绑定;因此,删除其中一个不会影响另一个。这在你尝试修改绑定时非常有用,尤其是在你希望从头开始,或者只是希望有一个备份,以防设置出现问题。不过要小心命名你的别名,因为如果它们的名称相同,任何现有的键绑定都会立即被新的别名替换!
注意
你应该避免命名自己的键映射以点符号.开头,因为 zsh 的未来版本可能会带有冲突的命名空间。
bindkey命令还有许多其他可用的选项。特别是在填充启动文件时,列出选项很有用。具体来说,l和L允许你以不同格式列出可用的键映射。通过输入bindkey -l,你可以快速查看当前可用的键映射,而输入bindkey -lL则会将输出格式化为一系列bindkey命令。
% bindkey -lL
> bindkey -N command
bindkey -N emacs
bindkey -N isearch
bindkey -N listscroll
bindkey -A emacs main
bindkey -N menuselect
bindkey -N vicmd
bindkey -N viins
你还可以使用此选项来检查某个特定的键映射是否是链接:
% bindkey -lL mymacs
> bindkey -A mycoolmap mymacs
这告诉你,正如预期的那样,mymacs是我们之前定义的mycoolmap键映射的别名。通过使用-lL选项来检查main别名,你就有了一个实用的方法来确定当前正在使用的键映射。
% bindkey -lL main
> bindkey -A emacs main
最后,你可以使用-L选项列出所有当前绑定的键,包括内置键映射的绑定,并将其格式化为可以在脚本中使用的方式:
% bindkey -L
bindkey "^@" set-mark-command
bindkey "^A" beginning-of-line
bindkey "^B" backward-char
bindkey "^D" delete-char-or-list
bindkey "^E" end-of-line
bindkey "^F" forward-char
bindkey "^G" send-break
bindkey "^H" backward-delete-char
# [...] large list of bindings omitted
bindkey -R "\M-^@"-"\M-^?" self-insert
只需将输出复制并粘贴到启动文件中,你就有了自定义键映射的基础。你只需将操作或快捷键替换为更适合你需求的内容,完成即可。很方便,不是吗?
提示
你可以使用read工具来找出终端仿真器发送给 shell 的实际转义序列;只需调用read,然后输入你想尝试的序列。例如,以下是Ctrl + back-arrow 在我的系统上发送的内容:
% read
> ^[[1;5D
一些键,例如Backspace,可能需要你使用-k选项,这样你可以指定要读取的字符数。单独使用时,它将默认读取一个字符。
% read -k
现在(按下Backspace键)。
^?
% # and you are back to the prompt
请记住,你可以随时通过按下Ctrl + C来退出read命令。
Emacs 用户会发现自己对Esc + X组合键非常熟悉。按下Esc,然后按X键,ZLE 会向你展示execute提示符。然后你可以开始输入命令,甚至可以使用Tab键来获得自动补全帮助。例如:
# type in "hello" and navigate to the beginning of the line (Ctrl + A) followed by Esc + X
% _hello
execute:
# ZLE waits for your command, type `ca` and press Tab key:
% _hello
execute: ca
% _hello
execute: capitalize-word
# now press return and watch how the command is applied
% Hello
我们使用Ctrl + A的原因是为了让提示符出现在行的最开始位置,紧跟其后的字符串之前。
提示
记住,你可以随时通过使用Ctrl + G组合键退出execute提示符。
正如敏锐的读者可能注意到的那样,确实有许多方法可以实现相同的行为,但这在某种程度上偏离了execute序列的重点。它的存在仅仅是为了让你做一些平时不会做的事情(可能是因为快捷键不便或缺乏肌肉记忆);执行它及其完成机制将使得回忆命令变得轻而易举。
与execute类似,where-is——默认情况下没有绑定任何序列——将向你展示如何执行给定的命令。只需调用execute,输入where-is(就像之前一样,你可以使用 Tab 进行自动补全),然后按下return键。这时,你将看到Where is:提示符,你也可以使用补全来列出你需要的命令。按下return键,ZLE 将显示绑定到该命令的序列。例如,我们可以使用where-is来找到我们capitalize-word示例的替代快捷键,如下所示:
% # enter where-is mode via Esc + X
> Where is: capitalize-word
> capitalize-word is on "^[C" "^[c"
哇,看看这个。原来我们可以通过使用Esc + C组合键,在提示符后立即将单词首字母大写。
别叫它们小部件
每个渴望学习 zsh 的学生都会经历谈论小部件的时刻。是时候你我一起谈谈它了。
曾经想过所有那些键绑定和特殊动作是如何组合在一起并完美运行的吗?嗯,我们得感谢小部件。看看,zsh 喜欢在能委派责任的地方就委派,而小部件就是一个典型的例子;它不需要处理每一个由按键序列执行的小动作(类似于你在键映射中定义的那些),而是依赖小部件来做实际的工作。把它们想象成执行简单任务的小函数。另一方面,我更喜欢把它们想象成那些在厨房里悄悄施展魔法的小矮人,只要我不在场。
ZLE 自带了相当多的内置小部件,每个小部件都有两个名称,一个是常规名称,另一个是隐藏名称,隐藏名称就是在常规名称前加上一个点(.)字符。隐藏名称的存在仅仅是为了表示它们不能被重新绑定到其他小部件(从而创建一个备份副本,确保在你的键绑定定义出错时始终可用)。
正如你可能猜到的,这并不是全部;小部件可以是用户自定义的,也可以由其他模块(如 ZLE 或内置的 FTP 客户端zftp)定义。
定义你自己的小部件
定义你自己的小部件并不会比用zle -N命令调用小部件的名称更复杂。
autoload -Uz tetris
zle -N tetris
bindkey '\et' tetris
上面的例子稍微改编自 zsh wiki 网站的建议之一(zshwiki.org),将Esc + T组合键绑定到内置的 tetris 模块,这样你就可以在命令行的空闲时光中更加娱乐。
让我们逐行解析一下:
autoload -Uz tetris
这是老掉牙的autoload模块,用于处理在 shell 中加载不同的模块和函数。在这个特定的例子中,我们正在导入tetris模块以供后续使用。
zle -N tetris
这里就是魔法真正发生的地方;我们通过调用 ZLE 并使用-N选项,告诉它我们新小部件的名称是tetris,来定义新的小部件。
注意
请记住,隐藏名称对于小部件来说是特殊的,因此避免使用以点(.)开头的名称。
我们通过将新定义的小部件绑定到键盘上的Esc + T快捷键,简单地完成了定义:
bindkey '\et' tetris
请注意,tetris的粗体调用指的是我们定义的小部件,而不是实际的tetris模块。
现在,为了实际看到它的效果,你必须将其添加到你的.zshrc文件中,或者将其保存为单独的文件并从.zshrc中引用,就像我们之前做的那样。所以,去吧,把它保存为.zsh_tetris到你的$HOME文件夹中,并通过添加以下行从.zshrc引用它:
source .zsh_tetris
现在按下Esc + T组合键,享受你新的小部件。

只是玩了几局俄罗斯方块。是的,我有点生疏了。
特殊变量
在 ZLE 中,有些特殊变量在定义你自己的小部件以编辑和/或操作命令行时会派上用场。
以下列表包含了一些最常用的引用:
-
CURSOR:这是当前光标在命令行上的位置。 -
BUFFER:这是当前编辑缓冲区的内容,可以跨越多行。 -
LBUFFER/RBUFFER:分别表示当前光标左侧和右侧的内容。它们也可以跨越多行。 -
PREBUFFER:这是编辑续行时已经读取的缓冲区内容。 -
WIDGET:这是当前编辑器正在使用的小部件的名称。
通过使用这些变量,你可以例如通过简单地使用${BUFFER[CURSOR]}表达式来精确知道当前光标下的字符是什么。这也可以理解为“BUFFER数组中CURSOR位置的值”(记住,CURSOR只是一个数字,表示提示符所在的列)。
你的第一个函数
你可以通过定义自己的函数来实现更复杂的行为。每次小部件执行时,它都会调用相应的函数。让我们用第二个小部件把它提升到一个新层次。
在下面的示例中,我们将使用一个改进版的优秀rationalize-dot小部件,正如在 ZSH-LOVERS 的 man 页中展示的那样(grml.org/zsh/zsh-lovers.html):
function rationalize-dot {
if [[ $LBUFFER = *.. ]]; then
LBUFFER+=/..
else
LBUFFER+=.
fi
}
zle -N rationalize-dot
bindkey . rationalize-dot
现在让我们逐行讲解它。
首先,我们在这里定义了自己的函数,叫做rationalize-dot。声明函数的方法很简单,就是给它起一个特殊的名字,后跟圆括号,如下所示:
my_function() {
my_code
}
你看到的花括号{}是函数体的定界符;它们之间的内容视为函数的一部分,就像前面示例中的my_code占位符。
另外,你也可以使用保留关键字function来定义函数,并略微变化之前的语法,如下所示:
function my_function {
my_code
}
如你所见,我们将圆括号换成了前面的函数关键字。否则,两种语法表示的是相同的内容,可以互换使用。所以,随便选择一个你喜欢的方式。
同样,调用一个函数并不会比明确写出它的名称更复杂;在这个例子中就是my_function。
回到rationalize-dot的例子,第二行是一个if语句,这是 shell 提供的最基本的控制流机制。用最完整的形式时,if语句将类似于如下:
if condition; then
my_code
elif another_condition; then
more_code
else
even_more_code
fi
在最基本的形式中,if语句测试一个布尔条件,即一个表达式或命令,结果为真或假(或具有表示这一点的退出状态),并据此采取相应的行动。对于第一个条件不适用的部分,else部分将会处理如下:
if condition; then
do_a_barrel_roll
else
echo "can't do it"
fi
提示
注意结尾的fi吗?把第一个if当作是一个打开的花括号{,而fi就是闭合的花括号}。
上面的示例会测试条件condition,如果其评估为真,我们的模拟函数将调用do_a_barrel_roll代码。如果condition不为真(即通常所说的假),那么else块将被调用,并负责发出echo "can't do it"命令。
elif语句意味着“否则,如果”,用于进一步评估条件。你可以根据选项的数量添加尽可能多的elif子句,但在遍历这个过程时要小心;如果不恰当地处理,整洁的代码会迅速变成混乱的“意大利面”代码。
在rationalize-dot的例子中,if语句会检查LBUFFER变量是否匹配表达式*..,实际上是“用户是否输入了某些内容并跟着两个句号?”如果是这样,那么将/..表达式追加到缓冲区变量中。否则,让else语句来处理它。
根据else块,它只会向缓冲区添加一个实际的句号:
else
LBUFFER+=.
fi
起初,这可能看起来并不合逻辑,直到我们进入接下来的几行:
zle -N rationalize-dot
bindkey . rationalize-dot
第一个是我们之前见过的标准小部件声明,但紧接其后的绑定使得rationalize-dot函数需要else语句来添加一个句号。因为它在每次按下句号键时被调用(即它所绑定的快捷键),所以如果用户还没有输入任何内容,它就需要像一个实际的句号键那样表现。
如同之前一样,你可以将其添加到.zshrc文件中(或其他任何由其加载的模块),然后进行测试;只需输入...,看看按下第三个句号后会发生什么。
正如我们稍后将在第五章中看到的,补全,你还可以通过扩展或将它们添加到你的$fpath变量中,让 shell 自动加载函数。
这在与cd命令和大量嵌套文件夹结合使用时尤其有用。
想更进一步吗?你会在zshzle(1)手册页的标准小部件部分找到大量用于自定义快捷键绑定的预定义内建小部件。只需输入man zshzle来开始。
使用区域
继续延续 Emacs 继承的行为,你可以通过按住Ctrl键并按下空格键来设置命令行中的区域。这将触发一个区域选择机制,你可以通过箭头键进行扩展,就像你用鼠标点击并拖动来高亮文本一样。
那么,为什么要使用区域呢?举个例子,你可以通过Ctrl + 空格键组合标记一个区域,然后在其上执行一个命令(类似于我们之前看到的capitalize-word),或者甚至将前面提到的execute-command混合进来,调用一个没有绑定的函数。总体来说,这些来自 Emacs 的小细节使得 ZLE(当然,还有 zsh)具备了几乎像一个完整编辑器一样的多功能性。
多行编辑
到此为止,得知 zsh 足够聪明,能够识别你是否完成了一行的输入,应该不会感到惊讶。不过,与大多数其他 shell 不同,zsh 还能够建议你可能缺少的内容,甚至允许你使用多行输入命令。不同于传统的续行方式,在那种方式中你在行末加上\字符并按回车继续输入下一行,ZLE 会用$PS2提示符迎接你,并且添加更多的上下文信息。
在大多数 Bourne 衍生的 shell 中,你可以使用以下命令:
% ls \
按回车(注意在\字符后面什么也没有)。
> -a
再次按回车,它将像ls -a命令一样工作。Zsh 会给你更多的上下文信息,像这样:
% echo " # press return immediately after the double quotes
dquote> _
$PS2提示符(备用/第二个提示符)会被调用,用来表示 shell 在等待剩余的双引号赋值。继续并按如下方式完成:
dquote> $HOME" # press return here
> /home/gfestari
多行编辑不仅仅是备用提示符那么简单。你可以使用Esc + 回车快捷键来添加一个新的继续行:
% echo hello world # press Esc + return
echo goodbye world
再次按回车,你将看到两行代码按顺序执行,就像它们是一个脚本一样。记住,你不只限于两行,你可以添加任意多的行。
这项魔法的力量归功于self-insert-unmeta命令,它的作用仅仅是将回车符插入到行中。所以现在你知道,每次按下Esc + 回车,你实际上是在使用self-insert-unmeta命令的快捷方式。
除了明显的“不同”的感觉之外,Esc + 回车方法的真正便利之处在于,你可以通过使用箭头键在行间随意移动。更棒的是,每个多行输入会被当作一整行处理。只需按上箭头,你会看到你之前输入的代码重新回到屏幕上,供你编辑。既然我们在讲这个,我还想让你了解一下push-line-or-edit命令,它允许你在继续输入时将之前输入的一块行转换成一个单独的块(否则它将像普通的 push-line 命令一样工作)。它大致是这样工作的:
在命令行中开始输入你的函数,在第一个if语句之后按回车:
% if [[ true = false ]]; then # press return here
then> echo _
停下来。意识到你在if语句的条件子句中犯了一个严重的错误(除了那个极其简单的逻辑...但嘿,这是一个示例)。不幸的是,由于你已经按下了回车,你无法通过上箭头按钮滚动回上一行,这会触发历史搜索行为,那么接下来该怎么办呢?当然是push-line-or-edit。按下Esc + X来执行一个命令,输入push-line-or-edit(你可以使用Tab键进行自动补全),然后按下回车。
提示符将变为传统样式(去掉了续行中的then>指示符),你将拥有一个新缓冲区,里面填充了你之前输入的所有行,当然,你可以像这样随意编辑:
% if [[ true = false ]]; then
echo_
看到了push-line-or-edit有多么优越,显然建议将其绑定到默认的push-line快捷键,^q 或 \eq:
bindkey '^Q' push-line-or-edit
bindkey '\eQ' push-line-or-edit
现在,你可以使用Ctrl + Q 或 Esc + Q 快捷键来编辑整个块,就像它是单行一样。与我们之前看到的history-search-forward绑定(默认是Ctrl + S)类似,Ctrl + Q 需要设置NO_FLOW_CONTROL选项,以避免与终端驱动程序的行为冲突。
这一切都始于push-line-or-edit,所以看起来我们应该讨论一下实际的push-line部分。当你不在续行时,这将是默认行为。只需像往常一样输入命令,但不要按return键:
% ls -a
意识到自己进入了错误的目录后,通过Ctrl + Q调用我们新绑定的push-line-or-edit命令,提示符会被清空,如下所示:
# push-line-or-edit
% _
现在,使用cd进入你想列出的文件夹,看看缓冲区如何重新激活:
% cd myfolder
myfolder % ls -a
一旦你执行了一行命令,提示符会填充你在调用push-line之前编辑的行。
把所有内容整合在一起
如我们之前所见,ZLE 的一个特殊之处在于它能够访问 Shell 的历史记录,这当然意味着我们可以利用一些我们学到的技巧,进一步改善与历史记录的互动。
利用上下箭头键的一种巧妙方法是通过history-beginning-search命令。我们可以定义自己的映射,以便为默认行为添加一些额外的增强,示例如下:
bindkey '\e[A' history-beginning-search-backward
bindkey '\e[B' history-beginning-search-forward
请注意,\e转义序列也可以用^[替代,这样绑定就分别变为^[[A和^[[B。
现在,如果你有一个空的提示符并按上箭头键,它会像往常一样检索历史记录中的最近条目。然而,一旦你输入内容并按上箭头键,它会自动补全与输入匹配的最近条目。
举个例子,输入以下内容,在每一行后按return键:
% echo hello world
% ls
% echo bye world
现在,按上箭头键。自然的向后滚动序列应如下所示:
> echo bye world
> ls
> echo hello world
按Ctrl + G退出搜索模式。现在输入ec并按上箭头键:
% ec
> echo bye world
在你忘记了一行内容并且不想进行搜索或丢弃当前行时,这非常有用。只需记住,如果你希望在会话之间保留这些更改,必须将绑定添加到启动文件中!
总结
在这一章中,我们深入探讨了在你按下回车之前,提示符与 shell 之间发生了什么。我们发现了一些新的技巧来处理历史记录,并通过创建自己的键位映射和绑定,征服了默认的快捷键。就像这还不够一样,你现在知道我们不再仅限于处理一行文本,错误和干扰也能通过几个按键轻松解决,而不必重新输入整行内容。
好吧,我承认,在这一章我们确实很忙。所以现在是时候稍微休息一下了,让我们回顾一下在这一节课中所涵盖的内容。我们做了以下几点:
-
了解到 zsh 是由多个模块组成的,并熟悉了 ZLE
-
使用键位映射来编辑文本,学习了各种快捷键,提升了在命令行中的工作效率
-
定义了我们自己的自定义键位映射,并与不同的区域和多行提示进行了互动
-
了解了小部件,这些是执行编辑器中每个小任务的特殊函数
-
编写了我们的第一个示例小部件,进一步扩展了编辑器的功能,提升了我们的 shell 使用体验
-
通过
if语句了解了函数和控制流 -
最后,我们了解到模块和函数都有特别的权限,可以访问 shell 的不同部分,我们可以做一些操作,比如将 ZLE 小部件与键位绑定连接起来,以便搜索历史记录
说一天不算糟吧。接下来,让我们进入下一章,在这一章中我们将学习关于 Globbing 和文件名生成的内容,这也是 zsh 在其中大放异彩的特性之一。如果你以为在这一章里通过 ZLE 学会了如何减少打字量,等你见识了大括号和限定符的实际应用后,你会更加惊讶。不过,保持你的自信和活力,因为前方还有更多的内容等着我们。
第四章 Globbing
在本章中,我们将了解 zsh 中最强大的功能之一:文件名生成。我们将学习处理系统文件和目录的新方法,甚至通过应用参数替换和修改器扩展一些更传统的命令的功能。本章还作为介绍 zmv 的一部分,这是一个内置功能,提供了许多有用的功能来处理文件的日常和更复杂的任务。我们将学会如何使用 zmv 根据我们新学到的模式来重命名、复制和链接文件。已经感到兴奋了吗?
引用你的字符串
安全声明字符串变量的一种方式涉及使用引号。把它看作是告诉函数“这里开始,这里结束我的字符串”的一种方式。虽然在这个特定示例中不是必需的,但是你可以在使用 echo 时引用短语,如下所示:
% echo 'this is a quoted phrase'
> this is a quoted phrase
单引号被 shell 视为分隔符,因此它们完全被忽略。相同的规则适用于 print 内置函数:
% print 'this is a quoted phrase'
> this is a quoted phrase
那么,使用引号的意义是什么呢?好吧,想象一下,你的输出看起来像以下这样:
% echo this is a backslash: \
~>
是的,这将触发一个续行,所以看起来似乎没有其他方法,除非使用引号。让我们再试一次:
% echo 'this is a backslash: \'
> this is a backslash: \
因此,作为一个经验法则,当我们的字符串中有特殊字符时,我们使用单引号如下所示:
% echo 'special characters like * # and \ need to be quoted'
> special characters like * # and \ need to be quoted
现在,是什么让这些特别呢?嗯,在本书的早些部分,我们看到注释是由 # 符号定义的;我们可以使用 * 字符作为匹配文件名的通配符,而 \ 字符可用于转义具有特殊含义的序列。把所有这些都当作 特殊字符,除非引用它们,否则它们永远不会字面上意味着键盘上显示的内容。
提示
一些特殊字符需要“转义”。这意味着除了它们表示的字符之外,它们还会有不同的含义,除非有一个 \ 字符在它们之前。
例如,echo *.rb 将列出所有具有 .rb 扩展名的文件。如果你想列出一个名为 *.rb 的目录——很奇怪,我知道——你需要调用 echo 并转义 * 特殊字符,如下所示:
% echo \*.rb
另外需要注意的是,\ 实际上是一个特殊字符,因此在需要字面上使用反斜杠的情况下,你需要对它进行转义:
% echo \\
> \
正如我们在前一章中看到的,一个单独的反斜杠(\)只会触发一个续行。
提示
你可以通过提供 (q) 参数来使 shell 输出原始字符串:
% string="This is a *string* with various 'special' characters"
% echo ${(q)string}
> this\ is\ a\ \*string\*\ with\ various\ \'special\'\ characters
双引号
好的,那么当我们需要使用特殊字符的优点,并且需要它们显示为它们的字面值时会发生什么?进入双引号。
提示
选项 RC_QUOTES 允许你在单引号字符串内部使用单引号:
% setopt rcquotes
% echo 'a single ''quoted'' string'
> a single ''quoted'' string
双引号的工作原理是允许你保留任何字符串的值,并在其中启用 参数替换 和 shell 扩展。
认真看看以下示例:
% echo "My username is $(whoami) and my home folder is located at '$HOME'."
> My username is gfestari and my home folder is located at '/Users/gfestari'.
shell 会在双引号内执行 $() 结构中的命令,先于其他任何操作。在这个特定的例子中,我们使用 whoami 程序来告诉当前用户的 ID——在这个例子中是 gfestari——(如果那也是你的名字,你好,久违的兄弟)。
然后,shell 会扩展环境变量 $HOME,该变量指向当前用户的主文件夹,在我的系统中指向 /Users/gfestari。注意单引号在双引号内像任何其他字符一样被处理。
开始使用 Globbing
文件名生成,广为人知的 Globbing(即,全局替换),是 shell 从模式生成文件名的能力。这仅仅是允许 shell 读取模式并生成一系列文件名的过程的名称;事实上,你可能已经在本书中使用 Globbing 很久了,唯一的区别是,我们现在正式介绍这个特性。此外,请注意,当我们在本文中提到 文件名 时,它既指文件 也 指文件夹名,因为你可以使用几乎相同的模式来匹配两者。
当处理 Globbing 时,最重要的是要记住,文件名替换发生在 shell 中的 输入行发送到命令之前。换句话说,你输入了内容,zsh 会做替换,然后 才将扩展后的字符串,而不是你刚输入的内容,发送给函数或程序。虽然有绕过的方法,但请务必保持警觉。
提示
如果你想深入了解本章中提到的一些特性,可以随时通过输入 man zshexpn 查阅官方文档。
使用星号进行 Globbing
Globbing 的工作原理是通过一系列称为 操作符 的特殊字符,创建一个模式,随后 shell 会扩展这个模式为更复杂的传统字符串,而你甚至不会注意到额外的工作量。可以说,这些操作符中最常用的就是星号或星号符号(*)。星号作为 通配符,允许你匹配任何文件名,即使你根本没有提供模式:
% echo *
README.md todo.txt draft.txt new_file.txt
这将列出当前目录中的任何文件和文件夹。注意我们只需要一个 单一的星号 就能做到这一点。然而,如果我们想要所有扩展名为 .txt 的文件,只需要提供相应的模式:任何以所需扩展名结尾的文件。
% echo *.txt
todo.txt draft.txt new_file.txt
发生的情况是,zsh 会读取 *.txt 模式,将其转化为字面意义(所有具有 txt 扩展名的文件名),然后将结果作为 echo 的参数传递,而 echo 实际上并不会处理模式本身。
可以说,这颗星星最棒的特点就是它的多功能性。就像一个醉酒的水手,星星几乎可以与任何东西相处,不仅仅是文件:
% echo *folder
out_folder src_folder
提示
如果你想让 Globbing 不区分大小写(即将大写字母和小写字母视为相等),可以使用NO_CASE_GLOB选项。
% setopt nocaseglob
% echo *.jpg
photo.jpg pic.JPG
但并非所有情况都是一帆风顺的。在使用星号操作符时,有一个细节你需要注意:隐藏文件。如果你记得在第二章中,别名与历史一节中,我们使用了la别名(或ls -a)来列出目录中的隐藏文件;否则,命令不会列出它们。
由于像rm *这样的操作可能导致删除父文件夹,给你带来麻烦,大多数 Unix shell 会忽略大多数命令中的隐藏文件。使用通配符操作符时,Globbing 也遵循相同的规则。解决这种行为的方法是显式使用类似.*some_pattern的模式,以包含隐藏文件,如下所示:
% echo .*zsh*
.zsh_aliases .zsh_funcs .zsh_history .zsh_prompt .zshenv .zshrc
我们使用两个星号来列出所有以点开头(Unix 中的传统隐藏文件)并且文件名中包含zsh模式的文件。换句话说,就是我们的启动文件。
这里的关键教训是:你可以在模式中的任何地方使用星号,不必局限于长度或仅限于扩展名;但要注意隐藏文件,因为星号不会显示隐藏文件,你需要使用类似.*some_pattern的模式才能做到这一点。
注意
你可以通过设置GLOBDOTS选项来绕过“忽略以点开头的文件”行为;然而,建议你不要在启动文件中永久设置该选项,因为它可能导致你删除父目录(.)等问题。
在脚本或函数中使用此选项时,最重要的是在退出前确保调用setopt NO_GLOBDOTS。不过大多数情况下,你只需使用之前讨论过的.*模式就能顺利完成。
匹配任意单个字符的问题
问号符号的作用与星号类似,不同之处在于它匹配单个字符而非多个字符。例如,你可以使用ls ???来列出任何三字母的目录内容,或者更实际一点,使用以下命令列出任何两字母扩展名的文件:
% echo *.??
script.sh
我们甚至可以通过以下类似的表达式查看所有具有扩展名的文件:
% echo main.?*
main.c main.o main.tmp
这与通配符限定符类似;然而,除非显式声明,否则你无法匹配以点开头的任何文件名。
字符序列的方括号
你可以使用方括号构造来匹配模式中的一组字符。例如,你可以使用[ML]*来匹配任何以大写字母M或L开头的文件名。
% ls
Log.log Main.rb README.md script.sh
% echo [ML]*
Log.log Main.rb
注意我们需要将字符类运算符与通配符结合使用,以表示可能包含多个大写字母的文件名。
更有用的是使用连字符(或减号)来命名连续字符的范围。例如,你可以使用[A-Z]*模式来匹配任何以大写字母开头的文件。同样,你也可以使用相同的模式来匹配连续的自然数:
% echo *.log_[1-9]
out.log_1 out.log_2 out.log_3
很简单,对吧?记住你可以声明自己的字符类。这里有一个示例,匹配任何以 1 到 5 的数字或大写字母M开头的文件名:
% echo [1-5M]*.*
Main.rb
就像之前一样,[.]*模式不会像你预期的那样工作;事实上,它根本无法工作。
注意
关于范围的说明
如果你的系统使用的是非英语字母或其他非 ASCII 字符集,那么你可能会期望像ü这样的字符匹配像[a-z]这样的类。然而,这种行为是由LANG和LC_*系列环境变量控制的,并且非常依赖于系统,甚至超出了本书的讨论范围。
在脚本中使用更安全的范围
虽然如果你最近使用了任何现代 Shell,这个可能没什么新鲜感,但有一系列的快捷方式可以让你在处理常见的字符类时免于无聊。你可以通过[[:shortcut:]]模式来访问它们。
所以,例如,如果你需要任何字母(比如包括大写和小写字母的范围[A-Za-z]),你可以使用alpha快捷方式来列出任何以字母字符开头的文件名,如下所示:
% echo [[:alpha:]]*
对字符集感到兴奋了吗?以下表格列出了一些流行的字符集:
| 字符集 | 描述 |
|---|---|
ascii |
ASCII 字符集中的任何字符(请参阅man ascii) |
lower |
小写字符 |
upper |
大写字符 |
alpha |
字母 |
digit |
数字 |
alnum |
字母数字字符 |
print |
任何可打印字符 |
blank |
空格或制表符 |
space |
空格字符(制表符、回车符、换行符等) |
punct |
任何既不是alnum也不是space的字符 |
你可以结合多个模式和字符集;只需记住,最内层的方括号属于字符集,其他内容都放在最外层的方括号之间。例如,如果我们想要所有以digit字符或小写字母b开头的文件,可以使用以下模式:
% echo [[:digit:]b]*.c
bindings.c
如你所见,内层的方括号声明了字符集,而b字符只是作为我们输入的[b]。
避免使用某些字符
好的,到目前为止我们一直在热烈欢迎模式,但当我们想要找到那些不匹配我们正在寻找的内容时会发生什么呢?原来也有一种简单的方法可以告诉 zsh“我想要的是那些与特定模式无关的文件名”,那么我们来看看吧。
假设我们在某个目录下有以下文件:
% ls
bindings.c bindings.h bindings.o main.c main.o
我们只想选择实际的代码文件,即以.c和.h结尾的文件,避免选择以.o结尾的文件。根据我们迄今为止学到的知识,我们可以做到类似如下:
% echo *.[hc]
bindings.c bindings.h main.c
但正如你所看到的,我们的需求越复杂,最终可能会遇到一个庞大的字符类混乱。幸运的是,我们可以通过插入符号(^)操作符来获取一个类的补集:
% echo *.[^o]
bindings.c bindings.h main.c
我们在这里所做的是告诉 zsh 扩展那些不匹配o扩展名的文件名的类。请注意,其余的模式保持不变,并且插入符号紧接在实际进行否定的左括号后面。你可以理解为“除了括号内的内容之外的任何东西”。
小贴士
你可以通过在内括号前使用插入符号来否定一个字符集。例如,如果我们想跳过以大写字母开头的文件,我们可以这样做:
% echo [^[:upper:]]*
处理不匹配的情况
到目前为止,我们已经看到了如何让 shell 解释我们的模式并尝试匹配它能匹配的任何文件名。在接下来的 Globbing 过程中,我们将看看那些不幸的模式,哪些未能匹配任何内容,以及 shell 如何处理它们。
让我们尝试列出一些不存在的 zip 文件:
% ls
bindings.c bindings.h bindings.o main.c main.o
% echo *.zip
zsh: no matches found: *.zip
看起来 zsh 默认会输出错误信息并中止命令的执行。幸运的是,有很多选项可以帮助我们应对这种情况。
首先,有NULL_GLOB,它会让 shell 丢弃任何没有正确匹配的模式。以下是一个例子,当没有匹配时会打印空行:
% setopt null_glob
% echo *.zip
>
这种方式在传递多个模式时非常有用,但有时会导致你调用一些没有任何参数的程序,因此在随便更新启动文件之前需要考虑这一点。
% echo *.c *.zip
bindings.c main.c
第一个模式(*.c)匹配并列出所有.c扩展名的文件;而第二个模式(*.zip)不匹配任何内容,并被丢弃(一个空的第二个条目传递给echo)。
接下来,还有NOMATCH选项,你可以取消设置它来实现类似于 bash 的行为;任何不匹配的模式都会作为字面参数传递给命令。通过以下示例可以轻松测试:
% unsetopt nomatch
% echo *.zip
*.zip
你知道吗?似乎手册页是对的,现在失败的*.zip模式就像我们调用了echo '*.zip'一样。这与NULL_GLOB不同,因为该模式也会被 shell 忽略,但无论是否匹配任何内容,都会作为参数传递给程序。
小贴士
记得你也可以使用setopt NO_NOMATCH来代替unsetopt。
最后,还有一个选项,它模仿了csh的遗留行为,名为CSH_NULL_GLOB。是的,命名惯例可谓不惜一切代价。无论如何,设置它时会发生如下情况:
% setopt csh_null_glob
% echo *.zip
zsh: no match
看来我们又回到了“错误信息并中止命令”的区域。作为好奇的学习者,我们不妨加大一点力度,看看当处理多个模式时会发生什么:
% echo *.c *.zip
bindings.c main.c
好的,现在看起来好多了。发生的情况是,CSH_NULL_GLOB 会在任何单一模式不匹配时显示错误信息并中止命令行,但如果至少有一个模式匹配,它会继续执行并丢弃失败的模式。可以把它当作 zsh 默认行为和 NULL_GLOB 之间那晚激情碰撞的产物。而且在此期间,别怪我带来这样的心理画面。
在我们转向另一个主题之前,还有一个你应该熟悉的选项,特别是在处理模式时。但首先,让我们看看当我们尝试将一个错误的模式传递给 shell 时会发生什么:
% echo *[[:alpha:]
zsh: bad pattern: *[[:alpha:]
注意我们错过了关闭括号(])吗?shell 报告了模式错误,我们则被失败的脚本弄得有些沮丧。让我们再试一次,不过这次我们会设置以下选项:
% setopt no_bad_pattern
% echo *[[:alpha:]
*[[:alpha:]
我们启用了 NO_BAD_PATTERN(或取消设置 BAD_PATTERN,随你喜欢),然后发生了什么?没错;不良模式被 shell 扩展机制忽略,而是作为参数传递给命令。如果你不想在实验新学的模式时看到那些烦人的警告,这个功能非常方便。
扩展 Globbing
如你现在可能已经注意到,当涉及到 Globbing 时,zsh 总是能超出预期,做得比要求的更多。接下来我们将讨论 Globbing 的更高级部分,通常被称为扩展 Globbing。简单来说,我们将学习一组新的字符和表达式,扩展我们之前使用的功能,为 shell 的操作提供更多功能。不过,在我们开始之前,先打开你的 .zshrc 文件,添加以下选项:
setopt EXTENDED_GLOB
或者,如果你打算稍后再添加它,可以从终端调用它。正如我们将很快看到的,扩展 Globbing 是为了赋予像 # 这样的字符特殊含义,回想一下,这个字符通常用于注释。现在让我们动手试试看。
特殊模式
Zsh 丰富的功能还包括一系列快捷方式或特殊模式,旨在使日常任务变得更加容忍。在本节中,我们将熟悉这些功能。
递归搜索
可以说,最受欢迎的模式之一是递归搜索。通过 **/ 组合可以访问到这个模式,它告诉 zsh 从当前目录开始进行递归搜索,并沿着目录树向内搜索。
例如,下面是我们如何在当前工作目录中查找所有 markdown 文件(通常具有 .md 扩展名的文件):
% echo **/*.md
README.md brew/README.md git/README.md scripts/README.md zsh/README.md
另外还有***/这种写法,它告诉 shell 跟随符号链接。不过要小心,因为它可能会导致“文件名过长”这样的错误,这是操作系统告诉你,可能是兔子洞太深,或者你在某个地方有循环引用。
提示
请记住,像find或 The Silver Searcher (github.com/ggreer/the_silver_searcher)这样的专业工具在处理目录递归时会比 shell 的机制更高效。因此,你应该避免依赖它来进行“重要”的操作。
关于使用递归模式表达式的注意事项,你可能最终会收到来自系统的“参数列表过长”警告。这通常意味着在展开**/模式时,shell 占用了太多的内存空间,这种情况通常出现在你有一个非常复杂的目录树时。如果你坚持使用递归展开的方式,解决方法是借助xargs将每个参数传递出去,如下所示:
% find **/*.md | xargs echo
我知道,这个例子有点傻,因为用一个简单的find **/*.md就能实现多行结果。这里的重点是让你了解如何通过xargs分隔结果并将它们pipe到echo中,所以请耐心一点。
最后,如果你想排除当前目录,可以使用某种技巧:
% echo */**/*.md
这样,只有包含base_dir/any_dir的文件名才会匹配该模式。
替代模式
当需要在两个选项之间做出选择时,然后又给出一个明显较差的第三个选项,确实会让人重新思考自己的决定……或者故事是这么说的。幸运的是,shell 不像我们这样复杂,我们可以为它提供一个模式选择,若一个失败,它可以选择另一个。我们通过使用带管道符号的括号构造来实现这一点,像下面这个例子:
% echo [[:upper:]]*.(md|txt)
README.md README.txt
我们继续搜索README文件,使用命名范围来指定我们想要的文件名,文件名前需要大写字母,后面跟上md或txt扩展名。简单吧?嗯,不完全是。小心不要让命令行以括号开头,因为这可能会让它们在子 shell 中运行。Zsh 足够智能,可以区分预期的用法,所以大多数时候你应该是安全的。但还是不要冒险。
在继续之前,需要提到的是,你不能在我们刚刚学到的组内替代项中使用包含/字符的模式。你已被警告!
数字范围
你可以让 shell 匹配它遇到的任何一系列数字,使用<->这种特殊模式。这个构造的优势在于,它能够匹配没有长度限制的任何一系列数字(这是因为 shell 会独立处理每个数字,而不是把它们当作一个整体的整数)。
以以下目录为例:
% ls
log.txt log_002.txt log_010.txt log_031.txt
log_001.txt log_009.txt log_030.txt
我们想处理那些符合 log_xxx.txt 模式的文件,其中 xxx 是数字。让我们把刚才学到的知识付诸实践:
% echo log_<->.txt
log_001.txt log_002.txt log_009.txt log_010.txt log_030.txt log_031.txt
如果我们想要的是从 10 开始的日志文件呢?Zsh 可以帮你搞定:
% echo log_<10->.txt
log_010.txt log_030.txt log_031.txt
如你所见,<-> 模式可以定义一个具有上下限的范围。让我们再试一次,这次匹配 10 到 20 之间的文件:
% echo log_<10-20>.txt
log_010.txt
这个表达式的另一个酷炫之处在于它不会考虑前导零,这使得你可以排序类似 00010 和 00013 的内容。说到这一点,还有 NUMERIC_GLOB_SORT 选项,你也可以设置它来输出任何模式匹配的排序数字匹配(并且是 任何 匹配,而不仅仅是数字范围模式)。
% setopt numericglobsort
% echo log_*
log_001.txt log_002.txt log_009.txt log_010.txt log_030.txt log_031.txt
重新审视插入符号运算符
如我们之前所见,插入符号(^)运算符用于否定模式(记住:“匹配任何不符合此模式的内容”)。这就是使用插入符号的另一种方式:
% ls
README.md README.txt bindings.c bindings.h bindings.o main.c main.o
% echo b^*.o
bindings.c bindings.h
所以基本上,我们告诉 shell 扩展该模式,以匹配以 b 开头但没有 .o 扩展名的文件名。
我们可以安全地说,pattern^other_pattern 表达式通过匹配第一个模式并避免匹配表达式中 other_pattern 部分来工作。不过,现在我们使用的这些具有不同含义的特殊字符,请记住要用单引号括起来你希望字面意义理解的名称或表达式,比如以下示例:
% echo '^c'
否则,你可能会自找麻烦。
波浪符运算符
类似于插入符号运算符的第二种用法,波浪符(~)运算符可以用于定义一个由应该匹配的部分和不应该匹配的第二部分组成的模式:
% ls
README.md README.txt bindings.c bindings.h bindings.o main.c main.o
% echo b*~*.o
bindings.c bindings.h
基本上,这只是两个模式的组合:b* 和 *.o,通过“不要匹配后续内容”运算符 ~ 连接。我们可以这样理解:“匹配以小写字母 b 开头且不匹配以 .o 结尾的所有内容”。
如果你还记得,我们用了 b^*.o 和插入符号运算符,所以如果使用波浪符,似乎更加直观。但别听我说的,试试看,举个例子,我们可以用波浪符排除临时目录中的文件:
% ls tmp
delete_me.sh out.txt
% echo **/*.sh~tmp/*
src/script.sh
发生的情况是,shell 运行第一个模式(**/*.sh),并递归地检查所有具有 sh 扩展名的文件。初步结果是一个可能的文件名列表,接着与第二个模式(tmp/*)进行匹配。与后者匹配的文件名被从列表中移除,剩下的就是我们要找的文件名。
仅出于学术目的,可能是时候提一下,**/ 等价于 (*/)# 模式。就目前而言,特殊运算符 # 将匹配一个重复的字符(在括号内),或者一个递归的表达式(在方括号内)。
通配符限定符
除了操作符,zsh 还拥有限定符,本质上是您应用于模式的过滤器,用以限制诸如仅匹配文件或文件夹、文件名的权限类型,甚至是此类条目的所有者。
所以下面的示例中,我们将列出所有与 *tmp 模式匹配的 目录。注意 (/) 构造,这正是直观地区分文件和文件夹的方式:
% echo *tmp(/)
tmp
那么,仅匹配普通文件呢?公平的说,(.) 是您为仅限文件的限制设计的限定符。
% ls -F
README.txt script.zsh zsh/ src/
突然,一个神秘的文件名出现了:
% echo *zsh(.)
script.zsh
我们有一个 zsh 目录和一个扩展名为 .zsh 的脚本文件。通常,我们会使用 echo *zsh 来列出它们两个,或者如果我们只是寻找具有特定扩展名的文件,则会使用更具限制性的 echo *.zsh。然而,(.) 限定符无疑更适合复杂的树状搜索,或者在处理大量相似文件名和目录时更为高效。
以下是最常用限定符的“备忘单”:
-
(N):如果没有找到匹配项,则移除参数,静默忽略错误。充当每个命令的NO_GLOB选项。 -
(@):符号链接限定符。仅用于选择符号链接。 -
(-@):上一个限定符的特殊变体。用于查找任何 损坏的 符号链接。 -
(/):仅限目录。 -
(.):仅限文件。任何不是链接、目录或之前提到的内容都会被此限定符选中。 -
(*):可执行文件。目录不适用。可以把它看作是对那些拥有+x权限的文件的(.)。 -
(r):文件对当前 shell 用户可读。 -
(w):文件对当前 shell 用户可写。 -
(x):文件对当前 shell 用户可执行。 -
(U):文件属于当前 shell 用户。 -
(R):文件对任何人可读。 -
(W):文件对任何人可写。 -
(X):文件对任何人可执行。 -
(u:root:):文件属于用户root。您可以将:字符替换为任何其他符号对,如花括号:(u{root})。只要避免使用管道符号(|)即可。 -
(on):按名称排序文件名。echo *(on)的构造与ls类似。 -
(On):按名称逆序排序文件名。 -
(oL):按文件大小排序文件名。 -
(OL):按文件大小逆序排序文件名。 -
(om):按修改日期排序文件名。 -
(Om):按修改日期逆序排序文件名。
和往常一样,随意混合搭配以增加趣味。例如,使用 (*r^w) 来查找当前用户可读但不可写的普通文件,或者使用 (@,/) 来查找符号链接或目录。
提示
渴望了解更多关于限定符的知识吗?亲爱的读者,别担心,来拥抱那神秘的力量……算了,我们还是使用 上下文补全 吧。
输入以下内容,并记得在打开括号后按 Tab 键:
% echo *zsh*<Tab>*
这将生成上下文补全,用于此处列出的 glob 限定符(以及更多!)。
接下来是更复杂的一组限定符,如时间戳和文件大小,它们需要更多的解释才能深入了解它们的使用方法。
时间戳限定符
Unix 系统通常会在其文件系统上记录三个时间戳:修改时间、访问时间和变更时间。考虑到这一点,你可以使用以下构造来匹配文件名:
% echo *(mh-1)
这将列出过去一小时内修改过的文件。你可以通过 ls -l 限定符轻松查看此结果。m 代表修改时间,这是你最常关注的时间戳类型。不过,你也可以检查过去一小时内的访问时间((ah-1))或创建时间((ch-1))限定符。
关于“过去一小时”这一部分,它由 h-1 限定符表示,其中 h 代表小时(是的,我知道),可以替换为分钟(m)、周(w)或月份(大写的 "M")。请注意,这个限定符的默认单位是天,因此 (m-1) 意味着一天前,或者更准确地说,是当前系统时间前最多 24 小时。
同样,+ 操作符可以翻译为“更多”,这样你就可以描述像 (mw+3) 这样的模式,这是一种简洁的表达方式,意思是“从今天起超过三周”。最后,你也可以通过结合这两个操作符来指定一个范围:
% echo *(m-5mh+2)
这将列出在五到两小时之间修改过的文件。
文件大小限定符
今天你将学习的最后一个限定符是文件大小。正如你可能已经猜到的,我们可以根据文件在磁盘上的大小来查询文件名:
-
(Lm+size):文件大小大于size兆字节。例如:(Lm+5)——大于五兆字节。 -
(Lm-size):文件大小小于size兆字节。例如:(Lm-2)——小于两兆字节。 -
(Lk+size):文件大小大于size千字节。例如:(Lk+5000)——大于 5000 千字节。 -
(Lk-size):文件大小小于size千字节。例如:(Lm-2000)——小于 2000 千字节。
zmv 函数
在上一章中,我们学习了 zle;这是 zsh 用于处理命令行的模块。现在是时候利用我们新学会的 Globbing 技能,来了解 zmv 了,它是为了让复制、移动和链接文件变得更加轻松而创建的函数。
那么,你可能会问,zmv 是怎么回事?与普通的 cp 相比,这个内建函数有什么特别之处呢?zmv 的魔力在于它基于模式进行操作。此外,正如我们在本节中将要看到的那样,zmv 默认设计为安全的,这意味着它会在执行任何可能带来风险的操作(如覆盖文件)之前,要求你确认。
不过,在我们开始之前,你需要将以下内容添加到你的 .zshrc 文件中,记得要执行或重新启动你选择的终端模拟器:
autoload zmv
这将使 zsh 在启动时加载该函数,使其在会话中可用。现在你只需输入zmv,就能看到一套相当简单明了的指令。基本上,zmv 语法需要两个模式:一个用于匹配文件名,另一个用于将结果转换成的目标模式。
zmv [OPTIONS] old_pattern new_pattern
正如你可能猜到的,zmv 涉及大量的 Globbing,这就是为什么我们现在才开始了解它。下面是如何用它将.txt文件重命名为 markdown 文件(.md):
% zmv -Wv '*.txt' '*.rb'
mv -- README.txt README.md
我们使用了详细的-v选项标志,因此我们可以从输出中获取更多信息。zmv函数通过展开两个模式,然后将实际功能委托给更强大的命令,如cp、ln,或者在这个特定情况下,mv。
你可以使用-W选项来自动转换通配符。结合noglob选项,你可以为mv命令增加一个全新的功能,这类似于 Windows 系统的cmd变体的特殊行为:
alias mmv='noglob zmv -W'
现在你可以在同一个调用中移动文件并重命名它们:
% mmv *.c.orig orig/*.c
对于适用于 zmv 的其他选项标志,以下是一些最相关的:
-
-f:强制覆盖目标文件 -
-i:每个操作的交互式提示 -
-n:不执行操作,仅打印将会发生的事情 -
-v:详细输出—在执行时打印每一行 -
-w:在模式中的通配符上隐式添加括号 -
-W:类似于-w,但将替换模式中的通配符转换为引用
不过,别想你需要记住这些选项。正如我们将在下一章看到的那样,你可以始终使用Tab键进行上下文补全,或者在 zmv 的特殊情况下,你只需输入zmv并按Return键,就能获得完整的选项列表。只要知道至少有几个选项可供你使用。
提示
你可以通过传递-n标志来执行通常所说的干跑操作。这将使 zmv 仅打印出将要执行的操作,而不真正执行。这是测试和调试脚本的最佳方式,避免了……你知道的,慌乱的发生。
% ls foo
% zmv -n '(*)' '${(U)1}''mv -- foo FOO
如果你需要更高级的用法,可以使用多个表达式,例如old_pattern参数。匹配这些的文件名将被分组,并可以通过new_pattern表达式(按照$1、$2等模式)访问。例如,我们可以使用以下方法在文件夹树上递归重命名图片,以便它们的扩展名全部为小写:
% zmv '(**/)(*).(#i)jpg' '$1$2.jpg'
总结一下,通过一些 Globbing 和练习,你可以充分利用 zmv。你只需要一个合适的模式来匹配,并且有一个字符串来实际使用该模式。zmv实际上会忽略在扩展过程中没有改变名字的文件,它甚至不关心目标是否应该是一个目录还是一个普通文件。
提示
你可以通过输入man zshcontrib来访问 zmv 的高级文档。
总结
这是我们旅程的一个阶段,需要我们收拾东西,结束这一章。不过,这次我们从把 Globbing 当作“像正则表达式一样”的工具开始,最终理解它实际上是完全不同的东西。幸运的是,一旦我们学会了最常用的操作符和限定符的行为,那个“怪兽”就变得相当容易驯服。随后,我们通过更多的特殊模式扩展了这些构造,并且了解了zmv,使得我们的大部分日常任务变得轻松自如。总结来说,我们可以说我们:
-
学习了引号、转义符号以及双引号与其中的 shell 扩展。
-
开始了命令行中的全局匹配和参数替换。
-
提升了技巧,深入学习了扩展 Globbing,了解了递归搜索、否定和排除模式的操作符。
-
我们了解了全局限定符,如何使用它们根据系统时间和文件大小来区分文件。
-
最后发现了 zmv,它让我们将前面提到的所有内容结合起来,让处理复杂的文件名变得像散步一样轻松。
看起来到目前为止我们已经学到了很多,这些内容可以满足我们大部分的需求。如果我可以这么说的话,这其实是一个不错的交易。实际上,我可以这么说,因为这正是戴上写作者帽子的一大好处。
下一章将介绍补全功能。到目前为止,我们已经取得了相当不错的进展,所以我不会对你说谎(再一次);补全实际上就是让大多数人一试成主顾,永不回头的原因。你目前尝试过了一部分,但更多的精彩内容就在这一页等着你。
接下来是第五章,补全。快点!
第五章:补全
这就是大多数用户切换到 zsh 的原因:补全。在这一章中,我们将介绍 zsh 的最佳功能之一:compsys。被称为“新”的补全机制,本章将重点讲解它的各种功能和配置。我们将学习如何调整补全行为,使其不再仅限于文件名,并通过样式和我们自己的函数进一步提升它。完成后,你应该能够阅读大多数 zsh 脚本,并调整许多现有的函数。
开始使用补全
没有人真正喜欢输入枯燥的文件名,这也是补全功能最初出现的原因——输入几个字母,按下Tab,然后 Shell 会帮你完成其余部分。不过 zsh 更进一步,实际上允许你补全几乎任何东西。默认情况下,Tab键在 zsh 中绑定到补全命令。
和 Bash 一样,zsh 默认启用文件名补全。不过与其他任何东西不同,zsh 可以启用几乎所有在命令行中出现的内容的补全——路径、外部和内建命令、别名、函数和选项,应有尽有。即使你说不出它的名字,你也可以编程来补全它,稍后我们将会学习这一点。
最初,zsh 使用内建模块和特殊语法来提供自动补全。幸运的是,这最终被一个更加简单的机制所替代。我们将重点介绍这个完全基于 Shell 函数的新补全系统。
打开你的.zshrc文件,添加以下内容以激活 Shell 补全功能:
autoload -U compinit
compinit
这个新增功能将使 Shell 加载并自动启动补全系统。-U标志告诉 Shell 避免展开任何别名。这将使双击Tab键触发补全模式。
注意
compinit是补全系统的核心部分。因此,直到你更新并引用了你的.zshrc文件,或者至少在终端中运行了autoload -U compinit && compinit,你无法进行任何测试。
记得给你的文件加上源文件引用,然后让我们尝试一下新启用的补全功能。输入ec并按下Tab:
% ec <Tab>
% echo
Shell 会自动补全外部命令,如echo。zsh 真是太贴心了,不是吗?
提示
正如我们之前提到的,zsh 有两种方式来执行命令行补全。你可以通过输入man zshcompctl来了解“旧方式”是如何工作的,当然,这只是为了学术目的。
补全也可以应用于环境变量,例如:
% echo $HOM <Tab>
% echo $HOME
默认情况下,zsh 启用AUTO_LIST选项,用于处理歧义匹配的解析,向你提供所有可能的补全。为了演示这一点,让我们回到之前的例子,这次我们只输入HO,看看补全是如何工作的:
% echo $HO<Tab>
Completing parameter
HOME HOST
Shell 并不完全知道我们想要的是什么,所以它会在提示符下方展示一份可能匹配的列表。当标准发生变化时,这个列表会被更新,因此我们只需担心按下Tab键。
现在,让我们尝试使用ls进行选项补全,如下所示:
% ls -<Tab>
下方的截图展示了如何触发ls命令的补全:

菜单选择演示
看到有几个可选项可以选择,zsh 会为你展示一个菜单,你可以通过反复按Tab键或使用箭头键来浏览。
最后,你还可以使用补全来扩展命令,如下所示:
% echo `which zsh`<Tab>
% echo /usr/local/bin/zsh
你可以看到补全的效果——它足够强大,让我们希望能在所有地方都能使用,而不仅仅是输入的单词上。然而,在我们开始编写自己的函数之前,我们先来看看 zsh 样式,即我们用来配置zstyle内建行为的选项。
与 zstyle 打交道
与我们在本书中设置和取消设置的 Shell 选项不同,zstyle 要求更复杂的语法作为启用上下文敏感补全的交换条件。
zstyle 是通过zstyle关键字来定义的,后面跟着一个以冒号分隔的参数列表:
:completion:function:completer:command:argument:tag
第一个参数completion用于定义一个上下文,因为在不同的上下文中,某个样式的行为可能会有所不同。不过这并不复杂,我们很快就会看到。
第二个参数是样式的名称,用来供内建引用。其余的参数则赋予该样式独特的补全行为。
这里模式也会回归,你可以将它们用作每个后续参数的标记。当你定义样式时,顺序非常重要,因此尽量将不太具体或通用的样式放在定义的底部,否则你会覆盖掉更具体的功能。
你可以定义的最通用的样式类型是:completion:*,它几乎适用于所有情况,因此在定义时要小心,避免错误地将其排在前面。
正如你所想,zsh 有一些技巧,比如能够在匹配列表中显示一些有用的消息。不过,为了使这个功能生效,我们需要启用以下样式:
zstyle ':completion:*' format %d
通过将这个添加到你的.zshrc文件中,每当 zsh 执行补全时,你现在可以获取更多的信息。例如:
% true<Tab>
no argument or option
细心的读者可能已经注意到样式格式中有%d模式。没错,我们可以使用与定义提示符时相同的转义序列。
提示
已经厌烦听到提示音了吗?那是 zsh 在告诉我们,尝试了一个模糊的补全。你可以通过在.zshrc文件中取消设置LIST_BEEP选项,来避免这种烦人的模糊提示:
unsetopt LIST_BEEP
正如我们之前提到的,你还可以将你的样式行为缩小到更具体的上下文。例如,你可以使用以下任意一个:
zstyle ':completion:*:descriptions' format '%B%d%b'
zstyle ':completion:*:messages' format %d
zstyle ':completion:*:warnings' format 'No matches for: %d'
这只是为了为属于warnings、messages和descriptions组的消息设置一个自定义模式。正如你所看到的,warnings现在将报告为No matches for: <argument>,这就不那么单调了。
你也可以为结果增加一点点装饰,像这样:
zstyle ':completion:*' group-name ''
这将单独显示所有不同类型的匹配项。如果没有为某个特定匹配项定义标签或组,它将显示在default组下。
提示
菜单选择让你心动了吗?下面是如何让它对你所有的匹配项都可用:
zstyle ':completion:*' menu select=1
对样式感觉舒服了吗?很高兴听到这个。正如从这些示例中可以看到的,这里没有神秘的魔法——只有一些文档和创造力,填补你和自定义样式之间的空白。
命令更正
补全也可以更正你可能输入的任何拼写错误的命令。我们将使用以下格式来定义样式:
zstyle ':completion:*' completer _expand _complete _correct
然后我们将使用以下命令来测试自动更正功能:
% prnti<Tab>
corrections (2 errors)
print printf
original
prnti
Zstyle 注意到我们拼错了print,并对此进行了详细说明。记住,你可以使用Tab键在可用选项之间切换。
或者,如果你想要一个更“手把手指导”的方式,你可以使用correct选项。具体来说,这个选项会让 zsh 在每次建议更正时都要求你确认:
% setopt correct
% prnti<Tab>
zsh: correct 'prnti' to 'print' [nyae]?
这个特殊的nyae首字母缩写代表No、Yes、Abort和Edit,其工作方式如下:
-
n:这将强制 shell 执行你在命令行中输入的内容(在这个例子中是prnti)。 -
y:这将执行更正(在此示例中,将prnti更正为print)。 -
a:这将中止并允许你输入一个完全不同的命令。可以把它当作一个紧急按钮。 -
e:这将允许你编辑命令行中的当前文本。如果你觉得 shell 提供的建议完全不对劲,可以使用此选项进行更精细的控制。
命令选项呢?你知道的,那些我们经常传递的标志?结果,竟然也有样式可以处理这个。以下内容将使命令显示它们选项的描述:
zstyle ':completion:*' verbose yes
这些可以很容易地解释清楚;现在,继续输入以下内容:
% print -<Tab>
-- option --
-C -- print arguments in specified number of columns
-D -- substitute any arguments which are named directories using ~ notation
-N -- print arguments separated and terminated by nulls
-O -- sort arguments in descending order
(list goes on...)
还不错吧?记得我提过在我们学会一些样式后,就不那么依赖 man 页面了吗?没有?没关系,反正我们不会那么...算了。
补全器
zstyle 中的第三个条目是为补全器保留的。这些是处理不同类型补全的函数。默认情况下,补全器列表包含一个函数,_complete,但补全器家族中的每个成员都会为你的样式添加独特的行为。
zstyle ':completion:*' completer _expand _complete _correct
在你的.zshrc文件中使用时,这个补全器将使用通配符扩展输入并与_complete和_correct补全器进行匹配。这里使用_correct补全器来修正任何打字错误和拼写错误。我们将其放在参数列表的最后,这样_complete就可以优先使用。
注意
当在样式中使用时,补全器名称会省略前导的下划线:
zstyle ':completion::complete:*' use-cache on
这个样式通过为任何需要的补全启用缓存层,来配置_complete补全器,从而提高此类功能的整体响应速度。
与_correct类似,_approximate将执行相同的任务,额外的好处是允许在光标位置输入一些额外的错误字符。请注意,如果你需要同时使用这两者,_approximate需要放在_correct之前。
作为一个函数,zstyle 也使用标志。我们特别关心的是-e选项,它告诉 zstyle 在每次调用时将最终的字符串作为一个参数进行评估。这使得我们可以使用更加动态的样式,例如以下:
# One error for every three characters
zstyle -e ':completion:*:approximate:*' max-errors 'reply=( $(( ($#PREFIX+$#SUFFIX)/3 )) numeric )'
这会配置approximate补全器,动态地评估max-errors参数的每个调用的参数。reply=( $(( ($#PREFIX+$#SUFFIX)/3 )) numeric )字符串使用reply钩子在行编辑器中显示结果,并将其值设置为表达式(PREFIX + SUFFIX)/3。这意味着“每三个字符一个错误”。PREFIX和SUFFIX分别是包含光标位置之前和之后的值的变量。
忽略匹配项
有时,某些匹配建议会让你感觉完全不合适。幸运的是,zsh 的开发者们为我们提供了一个_ignore补全器。
以以下目录树为例:
zsh
├── README.md
├── Completion/
├── Misc/
├── Scripts/
└── Util/
当我们在前面提到的任何子目录中工作时——例如,Completion文件夹——看一下我们尝试使用cd命令更改目录到同一级别的其他目录时发生了什么:
% cd ../ <Tab>
directory
Completion/
Misc/
Scripts/
Util/
让Completion机制显示我们当前所在的文件夹有点尴尬,而且这让整个cd命令显得有些没必要。为了让 shell 变得更具上下文敏感性,我们可以使用ignore-parents、parent和pwd选项来修改cd命令的补全行为:
zstyle ':completion:*:cd:*' ignore-parents parent pwd
以下将从补全结果中移除相应的匹配项。注意,现在Completion已经从结果中消失:
% cd ../ <Tab>
directory
Misc/
Scripts/
Util/
在此期间,你可以使用以下样式,当使用目录作为参数时,去除末尾的斜杠:
zstyle ':completion:*' squeeze-slashes true
函数定义
最后,我们将关注compsys,即 zsh 的补全系统。这是 shell 中最复杂的部分之一,对于用户和开发者来说都不容易。然后,在我们深入了解compsys之前,我们需要稍微停一下,见见一个实际的函数。
小贴士
和往常一样,你可以通过 man 页面了解更多关于 compsys 的信息,特别值得关注的是 man zshcompsys 和 man zshcompwid。
下面是其中一个例子:
hi() {
print 'Hello, world'
}
在这里,我们定义了 hi 函数,这就是我们以后需要时调用它的方式。每次使用时,它都会打印 Hello, world。那我们就开始吧,怎么样?
打开你选择的终端模拟器,逐行输入以下内容:
% hi() {
function> print 'Hello, World!'
function> }
注意 zsh 是如何识别出我们正在定义一个函数,并立即使用继续提示符(function>),让我们可以继续操作吗?真是太好了,zsh 等着我们直到我们正确关闭花括号。
现在,来测试你的第一个函数吧:
% hi
Hello, World!
它们增长得如此之快!
接下来是让人遗憾的部分——这个函数只在你当前的会话中有效,就像我们在第二章,别名和历史中定义别名时一样,这是我们 zsh 冒险的开始。如果你希望 hi() 或其他任何函数在每个交互会话中都能使用,你需要把它添加到启动文件中。
需要提醒的是:一旦你开始使用补全和函数,这些启动文件会变得相当拥挤。因此,最好是将函数移到一个更合适的地方,比如它们自己的 .zsh_functions 文件。别担心,这个过程非常简单。
首先,我们创建一个隐藏文件;你可以随意命名,但我们这里使用 .zsh_functions(注意前面的点,这样系统就会把它隐藏起来)。
% touch ~/.zsh_functions
一旦你在 $HOME 目录中创建了文件,接下来就是在其中添加你的函数。你可以使用你最喜欢的编辑器;我们这里为了方便,就用 cat:
% cat >> ~/.zsh_functions
greet() {
print 'Hello, World!'
}
按 Ctrl + D 关闭文件。
现在,正如我们之前所学,这个文件本身不会执行任何操作,除非我们让它生效。因为每次手动加载文件会很麻烦,所以我们只需再往前一步,把 .zsh_functions 的加载过程添加到启动文件中。接下来,打开你的 .zshrc 文件,添加以下内容:
[[ -f ~/.zsh_functions ]] && source ~/.zsh_functions
这是一个条件语句。这里使用的双中括号([)是 test 命令(或者说是新测试,如果你已经使用命令行一段时间了),它帮助你比较字符串并测试文件属性。-f 选项用于常规文件,只有在文件存在时才会成功。所以我们实际上是在说“测试 ~/.zsh_functions 文件是否存在”。如果测试通过,接下来的命令部分会被执行,我们最终就能加载我们的函数文件。
顺便提一下,这个表达式支持文件名通配符,因此我们在[第四章中学到的所有技巧,通配符,仍然适用。
你可以使用相同的机制来源代码多个文件;只需要记得在 .zshrc 文件中添加这行,别忘了测试失败开关,这样就能避免加载系统中不存在的文件(当然也避免错误)。
和往常一样,你可以通过在终端中输入 man [ 来深入了解 test 命令。关于 [[ 复合命令的更多细节,请参阅 zshmisc(1) 手册中的 条件表达式 部分。
好的,我明白了。那么,函数和补全有什么关系呢?其实,关系很大!看,compsys 完全是由函数组成的:每当你按下 Tab 键时,函数会自动被调用。区别在于,这些函数如何利用一些其他特殊命令与我们的老朋友 ZLE 进行交互,从而展示可用的补全项。不过别担心;与广泛的看法相反,这里没有任何神秘的魔法。
函数的路径
那么,函数。更准确地说,是一堆函数(嗯,这个你可以自己判断)。zsh 怎么知道该去哪里找呢?其实比听起来要简单;shell 会加载属于其函数路径或 $fpath 的任何东西,这是一系列包含补全所需函数文件的目录。去看看吧:
% print -l $fpath
在启动时,shell 会扫描并加载函数路径列表中出现的所有目录,前提是你首先调用了 compinit。所以记得在 .zshrc 文件中调用 autoload –U compinit。不过需要注意的是,这个调用会加载 $fpath 中的所有内容。如果你有单独的函数需求,可以通过 autoload 显式地调用它。如果你将之前的函数保存为名为 _greet 的文件,并将其放入 $fpath 中的某个目录,那么你可以在启动文件中使用以下内容,自动将该函数加载到 shell 中:
% cat >> _greet
echo 'Hello world!'
autoload -Uz _greet
看那个 -Uz 标志吗?-U 标志的作用是告诉 shell 使用名称 _greet 来引用我们刚创建的函数,而 -z 标志则告诉 zsh 以原生模式加载该函数。每次调用 autoload 时,-U 和 -z 标志都会隐式地添加,但我留着它们是为了让你注意到。
好的,单行函数很有趣,直到有人需要一些更复杂的东西。文件中的单个函数会毫无问题地加载。那么,我们如何在文件中使用辅助函数(用于我们主要功能的辅助方法)呢?zsh 的方式是我们应该定义一个函数,命名和文件名相同,并在文件的最后一行调用它:
_greet() {
echo "Hello, World!"
}
_meet() {
_greet
echo "Ohai there $@"
}
_meet "$@"
文件中的最后一行负责调用文件中的 _foo 函数,并传递相同的参数。如果你调用的是 meet John,那么这些参数就会传递给 meet 函数。
将文件保存为meet(没有扩展名),并将其放置在任何一个$fpath文件夹内;重新启动你的 shell 并调用以下命令:
% meet John
Hello, World!
Ohai there john
提示
扩展你的 fpath
如果你不想对函数的副本或链接进行修改,可以通过如下设置变量,轻松地使用更多的文件夹扩展fpath:
fpath=(~/my_folder $fpath)
这将把文件夹my_folder添加到 shell 的fpath,有效地扩展了其内容。这在你缺乏某些系统权限时特别有用。请注意,我们使用的是文件夹的绝对路径。
现在让我们来看看一个正式的完成函数。别担心,我们从一个简单的开始,比如_md5sum,它通常位于$ZSH_INSTALL_DIR/functions/文件夹下。它在这里显现出它的全部光辉:
#compdef md5sum
_arguments -S \
'(-b --binary)'{-b,--binary}'[read in binary mode]' \
'(-c --check)'{-c,--check}'[read MD5 sums from the FILEs and check them]' \
'(-t --text)'{-t,--text}'[read in text mode]' \
'--status[no output, status code shows success]' \
'(-w --warn)'{-w,--warn}'[warn about improperly formatted checksum lines]' \
'--help[display help and exit]' \
'--version[output version information and exit]' \
'*:files:_files'
尝试通过输入md5sum -,然后按下Tab键,你将看到来自arguments的选项。
任何完成函数的第一行代码必须是#compdef语句,后跟由该函数完成的程序的名称(在本例中是md5sum)。
接下来是对内部_arguments函数的调用,该函数实际上处理格式化并显示在屏幕上的选项。此函数通常在指定符合标准 Unix 约定的命令完成时使用,选项和参数列表遵循这些约定。通过使用-S选项,我们声明在--出现在行上时,不会再完成任何选项。--是用于结束选项解析的分隔符,因此,除非我们明确指定,否则此参数通常会被忽略。
如果你仔细观察,你会发现每个参数项(通过\分割为续行)遵循相同的模式:
'(optional exclusion list)'{options}'[help text in brackets]'
请注意,选项及其详细版本周围的花括号是用来将它们分组的,否则它们是可选的。
排除列表通过明确告诉 zsh 哪些内容不应包含在结果中来工作。换句话说,每当输入option参数时,隐藏所有来自(exclusions)的其他选项。以下这一行就是一个例子:
'(-t --text)'{-t,--text}'[read in text mode]'
如果命令行中出现了-t或--text,则不要显示-t或--text选项作为完成项。
这对像ln这样的命令更有意义,你可能想避免提供一些可能误导的选项:
'(-L -P)-H[with -R, follow symlinks on the command line]'
如果使用了-H选项,则隐藏-L和-P选项;这是因为这两个选项分别用于“始终跟随符号链接”和“永不跟随符号链接”。
最后是_md5sum函数的最后一行:
'*:files:_files'
这使用了_files助手函数,这是一个完成文件名的标准工具。通过这一行,我们确保即使没有建议其他选项标志,文件名仍然会被完成。
此外,_files 使用了一个额外的函数_path_files,并将其参数传递给后者。单独看,_path_files 是补全系统中用于完成文件名的事实标准函数。如果这还不够,_path_files 还有一些非常实用的技巧,例如部分路径的补全,这使得像/u/bi/zs这样的路径可以补全为/usr/bin/zsh。
另外,还有一些辅助函数,如_call_program,用于执行系统可用的任何命令。使用_call_program的常见做法是将标准错误重定向到/dev/null(这是一种婉转的方式,表示它会抑制任何由错误引发的错误信息),并允许我们将命令的输出保存到一个变量中。
就这样了。嗯,至少对于开始使用补全机制和自定义函数来说是这样的。虽然在某些情况下,亲自动手扩展补全系统和编写自己的函数只会让你走得更远,但这次快速浏览应该足以让你对那些可能性感到兴奋。再说一次,建议你尽量避免重复造轮子——正如我们将在下一章看到的那样,外面有许多其他项目可以在补全方面给你带来很大的帮助。
现在,你可以深入functions文件夹,开始熟悉你 zsh 安装中的成千上万行代码了。谁知道呢?也许下一个补全函数的起始模板就在那里等着你。
总结
我们几乎完成了这段旅程,现在看起来你比以往任何时候都更准备好应对一些常见的麻烦,比如你最喜欢的程序没有一组补全定义。更棒的是,你还可以调整并改善现有的功能,否则这些问题会让你的工作变得非常让人沮丧。
除了编写自己的函数,我们还学习了如何调整 shell 行为,并进一步提升文件名补全功能。通过一些练习和进一步的调整,你现在可以成为命令行的真正高手。最棒的是,只需要按下几次Tab键就能达到这个水平。
总结一下,本章涉及的内容如下:
-
zsh 提供的补全类型——zstyles 和函数,允许你自定义补全机制的行为并扩展其功能
-
不同类型的补全器(特别是
correct、approximate和ignore)及其在定义 zstyles 时的作用 -
创建和扩展你自己的补全函数的小贴士
好的,那么在我开始变得感性之前,我们应该赶紧进入下一章,那里有一些建议,在我们结束这段旅程之前。
第六章:提示与技巧
所以,这就是我们分道扬镳的地方。自从定义第一个别名以来,我们已经走了很长一段路,所以实际上没有太多东西需要我们去发现,至少在剩下的页面数量来看是这样。不过,就像一个爱管闲事的邻居一样,我忍不住在我们的旅程结束之前再给你一些小贴士。
主要资源
稍等一下,伙计。在你开始输入提示和调整配置之前,我再次要把你引导到 zsh 的官方网站。zsh 的页面位于 www.zsh.org,你可以打开浏览器去查看 常见问题解答 部分以及其他有趣的条目,如 脚本与贡献。原来这是我们了解我们新喜爱的 shell 的主要信息来源,所以我建议你参考它,以便跟上版本更新之间的变化,并查阅那里提供的精彩用户指南和手册。
也许在推荐清单中功能最全的项目就是 zsh wiki (zshwiki.org)。在那里,你会找到关于 zsh 的许多有用信息,附带技巧和用户推荐的配置。总体而言,这是为你的启动文件提供内容的绝佳起点。值得注意的是,这是一个由用户维护的网站,这意味着你可以通过提交自己的配置和脚本以及编辑现有内容来为其做出贡献。
没有一个像 zsh 这样庞大的项目会没有自己的邮件列表。你可以在 www.zsh.org/mla 找到 zsh 的邮件列表,并查看成千上万的有趣讨论,获取更多的提示、技巧和关于该项目的公告。记住,你也可以用它来提问有关 shell 和整个项目的任何问题。如果你打算为这个项目做出贡献,这也是一个很好的起点。
最后,对于那些喜欢“群聊”的人,freenode 上有一个 IRC 频道 (freenode.net),名为 #zsh。这是你获得帮助并与其他 zsh 用户讨论的首选渠道。
帮助提示
接下来是一些“配置文件、别名和函数中可以有的好东西”的清单。可以把这些看作是帮助你完成一些涉及命令行的无聊任务的小助手。
目录替换
这是你可以使用 zsh 实现的其中一个酷炫技巧,尽管它有点隐藏在不显眼的地方。你知道你可以使用 cd 在平行目录之间切换,而无需输入完整路径吗?让我们通过一个例子来演示一下。
假设你位于目录 /zsh/completion/unix/;现在,看看下面的命令:
% cd completion doc
这个命令会将你的当前工作目录有效地移动到 /zsh/doc/unix/,前提是这两个目录具有相同的树形结构并位于同一分支级别。我知道,我也无法想象没有它我会怎么过日子。
记住,你可以设置 AUTOCD 选项,只需输入目录的名称,就能进入该目录,前提是该目录存在且不是模糊匹配。
魔法空格
可以合理地假设到目前为止你已经在使用 Tab 键进行自动补全,但 shell 还提供了一个 magic-space 功能,真的值得绑定到你的空格键上。只需将以下代码添加到你的 .zshrc 文件中:
bindkey ' ' magic-space
然后尝试输入一些内容,并立即按下空格键,如下所示:
% echo !!<Space>
你会立刻注意到这个“魔法”是什么意思,因为现在按下空格键会触发当前行的历史扩展。
随机数
我已经不记得多少次我需要一个真正的随机数来填写表格或做出完全随机的决策,就像那些你无法决定是喝卡布奇诺还是拿铁的时候一样。
让我们借用一下我们的朋友 $RANDOM,并在其上加上一些算术扩展。把所有内容放在一起,我们得到了如下的别名:
alias rand='echo $(( ( RANDOM % 10 ) + 1 ))'
这段代码的作用是使用 $RANDOM 内置函数来获取一个 伪随机 数字。然后我们使用 取模 运算符(%)获取除以 10 的余数;这样,我们就能得到介于 1 和 10 之间的数字。你看到的加上的 1 是因为计算机实际上将 1 到 10 的范围解释为“0-9”,它包含了前十个数字,但对人类来说不太友好。
整个表达式用我们在第二章中学到的算术扩展构造 $(()) 包裹起来,别名与历史,这允许我们操作诸如 $RANDOM 之类的数字。
现在,每次你需要一个真正的随机数输出时,你可以直接输入 rand 在你的终端窗口中。
作为旁注,请记住,像所有计算机生成的内容一样,实际上没有纯粹的“随机”事件——除非你在和我的老板谈论我的某个 bug。那些完全是随机现象——所以不要依赖它用于安全或敏感操作。
zcalc
大多数时候,数学让我们在咖啡因水平过低时感到无助。在这种时候,尝试代数通常需要一个快速的计算器。事实证明,zsh 自带了一个这样的计算器。
它的工作原理类似于 tetris 和 zle 模块;只需将 autoload -Uz zcalc 添加到你的 .zshrc 文件中,然后在终端模拟器中输入 zcalc 以便在需要时使用。要退出 zcalc,只需按 Ctrl + D。
更改并列出目录内容
就像许多其他使用 shell 的用户一样,大部分时间你会在目录之间切换并列出它们的内容。可以合理地假设,在你的正常工作流程中,你会经常调用cd和ls命令。
看一下下面的示例:
% cd some_dir
> ~/gfestari/somedir/
% ls
> file1.txt file2.txt
别担心,亲爱的读者,你并不孤单。大多数 shell 用户都感同身受。幸运的是,我们可以做一些事情来解决这个问题,其中包括一个简单的函数,通过 cd 改变当前工作目录,然后调用 ls 来列出新目录的内容,代码如下所示:
# calls cd, and immediately list its contents
function cs {
cd "$@" && ls -A
}
我们的新 cs 函数将像 cd 一样工作,但会列出我们切换到的任何目录的内容。你看到的 $@ 字符串是我们调用 cs 时使用的当前命令参数。它们会完整地传递给 cd,所以我们不需要像处理实际程序那样精细地处理它们。然后,我们使用双重与运算符 &&(读作“and”)将 ls 命令与 -A 选项串联起来。这就像“执行 cd,如果成功,则调用 ls -A”。
将此添加到你的启动文件中,开始通过输入 cs 来切换目录。
通过命令查找你的路径
我们已经在本书中多次使用了 which,但现在是时候学习另一个酷炫的 zsh 特性了,它来自于命令替换机制:=command 快捷方式。
尝试以下命令行,它应该能指向 zsh 的二进制文件位置:
% echo $(which zsh)
> /usr/local/bin/zsh
现在,让我们尝试使用等效的快捷方式:
% echo =zsh
> /usr/local/bin/zsh
只要你记得在等号后立即输入系统中任何程序的名称,这个方法就能像 which 一样工作,但需要的输入要少得多。
其他项目
本节旨在引导你发现一些最有趣的项目和资源。这些的核心目的是为你的 zsh 增添一些“额外的调味料”。
zsh-lovers
zsh-lovers 项目 (grml.org/zsh/zsh-lovers.html) 是一个有用的技巧、窍门和示例集合,可以作为手册页面安装,并通过终端访问。这个项目的一个有趣特点是它收集了许多 zsh 中的“隐藏”功能(或者说不太显眼的功能)的示例。如果仅仅是节省数百小时的在线搜索时间,这些内容就值得每一个字节。
zsh-users
GitHub 上的 zsh 用户仓库 (github.com/zsh-users) 包含了许多极其有用的代码。任何 zsh 用户特别感兴趣的项目有 zsh-syntax-highlighting (github.com/zsh-users/zsh-syntax-highlighting) 和 zsh-history-substring-search (github.com/zsh-users/zsh-history-substring-search)。
正如名字所暗示的,zsh-syntax-highlighting 提供了类似于 fish shell 中的语法高亮,而 zsh-history-substring-search 再次借鉴了 fish 的功能,通过允许你输入历史记录条目的任何部分并按上下箭头键来切换匹配的命令,从而进行历史搜索。
在 zsh 用户的代码库中,还有zsh-completions项目 (github.com/zsh-users/zsh-completions),这是一个由社区提交的、为许多流行程序和工具(如 Node.js、Redis 和 Vagrant)提供补全功能的集合。
oh-my-zsh
除非你在过去的几年里都处于离线状态,否则你很可能已经听说过 oh-my-zsh (github.com/robbyrussell/oh-my-zsh)。这个社区驱动的项目通过简化 zsh 的初始配置和学习曲线,帮助 zsh 变得异常流行。这个框架包含了超过一百个插件,涵盖了如 Ruby on Rails、Git 和 Ant 等工具,还有一大堆提示主题;让命令行永远不再无聊。
Prezto
Prezto (github.com/sorin-ionescu/prezto) 是另一个流行的项目,提供了一些很棒的配置选项。像 oh-my-zsh 一样,Prezto 包含了它所谓的“理智默认配置”,一些有趣的别名和功能,加上自动补全和——你猜对了——提示主题。
好的,我听到了。我的 shell真的需要一个框架吗?事实是,你可能不需要整个包,只需要某个特定的功能,无论是补全功能还是提示样式。那么,既然有人已经想到了这个问题并提出了——希望是优雅的——解决方案,为什么要重新发明轮子呢?我想说的是:看看源代码,看看你能把什么融入到你的配置中,如果你愿意,可以将其回馈给社区。下一个人肯定会非常感激的。
Explain Shell
虽然不完全是 zsh 相关的,Explain Shell 项目 (explainshell.com) 旨在通过提供一个非常整洁的界面,逐词解析并解释那些极其尴尬的命令,从而帮助你解决问题。当你在试验陌生命令或网络深处找到的东西时,这会非常有用。
你的 dotfiles
注意到你的程序配置文件默认都是隐藏的吗?即使是你的启动文件和与 zsh 相关的配置,也会通过文件名前面的点隐藏在你的主目录中。通常被称为dotfiles,其中有很多很酷的设置和配置,它们最初是某人巧妙解决烦恼的尝试。所以,尽管去发布你的 dotfiles 让全世界看到吧。事实上,分享你的配置是帮助其他用户进行 zsh 冒险的一个很好的方式,并且还能获得别人对你所做的工作的反馈。只是,记得小心不要在分享时泄露任何密码或凭证!
如果在读完本书后,你想了解的另一部书,那应该是 《从 Bash 到 Z Shell:征服命令行》,作者为 Oliver Kiddle、Peter Stephenson 和 Jerry Peek。这本几乎是经典之作,无论是对初学者还是高级用户来说,都能帮助你扩展对命令行的知识。
总结
这也标志着本书的结束。注意我写的是“书”而不是“旅程”,因为希望这次初次接触 zsh 能让你对这个 shell 的潜力以及它作为工具的多样性充满兴趣。
那么接下来怎么办呢?幸运的是,这取决于你,亲爱的读者。zsh 还有很多内容等待你去探索,此外还有许多烦人的、枯燥的任务等着你去完成,这样你就能回到那些积压的重要事务上。
经过一些微调,尤其是在配置方面,zsh 真正能够大放异彩,让你的命令行操作更加轻松——甚至充满乐趣——那为什么不呢?所以,继续深入学习吧,你一定会感谢自己的。


浙公网安备 33010602011771号