在讨论不同的选项和配置之前,让我们先关注一些基本术语,比如终端和shell。在本节中,我将定义术语,并展示如何在shell中完成日常任务。我们还将回顾现代命令,并了解它们的实际操作。
我们首先介绍终端(有时也称为终端模拟器或软终端):终端是提供文本用户界面的程序。也就是说,终端支持从键盘读取字符并显示在屏幕上。许多年前,终端是集成设备(键盘和屏幕结合在一起),但现在终端只是简单的应用程序。
除了基本的面向字符的输入和输出之外,终端还支持所谓的转义序列或转义码( https://oreil.ly/AT5qC ),用于光标和屏幕处理,并可能支持颜色。例如,按Ctrl+H会导致退格,删除光标左侧的字符。
环境变量TERM使用了终端模拟器,它的配置可以通过infocmp获得,如下所示(注意输出被缩短了):
❶infocmp的输出不容易理解。如果你想详细了解这些功能,请查阅terminfo( https://oreil.ly/qjwiv )数据库。例如,在我的具体输出中,终端支持80列(cols#80)和24行(lines#24)输出以及256种颜色(colors#0x100,以十六进制表示)。
终端的例子不仅包括xterm、rxvt和Gnome,还包括利用GPU的新一代终端,例如Alacritty( https://oreil.ly/zm9M9 )、kitty( https://oreil.ly/oxyMn )、warp( https://oreil.ly/WBG9S )。
在3.3节中,我们将回到终端的主题。
接下来是shell,它是一个运行在终端内部的程序,充当命令解释器。shell通过流提供输入和输出处理,支持变量,有一些你可以使用的内置命令,处理命令执行和状态,通常支持交互式使用和脚本使用(见3.4节)。
shell形式上用sh( https://oreil.ly/ISxwU )定义,我们经常遇到术语POSIX shell( https://oreil.ly/rkfqG ),它在脚本和可移植性上下文中变得更加重要。
最初,我们使用Bourne shell sh(以作者的名字命名),但现在它通常被bash( https://oreil.ly/C9coL )shell所取代——这是原始版本的文字游戏,是“Bourne Again Shell”的缩写——它被广泛用作默认值。
如果你对正在使用的版本感到好奇,那么可以使用file -h /bin/sh命令来找出答案;如果失败,那么可以尝试echo $0或echo $SHELL。
在本节中,我们默认使用bash shell(bash),除非特别说明。
sh还有更多的实现以及其他变体,例如Korn shell(ksh)和C shell(csh),它们目前还没有被广泛使用。但是,我们将在3.2节中回顾现代bash替代品。
让我们先从两个基本特性来了解shell基础知识:流和变量。
让我们从输入(流)和输出(流)—或者简称为I/O的主题开始。如何为程序提供一些输入?如何控制程序输出的位置,比如在终端上还是在文件中?
首先,shell为每个进程提供了三个默认的文件描述符(FD),用于输入和输出:
·stdin(FD 0)
·stdout(FD 1)
·stderr(FD 2)
如图3-2所示,默认情况下,这些FD分别连接到屏幕和键盘。换句话说,除非指定其他内容,否则在shell中输入的命令将从键盘获取输入(stdin),并将输出(stdout)传递到屏幕。
下面的shell交互演示了这种默认行为:
在上面使用cat的示例中,可以看到默认值的作用。注意,我使用Ctrl+C(显示为^C)来终止命令。
图3-2:shell I/O默认流
如果你不想使用shell提供的默认值——例如,你不想在屏幕上输出stderr,但希望将其保存在文件中——那么你可以重定向( https://oreil.ly/pOIjp )流。
你可以使用$FD>和<$FD重定向进程的输出流,其中$FD是文件描述符——例如,2>表示重定向stderr流。注意,1>和>是相同的,因为stdout是默认值。如果你想重定向stdout和stderr,则可以使用&>。当你想摆脱一个流时,你可以使用/dev/null。
下面通过一个具体的例子来看看它是如何工作的——通过curl下载一些HTML内容:
❶通过将stdout和stderr重定向到 /dev/null 来丢弃所有输出。
❷将输出和状态重定向到不同的文件。
❸交互式进入输入并保存到文件,使用Ctrl+D停止捕获并存储内容。
❹所有单词小写,使用从stdin读取的tr命令。
shell通常理解一些特殊字符,例如:
与字符 (&)
置于命令的末尾,在后台执行命令(参见本节后面的“作业控制”)。
反斜杠 (\)
用于继续下一行的命令,以提高长命令的可读性。
管道连接符 (|)
连接一个进程的stdout与下一个进程的stdin,允许你传递数据,而不必将其存储在作为临时位置的文件中。
虽然管道( https://oreil.ly/ipSgr )乍一看似乎不怎么样,但它们还有更多的功能。我曾经和管道的发明者Doug McIlroy有过一次很好的交流。我写过一篇文章“Revisiting the UNIX Philosophy in 2018”( https://oreil.ly/KTU4q ),在文章中,我将UNIX和微服务进行了比较。有人对这篇文章进行了评论,这条评论导致Doug给我发了一封电子邮件(非常意外,我必须验证才能相信它)来澄清一些事情。
下面我们还是看看一些理论内容的实际应用。首先尝试通过curl下载HTML文件,然后将内容输送到wc工具来计算HTML文件包含多少行:
❶使用curl从URL下载内容,并丢弃它在stderr上输出的状态。(注意:在实践中,你会使用curl的-s选项,但我们想学习如何应用这来之不易的知识,对吗?)
❷curl的stdout被提供给wc的stdin,后者使用-l选项计算行数。
现在你已经对命令、流和重定向有了基本的了解,让我们转向另一个核心shell特性,即变量的处理。
在shell上下文中经常遇到的一个术语是变量。当你不想或不能硬编码值时,你可以使用变量来存储和更改值。用例包括以下内容:
·当你想要处理Linux公开的配置项时,例如,shell查找在$PATH变量中捕获的可执行文件的位置。这是一种可以读/写变量的接口。
·当你想交互式地查询用户的值时,例如,在一个脚本的上下文中。
·当你想通过一次定义一个长值来缩短输入时,例如,HTTP API的URL。这个用例大致对应于程序语言中的const值,因为在声明变量之后不需要更改该值。
我们区分两种变量:
环境变量
shell范围设置,用env列出它们。
shell变量
在当前执行的上下文中有效,在bash中用set列出。子进程不能继承shell变量。
在bash中,可以使用export创建环境变量。当你想访问一个变量的值时,在它前面放一个$;当你想摆脱它时,使用unset。
好的,内容很多。让我们看看这在实践中是怎样的(在bash中):
❶创建一个名为MY_VAR的shell变量,并赋值为42。
❷列出shell变量并过滤掉MY_VAR。注意,_=表示它没有被导出。
❸创建一个名为MY_GLOBAL_VAR的新环境变量。
❹列出shell变量并过滤掉所有以MY_开头的变量。正如预期的那样,我们看到了在前面步骤中创建的两个变量。
❺列出环境变量。我们看到了MY_GLOBAL_VAR,正如我们所希望的那样。
❻创建一个新的shell会话,即当前shell会话的子进程,它不继承MY_VAR。
❼访问环境变量MY_GLOBAL_VAR。
❽列出shell变量,它只提供MY_GLOBAL_VAR,因为我们在一个子进程中。
❾退出子进程,删除MY_VAR shell变量,并列出我们的shell变量。正如预期的那样,MY_VAR消失了。
在表3-1中,我将常见的shell变量和环境变量放在一起。你会发现这些变量几乎无处不在,理解和使用它们很重要。对于任何变量,你都可以使用echo $XXX查看各自的值,其中XXX是变量名。
表3-1:常见的shell和环境变量
此外,请查看bash特定变量的完整列表( https://oreil.ly/EIgVc ),并注意表3-1中的变量将在3.4节中再次派上用场。
shell使用所谓的退出状态将命令执行的完成传递给调用者。通常,Linux命令在终止时应该返回一个状态。这可以是一个正常的终止(愉快的路径),也可以是一个异常的终止(某些地方出了问题)。0退出状态表示命令成功运行,没有任何错误,而1~255之间的非零值表示失败。可以使用echo $?命令查询退出状态。
注意管道中的退出状态处理,因为有些shell只提供最后一个状态。你可以通过使用$PIPESTATUS( https://oreil.ly/mMz9k )来解决这个限制。
shell带有许多内置命令。一些有用的例子是yes、echo、cat或read(取决于Linux发行版,其中一些命令可能不是内置的,而是位于 /usr/bin )。你可以使用help命令列出内置程序。但是,请记住,其他所有内容都是shell外部程序,通常可以在 /usr/bin (用于用户命令)或 /usr/sbin (用于管理命令)中找到。
你如何知道在哪里可以找到可执行文件?这里有一些方法:
本书的一位技术审校者正确地指出,这是一个非POSIX的外部程序,可能并不总是可用的。此外,他们建议使用“命令-v”(而不是which)来获取程序路径或shell别名/函数。请参见shellcheck文档( https://oreil.ly/5toUM )来了解更多细节。
大多数shell支持的一个特性称为作业控制( https://oreil.ly/zeMsU )。默认情况下,当你输入一个命令时,它会控制屏幕和键盘,我们通常称之为在前台运行。但是,如果你不想以交互方式运行某些东西,或者在服务器的情况下,如果根本没有来自stdin的输入,该怎么办?输入作业控制和后台作业:要在后台启动进程,需要在最后加上&,或将前台进程发送到后台,按Ctrl+Z。
下面的例子展示了这一点,让你对作业控制有大致了解:
❶通过将&放在末尾,我们可以在后台启动该命令。
❷列出所有的作业。
❸使用fg命令,我们可以将进程放到前台。如果要退出watch命令,请使用“Ctrl+C”。
如果希望保持后台进程运行,那么即使在关闭shell之后,也可以预先使用nohup命令。此外,对于已经在运行且未预先添加nohup的进程,可以在事后使用disown来实现相同的效果。最后,如果你想摆脱一个正在运行的进程,那么你可以使用不同级别的强制kill命令(更多细节请参阅9.1.1节)。
我建议使用终端多路复用器,而不是作业控制,如3.3节所述。这些程序处理最常见的用例(shell关闭、多个进程运行和需要协调等),还支持与远程系统一起工作。
让我们继续讨论常用核心命令的现代替代品,这些核心命令一直存在。
你会发现自己每天都会反复使用一些命令。这些命令包括导航目录(cd)、列出目录内容(ls)、查找文件(find)和显示文件内容(cat、less)。考虑到经常使用这些命令,你希望尽可能高效——每一次按键都很重要。
这些经常使用的命令有一些现代变体,其中一些是临时替换,其他的则扩展了功能。它们都为常见操作提供了合理的默认值和丰富的输出,通常更容易理解,并且它们通常会导致你在完成相同的任务时输入更少。这减少了你使用shell时的摩擦,使它更令人愉快,并改善了流程。如果你想了解更多关于现代工具的知识,请查看附录B。在这种情况下,需要注意的是(特别是如果你要在企业环境中应用这些知识):我与这些工具并没有任何利害关系,推荐它们只是因为我自己觉得它们很有用。安装和使用这些工具的关键是使用经过Linux发行版审查的工具版本。
只要你想知道目录包含了什么,就要使用ls或其带有参数的变体之一。例如,在bash中,我曾经将l别名为ls -GAhltr。但还有更好的方法:exa( https://oreil.ly/5lPAl ),ls的现代替代品,用Rust编写,内置了对Git和树渲染的支持。在这种情况下,在列出目录内容之后,你猜最常用的命令是什么?根据我的经验,该命令是人们为了清除屏幕而经常使用的clear。那就是输入五个字符,然后按回车键。你可以更快地达到同样的效果——只需使用Ctrl+L。
假设你列出了一个目录的内容,并找到了一个想要检查的文件。也许你会用cat,但这里还有更好的推荐:bat( https://oreil.ly/w3K76 )。bat命令,如图3-3所示,带有语法高亮显示,显示不可输出的字符,支持Git,并具有集成的分页器(逐页查看比屏幕上显示的长度更长的文件)。
图3-3:用bat渲染一个Go文件(上)和一个YAML文件(下)
传统上,你将使用grep在文件中查找内容。然而,有一个现代命令rg( https://oreil.ly/u3Sfw ),它快速而强大。
在这个例子中,我们将rg与find和grep的组合进行比较,我们想要找到包含字符串“sample”的YAML文件:
❶使用find和grep一起在YAML文件中查找字符串。
❷使用rg完成相同的任务。
如果比较上述示例中的命令和结果,就会发现rg不仅更容易使用,而且结果的信息量也更大(提供上下文,在本例中是行号)。
现在介绍一个额外的命令。这个jq并不是一个真正的替代品,而更像是JSON(一种流行的文本数据格式)的专用工具。你可以在HTTP API和配置文件中找到JSON。
因此,使用jq( https://oreil.ly/9s7yh )而不是awk或sed来挑选特定的值。例如,通过使用JSON生成器( https://oreil.ly/bcT9d )生成一些随机数据,我有一个2.4KB的JSON文件 example.json ,看起来像这样(这里只显示第一条记录):
假设我们对所有“第一个”朋友感兴趣—friends数组中的第0项——喜欢“草莓”水果的人。使用jq,你可以这样做:
这就是CLI的乐趣。如果你有兴趣了解有关现代命令这一主题的更多信息,以及你可以替换的其他候选命令,请查看modern-unix仓库( https://oreil.ly/cBAXt ),其中列出了建议。现在让我们将注意力转移到目录导航和文件内容查看之外的一些常见任务,以及如何进行这些任务。
有许多事情你可能会发现自己经常做,并且可以使用某些技巧来加快完成shell中的任务。让我们回顾一下这些常见的任务,看看如何才能更有效率。
一个基本观点是,你经常使用的命令应该花费最少的时间——它们应该可以快速输入。现在将这个想法应用到shell:我输入d(单个字符)而不是git diff --color-moved,因为我每天要数百次查看存储库中的更改。根据shell的不同,有不同的方法来实现这一点:在bash中,这称为别名( https://oreil.ly/fbBvm ),而在Fish(见3.2.1节)中可以使用缩写( https://oreil.ly/rrmNI )。
当你在shell提示符上输入命令时,可能需要做许多事情,例如,导航行(例如,将光标移动到开头)或操作行(例如,删除光标左侧的所有内容)。常见的shell快捷方式如表3-2所示。
表3-2:shell导航和编辑快捷方式
表3-2:shell导航和编辑快捷方式(续)
请注意,并非所有shell都支持所有这些快捷方式,某些操作(如历史管理)在某些shell中实现方式可能不同。此外,这些快捷键是基于Emacs编辑按键的。如果你更喜欢vi,那么你可以在 .bashrc 文件中使用set -o vi,例如,根据vi按键来进行命令行编辑。最后,根据表3-2,尝试你的shell支持哪些,并查看如何配置它以满足你的需求。
你并不总是希望启动vi之类的编辑器来添加一行文本。有时你不能这样做,例如在编写shell脚本的上下文中(见3.4节)。
那么,如何操作文本内容呢?让我们来看几个例子:
❶通过重定向echo输出创建一个文件。
❷查看文件内容。
❸使用>>操作符将一行追加到文件,然后查看内容。
❹使用sed替换文件中的内容并输出到stdout。
❺使用这里的文档( https://oreil.ly/FPWqT )创建一个文件。
❻显示我们创建的文件之间的差异。
现在你已经了解了基本的文件内容操作技术,接下来让我们看看文件内容的高级用法。
对于长文件(也就是说,文件的行数超过了shell在屏幕上显示的行数),可以使用less或bat(bat带有内置换页程序)之类的命令。通过分页,程序将输出拆分成页,其中每页都包含适合屏幕显示的内容,以及一些导航页面的命令(查看下一页、前一页等)。处理长文件的另一种方法是只显示文件的一个选定区域,比如前几行。有两个方便的命令:head和tail。
例如,要显示文件的开头:
❶创建一个长文件(此处为100行)。
❷显示长文件的前五行。
或者,要获得一个不断增长的文件的实时更新,我们可以使用:
❶使用tail显示日志文件的结尾,其中-f选项表示跟随或自动更新。
在本节的最后,我们将讨论如何处理日期和时间。
date命令是生成唯一文件名的有用方法。它允许你生成各种格式的日期,包括UNIX时间戳( https://oreil . ly/xB7UG ),以及在不同的日期和时间格式之间进行转换。
❶创建UNIX时间戳。
❷将UNIX时间戳转换为人类可读的日期。
UNIX纪元时间(或简称UNIX时间)是自1970-01-01T00:00:00Z以来经过的秒数。UNIX时间将每天精确地视为86 400秒。
如果你正在处理将UNIX时间存储为有符号32位整数的软件,那么你可能需要注意,因为这会导致在2038-01-19上出现问题,因为计数器将溢出,这也称为2038年问题( https://oreil.ly/dKiWx )。
你可以使用在线转换器( https://oreil.ly/Z1a4A )进行更高级的操作,支持微秒和毫秒分辨率。
至此,我们结束了shell基础部分。到目前为止,你应该已经很好地理解了什么是终端和shell,以及如何使用它们来完成基本任务,如导航文件系统、查找文件等。现在我们转到对我们友好的shell主题。