Linux-命令行高效编程-全-
Linux 命令行高效编程(全)
原文:
zh.annas-archive.org/md5/4d74281c920bb9a9ca9b9eb2a2984c72译者:飞龙
前言
这本书将带领你的 Linux 命令行技能迈向一个新的高度,让你能够更快、更智能、更高效地工作。
如果你和大多数 Linux 用户一样,在工作中学习了你早期的命令行技能,或者通过阅读一本介绍书,或者在家里安装 Linux 然后试试各种东西。我写这本书是为了帮助你迈出下一步——在 Linux 命令行上建立中级到高级的技能。它充满了我希望能够改变你与 Linux 交互方式并提升你的生产力的技术和概念。把它看作是一本关于 Linux 使用的第二本书,让你超越基础知识。
命令行是最简单的界面,但也是最具挑战性的。它简单是因为你只看到一个提示符,等待你运行任何你可能知道的命令:^(1)
$
它具有挑战性,因为提示符之外的一切都取决于你。没有友好的图标、按钮或菜单来指导你。相反,你输入的每个命令都是一种创造性的行为。这对基本命令如列出你的文件也是如此:
$ ls
还有像这样复杂命令:
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g') \
| sed 's/^/mv /' \
| bash
如果你正盯着上述命令想,“那是什么*?”或者“我永远不需要这样一个复杂的命令”,那么这本书适合你。^(2)
你将学到什么
这本书将让你在三个关键技能上更快更有效:
-
选择或构建命令来解决手头的业务问题
-
运行这些命令的高效性
-
轻松导航 Linux 文件系统
最终,你将了解在运行命令时发生的背后情况,这样你就能更好地预测结果(而不是养成迷信)。你将看到十几种不同的启动命令的方法,并学会何时使用每种方法以取得最佳效果。你还将学到一些实用的技巧和窍门,让你的工作更加高效,例如:
-
逐步从更简单的命令构建复杂的命令,以解决实际问题,如管理密码或生成一万个测试文件
-
通过智能组织你的主目录来节省时间,这样你就不必寻找文件
-
转换文本文件并像数据库一样查询它们以实现业务目标
-
控制 Linux 命令行的点和点击功能,如使用剪贴板进行复制和粘贴,以及检索和处理 Web 数据,而无需从键盘上抬起你的手
最重要的是,你将学到通用的最佳实践,无论你运行哪些命令,你都可以在日常 Linux 使用中取得更多成功,并在职场上更有竞争力。这本书就是我学习 Linux 时希望拥有的书籍。
这本书不是什么
这本书不会优化你的 Linux 计算机以使其更有效地运行。它会让你在与 Linux 的交互中更加高效。
本书也不是命令行的全面参考资料——有数百个命令和功能我没有提到。本书侧重于专业技能。它按照实用顺序精选了一组命令行知识。要获取参考式指南,请尝试我的早前著作 Linux Pocket Guide(O’Reilly)。
受众和先决条件
本书假设您具有 Linux 使用经验;这不是入门指南。适合希望提升命令行技能的用户,比如学生、系统管理员、软件开发人员、站点可靠性工程师、测试工程师和 Linux 爱好者。高级 Linux 用户也许也能从中找到一些有用的资料,特别是那些希望通过实践运行命令来加深概念理解的用户。
要从本书中获得最大的收益,您应该已经对以下主题感到熟悉(如果不熟悉,请参见附录 A 进行快速复习):
-
使用文本编辑器(如
vim(vi)、emacs、nano或pico)创建和编辑文本文件 -
基本的文件处理命令,比如
cp(复制)、mv(移动或重命名)、rm(删除)、chmod(修改文件权限) -
基本的文件查看命令,比如
cat(查看整个文件)和less(逐页查看) -
基本的目录命令,比如
cd(切换目录)、ls(列出目录中的文件)、mkdir(创建目录)、rmdir(删除目录)和pwd(显示当前目录名称) -
Shell 脚本的基础知识:将 Linux 命令存储在文件中,使文件可执行(使用
chmod 755或chmod +x),然后运行该文件 -
使用
man命令查看 Linux 内置文档(称为 man 页面)(例如:man cat显示关于cat命令的文档) -
使用
sudo命令成为超级用户,以完全访问您的 Linux 系统(例如:sudo nano /etc/hosts编辑系统文件 /etc/hosts,普通用户无法访问)
如果您还熟悉常见的命令行功能,比如用于文件名的模式匹配(使用*和?符号)、输入/输出重定向(<和>)和管道(|),您已经迈出了良好的开端。
您的 Shell
我假设您的 Linux Shell 是bash,这是大多数 Linux 发行版的默认 Shell。每当我提到“shell”时,我指的是bash。本书中提到的大多数概念也适用于其他 shell,比如zsh或dash;参见附录 B 来帮助将本书的示例翻译成其他 shell 的语法。很多内容在苹果 Mac 终端上也能无需修改地运行,苹果 Mac 终端默认运行zsh,也可以运行bash。^(3)
本书使用的约定
本书中使用以下排版约定:
斜体
指示新术语、URL、电子邮件地址、文件名和文件扩展名。
等宽字体
用于程序列表,以及在段落内引用程序元素,例如变量或函数名、数据库、数据类型、环境变量、语句和关键字。
Constant width bold
显示用户应按字面意思键入的命令或其他文本。也偶尔在命令输出中用于突出显示感兴趣的文本。
Constant width italic
显示应由用户提供值或由上下文确定值替换的文本。也用于代码列表右侧的简要注释。
Constant width highlighted
用于复杂程序列表中以引起特定文本注意的标记。
Tip
这个元素表示提示或建议。
Note
这个元素表示一般注释。
Warning
这个元素表示警告或注意事项。
使用代码示例
补充材料(代码示例、练习等)可在https://efficientlinux.com/examples下载。
如果你有技术问题或者在使用代码示例时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
本书旨在帮助您完成工作。一般情况下,如果本书提供示例代码,则可以在您的程序和文档中使用它。除非您复制了代码的大部分内容,否则您无需联系我们以获取权限。例如,编写一个使用本书中几个代码块的程序不需要权限。销售或分发来自 O’Reilly 图书的示例代码则需要权限。通过引用本书并引用示例代码回答问题不需要权限。将本书中大量示例代码整合到产品文档中则需要权限。
我们欣赏,但通常不需要署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Efficient Linux at the Command Line by Daniel J. Barrett (O’Reilly)。版权所有 2022 Daniel Barrett, 978-1-098-11340-7。”
如果您认为您使用的代码示例超出了合理使用范围或以上述许可证给出的权限,请随时通过permissions@oreilly.com联系我们。
O’Reilly Online Learning
Note
超过 40 年来,O’Reilly Media 提供技术和商业培训、知识和洞见,帮助公司取得成功。
我们独特的专家和创新者网络通过书籍、文章和我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深度学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。更多信息,请访问https://oreilly.com。
如何联系我们
请将关于本书的评论和问题发送至出版社:
-
O’Reilly Media, Inc.
-
1005 Gravenstein Highway North
-
Sebastopol, CA 95472
-
800-998-9938(美国或加拿大境内)
-
707-829-0515(国际或本地)
-
707-829-0104(传真)
我们有这本书的网页,列出勘误、示例和任何其他信息。您可以访问https://oreil.ly/efficient-linux。
请发送电子邮件至bookquestions@oreilly.com,就本书的评论或技术问题发表意见。
关于我们的书籍和课程的新闻和信息,请访问https://oreilly.com。
在 Facebook 上找到我们:https://facebook.com/oreilly
在 Twitter 上关注我们:https://twitter.com/oreillymedia
在 YouTube 上观看我们:https://www.youtube.com/oreillymedia
致谢
书写这本书是一种快乐。感谢 O'Reilly 的出色团队,特别是编辑 Virginia Wilson 和 John Devins,制作编辑 Caitlin Ghegan 和 Gregory Hyman,内容经理 Kristen Brown,副本编辑 Kim Wimpsett,索引编辑 Sue Klefstad,以及永远乐于助人的工具团队。我也非常感谢本书的技术审阅者 Paul Bayer、John Bonesio、Dan Ritter 和 Carla Schroder,他们提供了许多有见地的评论和批评。还要感谢波士顿 Linux 用户组提供的书名建议。特别感谢 Google 的 Maggie Johnson,她慷慨允许我撰写这本书。
我要深深感谢 35 年前在约翰斯·霍普金斯大学的同学 Chip Andrews、Matthew Diaz 和 Robert Strandh。他们注意到我对 Unix 的新兴兴趣,并且出乎我的意料,推荐计算机科学系聘请我作为下一任系统管理员。他们这一小小的信任举动改变了我生活的轨迹。(Robert 还因在第三章中关于打字速度提示而获得赞誉。)也感谢 Linux、GNU Emacs、Git、AsciiDoc 以及许多其他开源工具的创造者和维护者们——如果没有这些聪明和慷慨的人们,我的职业生涯将会截然不同。
一如既往,感谢我美好的家人 Lisa 和 Sophia,他们的爱和耐心。
^(1) 本书将 Linux 提示符显示为一个美元符号。你的提示符可能有所不同。
^(2) 你将在第八章中了解这个神秘命令的用途。
^(3) macOS 上的bash版本古老且缺少重要功能。要升级bash,请参阅 Daniel Weibel 的文章“Upgrading Bash on macOS”。
第一部分:核心概念
前四章的目标是快速提高您的效率,涵盖应立即有用的概念和技术。您将学会如何使用管道组合命令,了解 Linux shell 的职责,快速回忆和编辑过去的命令,并以极快的速度浏览 Linux 文件系统。
第一章:组合命令
当你在 Windows、macOS 和大多数其他操作系统中工作时,你可能会花费大量时间运行诸如网页浏览器、文字处理器、电子表格和游戏等应用程序。典型的应用程序包含大量功能:设计师认为用户需要的一切。因此,大多数应用程序是自给自足的。它们不依赖于其他应用程序。你可能偶尔在应用程序之间复制粘贴,但基本上它们是独立的。
Linux 命令行与众不同。与具有大量功能的大型应用程序不同,Linux 提供了数千个功能很少的小命令。例如,命令cat只是在屏幕上打印文件而已。ls列出目录中的文件,mv重命名文件,依此类推。每个命令都有一个简单而相当明确的目的。
如果你需要做一些更复杂的事情怎么办?别担心。Linux 可以很容易地组合命令,使它们各自的功能一起工作,以实现你的目标。这种工作方式会让你对计算机有非常不同的思维方式。不再是问“我应该启动哪个应用程序?”来实现某个结果,而是变成了“我应该组合哪些命令?”
在这一章中,你将学习如何以不同的组合方式安排和运行命令来完成你的需求。为了保持简单,我将介绍只有六个 Linux 命令及其最基本的用法,这样你可以专注于更复杂和有趣的部分——命令的组合,而无需经历陡峭的学习曲线。这有点像只用六种成分学习烹饪,或者只用锤子和锯子学习木工。(在第五章中,我会向你的 Linux 工具箱添加更多命令。)
你将使用管道来组合命令,这是 Linux 的一个功能,将一个命令的输出连接到另一个命令的输入。在我介绍每个命令(wc、head、cut、grep、sort和uniq)时,我将立即演示它们与管道的使用。有些示例对日常 Linux 使用很实用,而其他示例只是演示一个重要功能的玩具示例。
输入、输出和管道
大多数 Linux 命令从键盘读取输入,将输出写入屏幕,或者两者兼而有之。Linux 为这种读取和写入赋予了很多花哨的名字:
stdin(读作“标准输入”或“标准输入”)
Linux 从你的键盘读取的输入流。当你在提示符下输入任何命令时,你就是在 stdin 上提供数据。
stdout(读作“标准输出”或“标准输出”)
Linux 写入到你的显示器的输出流。当你运行ls命令来打印文件名时,结果就会显示在 stdout 上。
现在来看看有趣的部分。你可以将一个命令的 stdout 连接到另一个命令的 stdin,这样第一个命令就会向第二个命令输入数据。我们从熟悉的ls -l命令开始,以长格式列出一个大目录,比如 /bin:
$ ls -l /bin
total 12104
-rwxr-xr-x 1 root root 1113504 Jun 6 2019 bash
-rwxr-xr-x 1 root root 170456 Sep 21 2019 bsd-csh
-rwxr-xr-x 1 root root 34888 Jul 4 2019 bunzip2
-rwxr-xr-x 1 root root 2062296 Sep 18 2020 busybox
-rwxr-xr-x 1 root root 34888 Jul 4 2019 bzcat
⋮
-rwxr-xr-x 1 root root 5047 Apr 27 2017 znew
这个目录包含的文件比你的显示器能显示的行数多得多,因此输出很快就会滚动到屏幕外。ls 无法一次打印信息,直到你按下键盘继续。但等等:另一个 Linux 命令有这个功能。less 命令以一页一页地显示文件:
$ less myfile *View the file; press q to quit*
你可以连接这两个命令,因为 ls 输出到 stdout,而 less 可以从 stdin 读取。使用管道将 ls 的输出发送到 less 的输入:
$ ls -l /bin | less
这个组合命令一次显示目录的内容一页一页。命令之间的竖线 (|) 是 Linux 的管道符号。^(1) 它连接第一个命令的 stdout 到下一个命令的 stdin。任何包含管道的命令行被称为 管道。
命令通常不知道它们是管道的一部分。ls 认为它在写入显示器,而实际上它的输出已被重定向到 less。而 less 则认为它从键盘读取输入,而实际上它正在读取 ls 的输出。
开始学习的六个命令
管道是 Linux 专家不可或缺的一部分。让我们通过一小组 Linux 命令来提升你的管道技能,这样无论你以后遇到哪些命令,你都能准备好将它们组合起来使用。
这六个命令——wc、head、cut、grep、sort 和 uniq——有许多选项和操作模式,我将大部分跳过,专注于管道。要了解任何命令的更多信息,请运行 man 命令以显示完整文档。例如:
$ man wc
为了演示我们的六个命令的作用,我将使用一个名为 animals.txt 的文件,其中列出了一些 O’Reilly 书籍信息,显示在 示例 1-1 中。
示例 1-1. animals.txt 文件内部
python Programming Python 2010 Lutz, Mark
snail SSH, The Secure Shell 2005 Barrett, Daniel
alpaca Intermediate Perl 2012 Schwartz, Randal
robin MySQL High Availability 2014 Bell, Charles
horse Linux in a Nutshell 2009 Siever, Ellen
donkey Cisco IOS in a Nutshell 2005 Boney, James
oryx Writing Word Macros 1999 Roman, Steven
每行包含有关 O'Reilly 书籍的四个事实,由单个制表符分隔:封面上的动物、书名、出版年份和第一作者的姓名。
命令 #1: wc
wc 命令会打印文件中的行数、单词数和字符数:
$ wc animals.txt
7 51 325 animals.txt
wc 报告文件 animals.txt 有 7 行,51 个单词和 325 个字符。如果你用眼睛数字符,包括空格和制表符,你会发现只有 318 个字符,但 wc 还包括每行结尾的不可见换行符。
选项 -l、-w 和 -c 指示 wc 只打印行数、单词数和字符数:
$ wc -l animals.txt
7 animals.txt
$ wc -w animals.txt
51 animals.txt
$ wc -c animals.txt
325 animals.txt
计数是一项非常有用的通用任务,wc 的作者设计了命令以处理管道。如果你省略文件名,它会从 stdin 读取,并将结果输出到 stdout。让我们使用 ls 列出当前目录的内容,并通过管道将其传递给 wc 来统计行数。这个管道回答了问题:“我的当前目录中有多少个文件可见?”
$ ls -1
animals.txt
myfile
myfile2
test.py
$ ls -1 | wc -l
4
选项 -1 告诉 ls 将其结果以单列方式打印,在这里并非严格必要。要了解为什么我使用它,请参阅边栏 “ls 在重定向时的行为更改”。
wc 是本章中你见过的第一个命令,所以你在管道中能做的事情有限。只是为了好玩,将 wc 的输出再次通过管道传递给 wc,展示同一个命令可以在管道中出现多次。这个组合命令报告说 wc 输出的单词数是四个:三个整数和一个文件名:
$ wc animals.txt
7 51 325 animals.txt
$ wc animals.txt | wc -w
4
为什么要停在这里?在管道中添加第三个 wc,并计算输出的行数、单词数和字符数,结果是“4”:
$ wc animals.txt | wc -w | wc
1 1 2
输出显示了一行(包含数字 4)、一个单词(数字 4 本身)和两个字符。为什么是两个?因为字符串 “4” 末尾有一个不可见的换行符。
对于 wc 的愚蠢管道已经足够了。随着你掌握更多命令,管道将变得更实用。
命令 #2: head
head 命令打印文件的前几行。使用 -n 选项,使用 head 打印 animals.txt 的前三行:
$ head -n3 animals.txt
python Programming Python 2010 Lutz, Mark
snail SSH, The Secure Shell 2005 Barrett, Daniel
alpaca Intermediate Perl 2012 Schwartz, Randal
如果请求的行数超过文件包含的行数,head 将打印整个文件(就像 cat 命令一样)。如果省略 -n 选项,head 默认为 10 行(-n10)。
head 命令在你不关心文件的其余内容时,非常方便,可以快速而高效地查看文件顶部。即使是非常大的文件,也是如此,因为它无需读取整个文件。此外,head 将结果输出到 stdout,使其在管道中非常有用。统计 animals.txt 文件前三行的单词数:
$ head -n3 animals.txt | wc -w
20
head 还可以从 stdin 读取,用于更多的管道乐趣。一个常见的用法是在你不想看到全部输出时,从另一个命令中减少输出,比如长目录列表。例如,在 /bin 目录中列出前五个文件名:
$ ls /bin | head -n5
bash
bsd-csh
bunzip2
busybox
bzcat
命令 #3: cut
cut 命令从文件中打印一个或多个列。例如,打印 animals.txt 中第二列中出现的所有书名:
$ cut -f2 animals.txt
Programming Python
SSH, The Secure Shell
Intermediate Perl
MySQL High Availability
Linux in a Nutshell
Cisco IOS in a Nutshell
Writing Word Macros
cut 提供两种定义“列”的方式。第一种是按字段(-f)分割,当输入由每个由单个制表符分隔的字符串(字段)组成时。方便的是,这正是 animals.txt 文件的格式。前面的 cut 命令通过 -f2 选项打印每行的第二个字段。
为了缩短输出,将其通过管道传递给 head,仅打印 animals.txt 的前三行:
$ cut -f2 animals.txt | head -n3
Programming Python
SSH, The Secure Shell
Intermediate Perl
你还可以通过用逗号分隔它们的字段号来剪切多个字段:
$ cut -f1,3 animals.txt | head -n3
python 2010
snail 2005
alpaca 2012
或者按数字范围:
$ cut -f2-4 animals.txt | head -n3
Programming Python 2010 Lutz, Mark
SSH, The Secure Shell 2005 Barrett, Daniel
Intermediate Perl 2012 Schwartz, Randal
第二种为 cut 定义“列”的方式是通过字符位置,使用 -c 选项。从文件的每行中打印前三个字符,可以使用逗号(1,2,3)或范围(1-3)来指定:
$ cut -c1-3 animals.txt
pyt
sna
alp
rob
hor
don
ory
现在您已经看到了基本功能,请尝试使用cut和管道来进行更实际的操作。想象一下animals.txt文件有数千行长,并且您需要提取作者的姓氏。首先,隔离第四个字段,作者名:
$ cut -f4 animals.txt
Lutz, Mark
Barrett, Daniel
Schwartz, Randal
⋮
然后再次将结果管道传输到cut,使用选项-d(意味着“分隔符”)将分隔字符更改为逗号而不是制表符,以隔离作者的姓氏:
$ cut -f4 animals.txt | cut -d, -f1
Lutz
Barrett
Schwartz
⋮
保存时间与命令历史和编辑
您正在重复输入许多命令吗?而不是重复输入,使用向上箭头键滚动前面运行过的命令。 (这个 shell 功能称为命令历史。)当您到达所需命令时,按 Enter 键立即运行它,或者使用左右箭头键定位光标和 Backspace 键删除以进行编辑。 (这个功能是命令行编辑。)
我将在第三章中讨论更强大的命令历史和编辑功能。
命令 #4: grep
grep是一个非常强大的命令,但目前我将隐藏其大部分功能,简单说它打印与给定字符串匹配的行。(更多详细信息请参见第五章。)例如,以下命令显示animals.txt中包含字符串Nutshell的行:
$ grep Nutshell animals.txt
horse Linux in a Nutshell 2009 Siever, Ellen
donkey Cisco IOS in a Nutshell 2005 Boney, James
您还可以使用-v选项打印不匹配给定字符串的行。请注意不包含“Nutshell”字符串的行:
$ grep -v Nutshell animals.txt
python Programming Python 2010 Lutz, Mark
snail SSH, The Secure Shell 2005 Barrett, Daniel
alpaca Intermediate Perl 2012 Schwartz, Randal
robin MySQL High Availability 2014 Bell, Charles
oryx Writing Word Macros 1999 Roman, Steven
总的来说,grep对于在文件集合中查找文本非常有用。以下命令打印在以.txt结尾的文件中包含字符串Perl的行:
$ grep Perl *.txt
animals.txt:alpaca Intermediate Perl 2012 Schwartz, Randal
essay.txt:really love the Perl programming language, which is
essay.txt:languages such as Perl, Python, PHP, and Ruby
在这种情况下,grep找到三行匹配的内容,一个在animals.txt中,两个在essay.txt中。
grep读取标准输入并写入标准输出,非常适合管道。假设您想知道大目录/usr/lib中有多少个子目录。没有单个 Linux 命令可以提供答案,因此构建一个管道。从ls -l命令开始:
$ ls -l /usr/lib
drwxrwxr-x 12 root root 4096 Mar 1 2020 4kstogram
drwxr-xr-x 3 root root 4096 Nov 30 2020 GraphicsMagick-1.4
drwxr-xr-x 4 root root 4096 Mar 19 2020 NetworkManager
-rw-r--r-- 1 root root 35568 Dec 1 2017 attica_kde.so
-rwxr-xr-x 1 root root 684 May 5 2018 cnf-update-db
⋮
注意,ls -l在行首用d标记目录。使用cut来隔离第一列,可能是d也可能不是:
$ ls -l /usr/lib | cut -c1
d
d
d
-
-
⋮
然后使用grep仅保留包含d的行:
$ ls -l /usr/lib | cut -c1 | grep d
d
d
d
⋮
最后,用wc计算行数,您就得到了答案,由一个四条命令的管道产生——/usr/lib包含 145 个子目录:
$ ls -l /usr/lib | cut -c1 | grep d | wc -l
145
命令 #5: sort
sort命令将文件行按升序(默认)重新排序:
$ sort animals.txt
alpaca Intermediate Perl 2012 Schwartz, Randal
donkey Cisco IOS in a Nutshell 2005 Boney, James
horse Linux in a Nutshell 2009 Siever, Ellen
oryx Writing Word Macros 1999 Roman, Steven
python Programming Python 2010 Lutz, Mark
robin MySQL High Availability 2014 Bell, Charles
snail SSH, The Secure Shell 2005 Barrett, Daniel
或按降序排列(使用-r选项):
$ sort -r animals.txt
snail SSH, The Secure Shell 2005 Barrett, Daniel
robin MySQL High Availability 2014 Bell, Charles
python Programming Python 2010 Lutz, Mark
oryx Writing Word Macros 1999 Roman, Steven
horse Linux in a Nutshell 2009 Siever, Ellen
donkey Cisco IOS in a Nutshell 2005 Boney, James
alpaca Intermediate Perl 2012 Schwartz, Randal
sort可以按字母顺序(默认)或数字顺序(使用-n选项)对行进行排序。我将演示使用管道切割animals.txt中第三个字段,即出版年份:
$ cut -f3 animals.txt *Unsorted*
2010
2005
2012
2014
2009
2005
1999
$ cut -f3 animals.txt | sort -n *Ascending*
1999
2005
2005
2009
2010
2012
2014
$ cut -f3 animals.txt | sort -nr *Descending*
2014
2012
2010
2009
2005
2005
1999
要了解animals.txt中最新一本书的年份,请将sort的输出通过管道传输到head的输入,并仅打印第一行:
$ cut -f3 animals.txt | sort -nr | head -n1
2014
最大值和最小值
sort和head在处理数值数据时是强大的搭档,每行一个值。您可以通过管道数据到以下命令来打印最大值:
... | sort -nr | head -n1
并打印出最小值:
... | sort -n | head -n1
作为另一个例子,让我们玩玩文件 /etc/passwd,其中列出了可以在系统上运行进程的用户。^(4) 你将生成一个按字母顺序排列的所有用户列表。浏览前五行,你会看到像这样的内容:
$ head -n5 /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
smith:x:1000:1000:Aisha Smith,,,:/home/smith:/bin/bash
jones:x:1001:1001:Bilbo Jones,,,:/home/jones:/bin/bash
每行由冒号分隔的字符串组成,第一个字符串是用户名,所以你可以用 cut 命令来隔离用户名:
$ head -n5 /etc/passwd | cut -d: -f1
root
daemon
bin
smith
jones
并对它们进行排序:
$ head -n5 /etc/passwd | cut -d: -f1 | sort
bin
daemon
jones
root
smith
要生成所有用户名按字母顺序排序的列表,而不只是前五个,请用 head 替换成 cat:
$ cat /etc/passwd | cut -d: -f1 | sort
要检测给定用户在您的系统上是否有账户,可以将他们的用户名与 grep 命令匹配。空输出意味着没有账户:
$ cut -d: -f1 /etc/passwd | grep -w jones
jones
$ cut -d: -f1 /etc/passwd | grep -w rutabaga *(produces no output)*
-w 选项告诉 grep 只匹配完整的单词,而不是部分单词,以防您的系统也有包含“jones”的用户名,比如 sallyjones2。
命令 #6:uniq
uniq 命令用于检测文件中重复的相邻行。默认情况下,它会删除重复行。我将用一个包含大写字母的简单文件来演示这一点:
$ cat letters
A
A
A
B
B
A
C
C
C
C
$ uniq letters
A
B
A
C
注意,uniq 将前三行的 A 缩减为单个 A,但保留最后一个 A,因为它与前三个不是相邻的。
你也可以使用 -c 选项来计算出现次数:
$ uniq -c letters
3 A
2 B
1 A
4 C
我承认,当我第一次接触到 uniq 命令时,并没有看到它的多大用处,但它很快成为我最喜欢的命令之一。假设你有一个以制表符分隔的学生最终成绩文件,从 A(最好)到 F(最差):
$ cat grades
C Geraldine
B Carmine
A Kayla
A Sophia
B Haresh
C Liam
B Elijah
B Emma
A Olivia
D Noah
F Ava
你希望打印出现次数最多的等级。(如果有并列的情况,只打印其中一个获胜者。)从 cut 命令开始隔离等级并进行排序:
$ cut -f1 grades | sort
A
A
A
B
B
B
B
C
C
D
F
接下来,使用 uniq 命令来计算相邻行:
$ cut -f1 grades | sort | uniq -c
3 A
4 B
2 C
1 D
1 F
然后以逆序的数值方式对行进行排序,将最频繁出现的等级移动到顶部行:
$ cut -f1 grades | sort | uniq -c | sort -nr
4 B
3 A
2 C
1 F
1 D
并只保留第一行,使用 head 命令:
$ cut -f1 grades | sort | uniq -c | sort -nr | head -n1
4 B
最后,因为你只想要字母等级,而不是数量,所以用 cut 命令来隔离等级:
$ cut -f1 grades | sort | uniq -c | sort -nr | head -n1 | cut -c9
B
至此,你的答案就出来了,多亏了一个六步骤的管道—我们迄今为止最长的管道。这种一步一步的管道构建不仅是一个教育性的练习。这是 Linux 专家实际工作的方式。第八章 就专门讨论了这种技术。
检测重复文件
让我们结合你所学到的内容,看一个更大的例子。假设你在一个充满 JPEG 文件的目录中,想知道是否有重复的文件:
$ ls
image001.jpg image005.jpg image009.jpg image013.jpg image017.jpg
image002.jpg image006.jpg image010.jpg image014.jpg image018.jpg
⋮
你可以用一个管道来回答这个问题。你需要另一个命令 md5sum,它检查文件的内容并计算一个称为校验和的 32 个字符的字符串:
$ md5sum image001.jpg
146b163929b6533f02e91bdf21cb9563 image001.jpg
由于数学原因,给定文件的校验和很可能是唯一的。如果两个文件具有相同的校验和,那么它们几乎肯定是重复的。在这里,md5sum 表示第一个和第三个文件是重复的:
$ md5sum image001.jpg image002.jpg image003.jpg
146b163929b6533f02e91bdf21cb9563 image001.jpg
63da88b3ddde0843c94269638dfa6958 image002.jpg
146b163929b6533f02e91bdf21cb9563 image003.jpg
当只有三个文件时,重复的校验和很容易用肉眼检测,但是如果有三千个文件呢?管道来拯救。计算所有的校验和,使用cut来分离每行的前 32 个字符,并对行进行排序以使任何重复项相邻:
$ md5sum *.jpg | cut -c1-32 | sort
1258012d57050ef6005739d0e6f6a257
146b163929b6533f02e91bdf21cb9563
146b163929b6533f02e91bdf21cb9563
17f339ed03733f402f74cf386209aeb3
⋮
现在添加uniq来计算重复的行数:
$ md5sum *.jpg | cut -c1-32 | sort | uniq -c
1 1258012d57050ef6005739d0e6f6a257
2 146b163929b6533f02e91bdf21cb9563
1 17f339ed03733f402f74cf386209aeb3
⋮
如果没有重复项,uniq生成的所有计数都将为 1。将结果按数字从高到低排序,任何计数大于 1 的都将出现在输出的顶部:
$ md5sum *.jpg | cut -c1-32 | sort | uniq -c | sort -nr
3 f6464ed766daca87ba407aede21c8fcc
2 c7978522c58425f6af3f095ef1de1cd5
2 146b163929b6533f02e91bdf21cb9563
1 d8ad913044a51408ec1ed8a204ea9502
⋮
现在让我们移除非重复项。它们的校验和前面有六个空格,数字 1 和一个空格。我们将使用grep -v来移除这些行:^(5)
$ md5sum *.jpg | cut -c1-32 | sort | uniq -c | sort -nr | grep -v " 1 "
3 f6464ed766daca87ba407aede21c8fcc
2 c7978522c58425f6af3f095ef1de1cd5
2 146b163929b6533f02e91bdf21cb9563
最后,你有一个按出现次数排序的重复校验和列表,由一个美丽的六命令管道产生。如果没有输出,那么就没有重复文件。
如果该命令能显示重复文件的文件名,那么它将更加有用,但这需要我们尚未讨论的功能。 (你将在“改进重复文件检测器”中了解它们。)现在,通过使用grep来搜索具有给定校验和的文件来识别文件:
$ md5sum *.jpg | grep 146b163929b6533f02e91bdf21cb9563
146b163929b6533f02e91bdf21cb9563 image001.jpg
146b163929b6533f02e91bdf21cb9563 image003.jpg
并使用cut来清理输出:
$ md5sum *.jpg | grep 146b163929b6533f02e91bdf21cb9563 | cut -c35-
image001.jpg
image003.jpg
总结
现在你已经看到了标准输入 stdin、标准输出 stdout 和管道的强大之处。它们将少量命令转变为可组合的工具集合,证明整体大于各个部分之和。任何能够读取标准输入或写入标准输出的命令都可以参与管道操作。^(6) 随着你学习更多命令,你可以将本章的一般概念应用到自己的强大组合中。
^(1) 在美国键盘上,管道符号位于与反斜杠(\)相同的键上,通常位于回车和退格键之间或左 Shift 键和 Z 键之间。
^(2) POSIX 标准将这种形式的命令称为实用程序。
^(3) 根据你的设置,ls可能还会在打印到屏幕时使用其他格式功能,例如颜色,但在重定向时不会使用。
^(4) 一些 Linux 系统将用户信息存储在其他地方。
^(5) 在技术上,你不需要在这个管道中的最后加上sort -nr来隔离重复项,因为grep会移除所有非重复项。
^(6) 一些命令不使用标准输入/输出,因此无法从管道读取或向管道写入。例如mv和rm。然而,管道可以以其他方式整合这些命令;你将在第八章中看到例子。
第二章:介绍 Shell
所以,你可以在提示符下运行命令。但是那个提示符到底是什么?它从哪里来,你的命令是如何运行的,这又为什么重要呢?
那个小提示符是一个叫做 shell 的程序产生的。它是一个用户界面,位于你和 Linux 操作系统之间。Linux 提供了几种 shell,最常见的(也是本书的标准)叫做 bash。(有关其他 shell 的说明,请参见附录 B。)
bash 和其他 shell 做的远不止运行命令这么简单。例如,当一个命令包含通配符 (*) 来一次引用多个文件时:
$ ls *.py
data.py main.py user_interface.py
通配符完全由 shell 处理,而不是由程序 ls 处理。Shell 评估表达式 *.py 并在 ls 运行之前隐式地替换它为匹配的文件列表。换句话说,ls 从不看到通配符。从 ls 的角度来看,你输入了以下命令:
$ ls data.py main.py user_interface.py
Shell 还处理你在第一章中看到的管道。它透明地重定向 stdin 和 stdout,这样涉及的程序并不知道它们正在相互通信。
每次运行命令时,一些步骤是被调用程序的责任,比如 ls,而另一些则是 shell 的责任。专家用户明白哪个是哪个。这就是他们能够头脑风暴出长而复杂命令并成功运行的一个原因。他们在按下 Enter 键之前就已经知道命令会做什么,部分原因是他们理解了 shell 与其调用的程序之间的分离。
在本章中,我们将启动你对 Linux shell 的理解。我将采用与第一章命令和管道相同的最简方法。与其涵盖数十种 shell 功能,不如仅提供足够的信息来带你迈向学习之路的下一步:
-
用于文件名的模式匹配
-
存储值的变量
-
输入和输出的重定向
-
使用引号和转义字符禁用某些 shell 功能
-
用于查找要运行程序的搜索路径
-
保存对你的 shell 环境的更改
Shell 词汇
单词shell有两个含义。有时它指的是 Linux shell 的概念,如“shell 是一个强大的工具”或“bash 是一个 shell”。其他时候它指的是在给定的 Linux 计算机上运行的特定实例,等待你下一个命令。
在本书中,“shell”的含义大部分时间应该根据上下文清楚。必要时,我会提到第二个含义,即“shell 实例”、“运行中的 shell”或者你的“当前 shell”。
一些 shell 实例具有提示符,这样你可以与它们交互。我将使用术语交互式 shell来指代这些实例。其他 shell 实例是非交互式的—它们运行一系列命令然后退出。
用于文件名的模式匹配
在第一章中,你使用了几个接受文件名作为参数的命令,如 cut、sort 和 grep。这些命令(及许多其他命令)接受多个文件名作为参数。例如,你可以一次在一百个文件中搜索Linux一词,文件名从chapter1到chapter100:
$ grep Linux chapter1 chapter2 chapter3 chapter4 chapter5 *...and so on...*
按名称列出多个文件是一种繁琐且浪费时间的做法,因此 shell 提供了特殊字符作为文件或目录的简写。许多人称这些字符为通配符,但更普遍的概念称为模式匹配或通配符展开。模式匹配是 Linux 用户学习的两种最常见的加速技术之一(另一种是按上箭头键来回忆 shell 的先前命令,我在第三章中描述了这个技巧)。
大多数 Linux 用户熟悉星号或星号字符(*),它匹配文件或目录路径中的任意长度的零个或多个字符(不包括前导点)^(1):
$ grep Linux chapter*
在幕后,shell(而不是 grep!)展开模式 chapter* 为一系列匹配的文件名。然后 shell 运行 grep。
许多用户还看到了问号(?)特殊字符,它匹配任意单个字符(除了前导点)。例如,通过提供一个问号来使 shell 匹配单个数字,你可以仅在第 1 至第九章中搜索Linux一词:
$ grep Linux chapter?
或者在第 10 至第九十九章中使用两个问号来匹配两位数:
$ grep Linux chapter??
较少的用户熟悉方括号([]),它请求 shell 从集合中匹配单个字符。例如,你可以仅搜索前五章:
$ grep Linux chapter[12345]
同样,你可以使用连字符提供字符范围:
$ grep Linux chapter[1-5]
你还可以结合星号和方括号来匹配以偶数数字结尾的文件名,以搜索偶数章节:
$ grep Linux chapter*[02468]
方括号中可以出现任何字符,而不仅仅是数字。例如,以大写字母开头,包含下划线,并以@符号结尾的文件名将被 shell 匹配到:
$ ls [A-Z]*_*@
术语:评估表达式和展开模式
在命令行上输入的字符串,如 chapter* 或 Efficient Linux,称为表达式。像 ls -l chapter* 这样的整个命令也是一个表达式。
当 shell 解释和处理表达式中的特殊字符(如星号和管道符号)时,我们称 shell 评估该表达式。
模式匹配是一种评估方式。当 shell 评估包含模式匹配符号(例如 chapter*)的表达式,并用匹配该模式的文件名替换时,我们称 shell 展开了该模式。
模式几乎可以应用于您在命令行上提供文件或目录路径的任何地方。例如,您可以使用模式列出目录/etc中以.conf结尾的所有文件:
$ ls -1 /etc/*.conf
/etc/adduser.conf
/etc/appstream.conf
⋮
/etc/wodim.conf
谨慎使用仅接受一个文件或目录参数的命令与模式一起使用,例如cd。您可能得不到您期望的行为:
$ ls
Pictures Poems Politics
$ cd P* *Three directories will match*
bash: cd: too many arguments
如果一个模式不匹配任何文件,shell 将其保留为未更改的命令参数文字传递。在以下命令中,模式*.doc在当前目录中找不到任何匹配项,因此ls寻找一个名为*.doc的文件名并失败:
$ ls *.doc
/bin/ls: cannot access '*.doc': No such file or directory
在使用文件模式时,有两个非常重要的要点需要记住。首先,正如我已经强调的,模式匹配由 shell 执行,而不是调用的程序。我知道我一直在重复这一点,但我经常对多少 Linux 用户不知道它并且会对某些命令成功或失败发展出迷信感到惊讶。
第二个重要点是 shell 模式匹配仅适用于文件和目录路径。它不适用于用户名、主机名和某些命令接受的其他类型的参数。您也不能在命令行开头键入(例如)s?rt并期望 shell 运行sort程序。(某些 Linux 命令如grep、sed和awk执行它们自己的模式匹配,我们将在第五章中探讨。)
文件名模式匹配和您自己的程序
所有接受文件名作为参数的程序都自动“使用”模式匹配,因为 shell 在程序运行之前评估模式。即使是您自己编写的程序和脚本也是如此。例如,如果您编写了一个程序english2swedish,它将文件从英语翻译成瑞典语并接受命令行上的多个文件名,您可以立即使用模式匹配运行它:
$ english2swedish *.txt
变量评估
运行中的 shell 可以定义变量并将值存储在其中。shell 变量与代数中的变量很像——它有一个名称和一个值。一个例子是 shell 变量HOME。它的值是您的 Linux 主目录路径,例如/home/smith。另一个例子是USER,其值是您的 Linux 用户名,我将在本书中假设为smith。
要在 stdout 上打印HOME和USER的值,请运行printenv命令:
$ printenv HOME
/home/smith
$ printenv USER
smith
当 shell 评估一个变量时,它将变量名替换为其值。只需在名称前面放置一个美元符号来评估变量。例如,$HOME评估为字符串/home/smith。
观察 shell 评估命令行最简单的方法是运行echo命令,该命令简单地打印其参数(在 shell 完成评估后):
$ echo My name is $USER and my files are in $HOME *Evaluating variables*
My name is smith and my files are in /home/smith
$ echo ch*ter9 *Evaluating a pattern*
chapter9
变量的来源
USER和HOME等变量由 shell 预定义。它们的值在你登录时自动设置。(稍后详细介绍这个过程。)传统上,这些预定义变量使用大写名称。
你也可以随时通过使用以下语法为变量分配一个值来定义或修改变量:
*name*=*value*
例如,如果你经常在目录/home/smith/Projects中工作,你可以将其名称分配给一个变量:
$ work=$HOME/Projects
并将其用作cd的便捷快捷方式:
$ cd $work
$ pwd
/home/smith/Projects
你可以将$work提供给任何期望一个目录的命令:
$ cp myfile $work
$ ls $work
myfile
定义变量时,等号周围不允许有空格。如果你忘记了,shell 会错误地假设命令行上的第一个单词是要运行的程序,等号和值则是其参数,你会看到一个错误消息:
$ work = $HOME/Projects *The shell assumes "work" is a command*
work: command not found
类似work这样的用户定义变量与HOME这样的系统定义变量一样合法且可用。唯一的实际区别是,一些 Linux 程序会根据HOME、USER和其他系统定义变量的值内部改变其行为。例如,具有图形界面的 Linux 程序可能会从 shell 中检索你的用户名并显示它。这些程序不会关注像work这样的虚构变量,因为它们没有被编程来这么做。
变量与迷信
当你使用echo打印变量值时:
$ echo $HOME
/home/smith
你可能会认为echo命令会检查HOME变量并打印其值。实际情况并不是这样的。echo对变量一无所知。它只会打印你传递给它的参数。真正发生的是,在运行echo之前,shell 会评估$HOME。从echo的角度来看,你输入的是:
$ echo /home/smith
这种行为非常重要,特别是在我们深入了解更复杂的命令时。shell 在执行命令前会评估命令中的变量,以及模式和其他 shell 结构。
模式与变量
让我们测试一下你对模式和变量评估的理解。假设你在一个包含两个子目录mammals和reptiles的目录中,奇怪的是mammals子目录包含名为lizard.txt和snake.txt的文件:
$ ls
mammals reptiles
$ ls mammals
lizard.txt snake.txt
在现实世界中,蜥蜴和蛇不是哺乳动物,所以这两个文件应该移动到reptiles子目录中。以下是两种提议的方法。一种有效,一种无效:
mv mammals/*.txt reptiles *`Method` `1`*
FILES="lizard.txt snake.txt"
mv mammals/$FILES reptiles *`Method` `2`*
方法 1 有效,因为模式匹配整个文件路径。看看目录名mammals如何成为mammals/*.txt两个匹配的一部分:
$ echo mammals/*.txt
mammals/lizard.txt mammals/snake.txt
因此,方法 1 操作就像你输入以下正确的命令一样:
$ mv mammals/lizard.txt mammals/snake.txt reptiles
方法 2 使用的是变量,它们只评估为它们的字面值。它们对文件路径没有特殊处理:
$ echo mammals/$FILES
mammals/lizard.txt snake.txt
因此,方法 2 操作就像你输入以下有问题的命令一样:
$ mv mammals/lizard.txt snake.txt reptiles
此命令在当前目录中查找snake.txt文件,而不是mammals子目录中,所以失败了:
$ mv mammals/$FILES reptiles
/bin/mv: cannot stat 'snake.txt': No such file or directory
要使变量在这种情况下起作用,请使用 for 循环,在每个文件名之前添加目录名 mammals:
FILES="lizard.txt snake.txt"
for f in $FILES; do
mv mammals/$f reptiles
done
简化命令使用别名
变量是代表值的名称。Shell 还有一种代表命令的名称,它们称为别名。通过发明一个名称,并在名称后跟等号和一个命令来定义别名:
$ alias g=grep *A command with no arguments*
$ alias ll="ls -l" *A command with arguments: quotes are required*
通过键入其名称作为命令来运行别名。当别名比调用的命令更短时,你可以节省打字时间:
$ ll *Runs "ls -l"*
-rw-r--r-- 1 smith smith 325 Jul 3 17:44 animals.txt
$ g Nutshell animals.txt *Runs "grep Nutshell animals.txt"*
horse Linux in a Nutshell 2009 Siever, Ellen
donkey Cisco IOS in a Nutshell 2005 Boney, James
提示
始终将别名定义在单独的行上,而不是作为组合命令的一部分。(有关技术细节,请参阅 man bash。)
你可以定义一个与现有命令同名的别名,从而在你的 shell 中有效地替换该命令。这种做法称为屏蔽命令。假设你喜欢 less 命令用于阅读文件,但你希望它在显示每一页之前清除屏幕。这可以通过 -c 选项启用,因此定义一个名为 less 的别名,运行 less -c:^(2)
$ alias less="less -c"
别名优先于具有相同名称的命令,因此你现在在当前 shell 中已经屏蔽了 less 命令。我将在“搜索路径和别名”中解释优先级的含义。
要列出 shell 的别名及其值,请无参数运行 alias:
$ alias
alias g='grep'
alias ll='ls -l'
要查看单个别名的值,请运行 alias,然后跟随其名称:
$ alias g
alias g='grep'
要从 shell 中删除别名,请运行 unalias:
$ unalias g
重定向输入和输出
Shell 控制其运行的命令的输入和输出。你已经见过一个例子:管道,它将一个命令的 stdout 重定向到另一个命令的 stdin。管道语法 | 是 shell 的一个特性。
另一个 shell 特性是将 stdout 重定向到文件。例如,如果你使用 grep 从 Example 1-1 中的 animals.txt 文件中打印匹配行,则该命令默认将输出写入 stdout:
$ grep Perl animals.txt
alpaca Intermediate Perl 2012 Schwartz, Randal
你可以使用称为输出重定向的 shell 功能将该输出发送到文件中。只需添加符号 >,然后是接收输出的文件名即可:
$ grep Perl animals.txt > outfile *(displays no output)*
$ cat outfile
alpaca Intermediate Perl 2012 Schwartz, Randal
刚刚将 stdout 重定向到 outfile 文件而不是显示器。如果文件 outfile 不存在,则会创建它。如果存在,则重定向会覆盖其内容。如果你想要追加而不是覆盖输出文件,请使用 >> 符号:
$ grep Perl animals.txt > outfile *Create or overwrite outfile*
$ echo There was just one match >> outfile *Append to outfile*
$ cat outfile
alpaca Intermediate Perl 2012 Schwartz, Randal
There was just one match
输出重定向还有一个伙伴,输入重定向,它将 stdin 重定向到来自文件而不是键盘。使用符号 <,然后是文件名来重定向 stdin。
许多 Linux 命令在没有参数运行时,接受文件名作为参数,并从这些文件中读取,同时也可以从 stdin 中读取。例如,用于统计文件中行数、单词数和字符数的 wc 命令就是一个例子:
$ wc animals.txt *Reading from a named file*
7 51 325 animals.txt
$ wc < animals.txt *Reading from redirected stdin*
7 51 325
理解这两个 wc 命令在行为上的差异非常重要:
-
在第一个命令中,
wc接收到文件名animals.txt作为参数,所以wc知道文件的存在。wc会在磁盘上打开文件并读取其内容。 -
在第二个命令中,
wc被调用时没有参数,所以它从标准输入读取数据,通常是键盘输入。然而,Shell 却巧妙地将标准输入重定向到animals.txt文件。wc并不知道文件animals.txt的存在。
Shell 可以在同一条命令中重定向输入和输出:
$ wc < animals.txt > count
$ cat count
7 51 325
并且可以同时使用管道。在这里,grep从重定向的标准输入读取数据,并将结果通过管道传递给wc,后者将结果写入重定向的标准输出,生成文件count:
$ grep Perl < animals.txt | wc > count
$ cat count
1 6 47
你将在第八章深入探讨这类组合命令,并在整本书中看到许多其他重定向的示例。
用引号和转义字符禁用评估
通常,Shell 使用空格作为单词之间的分隔符。以下命令包含四个单词——一个程序名称后面跟着三个参数:
$ ls file1 file2 file3
然而有时候,你需要 Shell 将空格视为显著的字符,而不是分隔符。一个常见的例子是文件名中的空格,比如Efficient Linux Tips.txt:
$ ls -l
-rw-r--r-- 1 smith smith 36 Aug 9 22:12 Efficient Linux Tips.txt
如果在命令行中引用此类文件名,你的命令可能会失败,因为 Shell 会将空格字符视为分隔符:
$ cat Efficient Linux Tips.txt
cat: Efficient: No such file or directory
cat: Linux: No such file or directory
cat: Tips.txt: No such file or directory
强制 Shell 将空格视为文件名的一部分,你有三种选择——单引号、双引号和反斜杠:
$ cat 'Efficient Linux Tips.txt'
$ cat "Efficient Linux Tips.txt"
$ cat Efficient\ Linux\ Tips.txt
单引号告诉 Shell 将字符串中的每个字符都视为文字,即使该字符通常对 Shell 具有特殊含义,例如空格和美元符号:
$ echo '$HOME'
$HOME
双引号告诉 Shell 将所有字符都视为文字,除了某些美元符号和其他几个你稍后会了解的字符:
$ echo "Notice that $HOME is evaluated" *Double quotes*
Notice that /home/smith is evaluated
$ echo 'Notice that $HOME is not' *Single quotes*
Notice that $HOME is not
反斜杠,也称为转义字符,告诉 Shell 按照字面意义处理下一个字符。以下命令包含了一个转义的美元符号:
$ echo \$HOME
$HOME
即使在双引号内,反斜杠也作为转义字符:
$ echo "The value of \$HOME is $HOME"
The value of $HOME is /home/smith
但不适用于单引号内:
$ echo 'The value of \$HOME is $HOME'
The value of \$HOME is $HOME
使用反斜杠转义双引号字符在双引号内部:
$ echo "This message is \"sort of\" interesting"
This message is "sort of" interesting
行尾的反斜杠可以禁用不可见的换行符的特殊性质,使 Shell 命令可以跨越多行:
$ echo "This is a very long message that needs to extend \
onto multiple lines"
This is a very long message that needs to extend onto multiple lines
最后的反斜杠非常适合使管道更易读,就像从“Command #6: uniq”中的这个一样:
$ cut -f1 grades \
| sort \
| uniq -c \
| sort -nr \
| head -n1 \
| cut -c9
当以这种方式使用时,反斜杠有时被称为行继续字符。
别名前面的反斜杠会使别名无效,Shell 会查找同名命令,忽略任何遮蔽:
$ alias less="less -c" *Define an alias*
$ less myfile *Run the alias, which invokes less -c*
$ \less myfile *Run the standard less command, not the alias*
查找要运行的程序
当 Shell 首次遇到一个简单命令,比如ls *.py时,它只是一串毫无意义的字符。Shell 立即将字符串分成两个单词,“ls”和“*.py”。在这种情况下,第一个单词是磁盘上的程序名称,Shell 必须找到该程序才能运行它。
程序 ls 实际上是目录 /bin 中的可执行文件。您可以使用此命令验证其位置:
$ ls -l /bin/ls
-rwxr-xr-x 1 root root 133792 Jan 18 2018 /bin/ls
或者您可以使用 cd /bin 更改目录并运行这个可爱的,看起来神秘的命令:
$ ls ls
ls
使用 ls 命令列出可执行文件 ls。
shell 如何在 /bin 目录中定位 ls?在幕后,shell 会咨询一个预先安排好的目录列表,称为搜索路径,这个列表存储为 shell 变量 PATH 的值:
$ echo $PATH
/home/smith/bin:/usr/local/bin:/usr/bin:/bin:/usr/games:/usr/lib/java/bin
搜索路径中的目录由冒号 (:) 分隔。为了更清晰的视角,通过管道将输出传输到 tr 命令,将冒号转换为换行符,tr 命令能够将一个字符翻译成另一个字符(更多细节参见第五章):
$ echo $PATH | tr : "\n"
/home/smith/bin
/usr/local/bin
/usr/bin
/bin
/usr/games
/usr/lib/java/bin
当 shell 定位像 ls 这样的程序时,它按顺序从搜索路径中的目录进行查询。“/home/smith/bin/ls 存在吗?不。/usr/local/bin/ls 存在吗?也不。那 /usr/bin/ls 呢?还是不。或许 /bin/ls 呢?是的,找到了!我将运行 /bin/ls。” 这个搜索过程非常迅速,几乎察觉不到。^(3)
要在搜索路径中定位程序,请使用 which 命令:
$ which cp
/bin/cp
$ which which
/usr/bin/which
或者更强大(和冗长的)type 命令,这是一个 shell 内建命令,也可以定位别名,函数和 shell 内建命令:^(4)
$ type cp
cp is hashed (/bin/cp)
$ type ll
ll is aliased to ‘/bin/ls -l’
$ type type
type is a shell builtin
您的搜索路径可能在不同目录中包含相同命名的命令,例如 /usr/bin/less 和 /bin/less。Shell 将运行出现在路径中较早目录的命令。通过利用这种行为,您可以在搜索路径中较早的目录(例如个人 $HOME/bin 目录)中放置相同命名的命令来覆盖 Linux 命令。
搜索路径和别名
当 shell 按名称搜索命令时,在检查搜索路径之前会先检查该名称是否为别名。这就是为什么别名可以覆盖同名命令的原因。
搜索路径是一个很好的例子,展示了 Linux 中某些神秘事物其实有普通的解释。Shell 不会凭空提取命令或通过魔法定位它们。它会有条不紊地检查列表中的目录,直到找到所请求的可执行文件。
环境和初始化文件,简要版
运行中的 shell 中保存了许多重要信息在变量中:搜索路径,当前目录,首选文本编辑器,定制的 shell 提示符等等。运行中的 shell 中的变量总称为 shell 的环境。当 shell 退出时,它的环境被销毁。
逐个手动定义每个 shell 的环境将非常乏味。解决方案是在称为启动文件和初始化文件的 shell 脚本中定义环境,并让每个 shell 在启动时执行这些脚本。效果是某些信息似乎“全局”或“已知”于您的所有运行中的 shell。
我将深入讲解“配置你的环境”的细节。现在,我将教你一个初始化文件,让你能够顺利通过接下来的几章。它位于你的主目录中,名为 .bashrc(读作“点 bash R C”)。因为它的名字以点开头,ls 默认不会列出它:
$ ls $HOME
apple banana carrot
$ ls -a $HOME
.bashrc apple banana carrot
如果 $HOME/.bashrc 不存在,请使用文本编辑器创建它。你放置在这个文件中的命令将在 shell 启动时自动执行,^(5) 因此这是定义 shell 环境变量和其他对 shell 重要的事物(如别名)的好地方。这是一个示例 .bashrc 文件。以 # 开头的行是注释:
# Set the search path
PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
# Set the shell prompt
PS1='$ '
# Set your preferred text editor
EDITOR=emacs
# Start in my work directory
cd $HOME/Work/Projects
# Define an alias
alias g=grep
# Offer a hearty greeting
echo "Welcome to Linux, friend!"
你对 \(HOME/.bashrc* 的任何更改都不会影响任何正在运行的 shell,只会影响未来的 shell。你可以使用以下任何一个命令强制运行中的 shell 重新读取和执行 *\)HOME/.bashrc:
$ source $HOME/.bashrc *Uses the builtin "source" command*
$ . $HOME/.bashrc *Uses a dot*
这个过程称为“源化”初始化文件。如果有人告诉你“源化你的点-bash-R-C 文件”,他们的意思是运行上述命令之一。
警告
在现实生活中,不要将所有 shell 配置放在 \(HOME/.bashrc* 中。一旦你阅读了“配置你的环境”的详细信息,请检查你的 *\)HOME/.bashrc 并根据需要将命令移到适当的文件中。
总结
我只涵盖了一小部分 bash 的功能及其最基本的用法。在接下来的章节中,特别是第六章,你会看到更多。现在,你最重要的任务是理解以下概念:
-
shell 存在并且承担着重要责任。
-
shell 在运行任何命令之前评估命令行。
-
命令可以重定向标准输入、标准输出和标准错误。
-
引用和转义可以防止特殊的 shell 字符被评估。
-
shell 使用目录搜索路径来定位程序。
-
通过在文件 $HOME/.bashrc 中添加命令,你可以更改 shell 的默认行为。
你越了解 shell 与它调用的程序之间的分隔,命令行就会更加合理,你按下 Enter 运行命令前能预测发生的情况也越好。
^(1) 这就是为什么命令 ls * 不会列出以点开头的文件名(即点文件)。
^(2) bash 通过不将第二个 less 扩展为别名来防止无限递归。
^(3) 一些 shell 会记住(缓存)程序的路径,这样可以减少未来的搜索。
^(4) 请注意,命令 type which 会产生输出,但命令 which type 不会。
^(5) 这个声明过于简化;更多细节见表 6-1。
第三章:重新运行命令
假设您刚刚执行了一个包含详细管道的长命令,例如来自“检测重复文件”的命令:
$ md5sum *.jpg | cut -c1-32 | sort | uniq -c | sort -nr
而您希望再次运行它。不要重新输入!相反,请让 Shell 回溯历史记录并重新运行命令。在幕后,Shell 会记录您调用的命令,因此您可以轻松地通过几个按键回忆并重新运行它们。这个 Shell 功能称为命令历史。熟练的 Linux 用户大量使用命令历史来加快工作速度,避免浪费时间。
类似地,假设在运行之前您在输入前述命令时出现拼写错误,比如将“jpg”拼写为“jg”:
$ md5sum *.jg | cut -c1-32 | sort | uniq -c | sort -nr
要修正错误,请不要按下退格键几十次并重新键入所有内容。相反,请在原地更改命令。Shell 支持命令行编辑,用于修正拼写错误和执行各种修改,就像文本编辑器一样。
本章将向您展示如何通过利用命令历史和命令行编辑来节省大量时间和输入。像往常一样,我不会试图面面俱到——我将专注于这些 Shell 功能中最实用和最有用的部分。(如果您使用的 Shell 不是bash,请参阅附录 B 获取额外的注意事项。)
学会盲打
如果您能快速打字,本书中所有建议都会对您有所帮助。无论您有多么博学,如果您每分钟打字 40 个单词,而您的同样博学的朋友每分钟打字 120 个,他们的工作速度将比您快三倍。搜索“打字速度测试”以测量您的速度,然后搜索“打字教程”,培养一项终生技能。努力达到每分钟 100 个单词。这是值得努力的。
查看命令历史
命令历史简单来说就是您在交互式 Shell 中执行的先前命令列表。要查看 Shell 的历史记录,请运行history命令,这是一个 Shell 内置命令。命令按时间顺序显示,并附有 ID 编号以便于参考。输出看起来类似于这样:
$ history
1000 cd $HOME/Music
1001 ls
1002 mv jazz.mp3 jazzy-song.mp3
1003 play jazzy-song.mp3
⋮ *Omitting 479 lines*
1481 cd
1482 firefox https://google.com
1483 history *Includes the command you just ran*
history 命令的输出可能有几百行长(或更多)。通过添加整数参数来限制它仅打印最近的命令行数,该参数指定要打印的行数:
$ history 3 *Print the 3 most recent commands*
1482 firefox https://google.com
1483 history
1484 history 3
由于history输出到 stdout,您也可以使用管道处理输出。例如,逐屏查看您的历史记录:
$ history | less *Earliest to latest entry*
$ history | sort -nr | less *Latest to earliest entry*
或者仅打印包含单词cd的历史命令:
$ history | grep -w cd
1000 cd $HOME/Music
1092 cd ..
1123 cd Finances
1375 cd Checking
1481 cd
1485 history | grep -w cd
要清除(删除)当前 Shell 的历史记录,请使用 -c 选项:
$ history -c
从历史记录中召回命令
我将向您展示三种从 Shell 历史中召回命令的省时方法:
光标移动
学起来极其简单,但在实践中通常较慢
历史扩展
更难学(坦白地说,它很神秘),但可以非常快速
渐进搜索
简单而快速
每种方法在特定情况下都是最好的,所以我建议学习所有三种。你掌握的技术越多,就能在任何情况下更好地选择合适的方法。
历史记录浏览
要在给定 shell 中回忆你的上一个命令,按上箭头键。就是这么简单。继续按上箭头以逆时间顺序回忆较早的命令。按下箭头向另一个方向移动(向更近的命令)。当你到达想要的命令时,按 Enter 运行它。
浏览命令历史记录是 Linux 用户学习的两种最常见的加速方法之一。(另一种是使用*进行文件名模式匹配,如你在第二章中看到的。)如果你想要的命令在历史中附近——最多是两三个命令之前——浏览是有效的,但是要达到更远的命令则很烦琐。连续按上箭头键 137 次很快就会令人厌倦。
光标浏览的最佳用例是回忆和运行即时前一个命令。在许多键盘上,上箭头键靠近 Enter 键,因此您可以快速连续按这两个键。在标准美式 QWERTY 键盘上,我将右手的无名指放在上箭头上,食指放在 Enter 上,可以高效地轻敲这两个键。(试试看。)
历史扩展
历史扩展是一种利用特殊表达式访问命令历史的 shell 特性。这些表达式以感叹号开头,传统上发音为“bang”。例如,两个感叹号连续使用(“bang bang”)将评估立即前一个命令:
$ echo Efficient Linux
Efficient Linux
$ !! *"Bang bang" = previous command*
echo Efficient Linux *The shell helpfully prints the command being run*
Efficient Linux
要引用以特定字符串开头的最近命令,请在该字符串前面加上感叹号。因此,要重新运行最近的grep命令,请运行“bang grep”:
$ !grep
grep Perl animals.txt
alpaca Intermediate Perl 2012 Schwartz, Randal
要引用包含给定字符串任意位置的最近命令,请将字符串用问号包围:^(1)
$ !?grep?
history | grep -w cd
1000 cd $HOME/Music
1092 cd ..
⋮
你还可以通过 shell 历史记录中命令的绝对位置来检索特定命令——在history输出中其左侧的 ID 号码。例如,表达式!1203(“bang 1023”)表示“历史记录中位置 1023 处的命令”:
$ history | grep hosts
1203 cat /etc/hosts
$ !1203 *The command at position 1023*
cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 example.oreilly.com
::1 example.oreilly.com
负值通过历史记录中的相对位置检索命令,而不是绝对位置。例如,!-3(“bang minus three”)表示“你执行的三个命令前的命令”:
$ history
4197 cd /tmp/junk
4198 rm *
4199 head -n2 /etc/hosts
4199 cd
4200 history
$ !-3 *The command you executed three commands ago*
head -n2 /etc/hosts
127.0.0.1 localhost
127.0.1.1 example.oreilly.com
历史扩展快速方便,虽然有点晦涩。但如果提供错误的值并盲目执行,可能会有风险。仔细看看前面的例子。如果计数错误,输入!-4而不是!-3,你会意外地运行rm *而不是预期的head命令,从而删除家目录中的文件!为了减少这种风险,在命令后面附加修饰符:p以打印历史命令但不执行它:
$ !-3:p
head -n2 /etc/hosts *Printed, not executed*
Shell 将未执行的命令(head)附加到历史记录中,因此如果看起来没问题,您可以方便地使用快速的“叹号叹号”来运行它:
$ !-3:p
head -n2 /etc/hosts *Printed, not executed, and appended to history*
$ !! *Run the command for real*
head -n2 /etc/hosts *Printed and then executed*
127.0.0.1 localhost
127.0.1.1 example.oreilly.com
有些人将历史扩展称为“叹号命令”,但像!!和!grep这样的表达式并不是命令。它们是您可以在任何地方放置的字符串表达式。作为演示,使用echo在标准输出上打印!!的值,而不执行它,并用wc计算单词数:
$ ls -l /etc | head -n3 *Run any command*
total 1584
drwxr-xr-x 2 root root 4096 Jun 16 06:14 ImageMagick-6/
drwxr-xr-x 7 root root 4096 Mar 19 2020 NetworkManager/
$ echo "!!" | wc -w *Count the words in the previous command*
echo "ls -l /etc | head -n3" | wc -w
6
这个玩具例子演示了历史扩展比执行命令更有用途。在下一节中,您将看到一个更实用、更强大的技术。
我在这里只介绍了命令历史的一些功能。要获取完整信息,请运行man history。
命令历史中没有历史表达式。
如我在“关于命令历史的常见问题”中所述,shell 将命令原封不动地追加到历史中,不经过评估。唯一的例外是历史扩展。它的表达式总是在添加到命令历史之前进行评估:
$ ls *Run any command*
hello.txt
$ cd Music *Run some other command*
$ !-2 *Use history expansion*
ls
song.mp3
$ history *View the history*
1000 ls
1001 cd Music
1002 ls *"ls" appears in the history, not "!-2"*
1003 history
这个例外情况是有道理的。想象一下,试图理解一个充满像!-15和!-92这样引用其他历史条目的表达式的命令历史。您可能不得不通过眼睛追踪整个历史记录路径,才能理解一个单独的命令。
再也不会因为误删错误文件(感谢历史扩展)。
您是否曾经打算使用模式(例如*.txt)删除文件,但却意外地错误输入了模式,导致删除了错误的文件?这里有一个带有星号后意外空格字符的示例:
$ ls
123 a.txt b.txt c.txt dont-delete-me important-file passwords
$ rm * .txt *DANGER!! Don't run this! Deletes the wrong files!*
避免这种危险的最常见解决方案是将rm的别名设置为运行rm -i,这样在每次删除之前都会提示确认:
$ alias rm='rm -i' *Often found in a shell configuration file*
$ rm *.txt
/bin/rm: remove regular file 'a.txt'? y
/bin/rm: remove regular file 'b.txt'? y
/bin/rm: remove regular file 'c.txt'? y
因此,多余的空格字符不会致命,因为来自rm -i的提示将警告您正在删除错误的文件:
$ rm * .txt
/bin/rm: remove regular file '123'? *Something is wrong: kill the command*
然而,别名解决方案很麻烦,因为大多数时候您可能不想要或不需要rm提示您。如果您在另一台没有别名的 Linux 机器上登录,它也不起作用。我将向您展示一种更好的方法来避免使用模式匹配错误的文件名。该技术有两个步骤,并依赖于历史扩展:
-
验证。在运行
rm之前,使用所需模式运行ls以查看匹配的文件。$ ls *.txt a.txt b.txt c.txt -
删除。如果
ls的输出看起来正确,请运行rm !$以删除匹配的相同文件[²]。$ rm !$ rm *.txt
历史扩展!$(“叹号美元”)表示“前一个命令中您键入的最后一个词”。因此,这里的rm !$是“删除我刚刚用ls列出的任何东西”的简写,即*.txt。如果您在星号后意外添加了一个空格,ls的输出将明确显示——安全地——出现了问题。
$ ls * .txt
/bin/ls: cannot access '.txt': No such file or directory
123 a.txt b.txt c.txt dont-delete-me important-file passwords
很幸运你先运行了 ls 而不是 rm!现在你可以修改命令以去除多余的空格并安全进行。这个由ls后跟rm !$组成的两步命令序列是你 Linux 工具箱中很好的安全功能。
一个相关的技巧是在删除文件之前使用 head 查看文件内容,确保你操作的是正确的文件,然后运行 rm !$:
$ head myfile.txt
*(first 10 lines of the file appear)*
$ rm !$
rm myfile.txt
shell 还提供了一个历史扩展 !*(“bang star”),它匹配你在前一个命令中键入的所有参数,而不仅仅是最后一个参数:
$ ls *.txt *.o *.log
a.txt b.txt c.txt main.o output.log parser.o
$ rm !*
rm *.txt *.o *.log
在实践中,我使用 !* 的频率要比 !$ 少得多。它的星号与手动输入类似 *.txt 一样存在风险,因为如果你误输入了,它可能会被解释为文件名的模式匹配字符,所以并不比手动输入更安全。
命令历史的增量搜索
如果你可以输入命令的几个字符,剩下的会立即显示并准备运行,那不是太棒了吗?事实上你可以。这个 shell 的快速功能,称为 增量搜索,类似于网页搜索引擎提供的交互式建议。在大多数情况下,增量搜索是从历史记录中召回命令的最简单和最快速的技术,即使是你很久以前运行的命令也是如此。我强烈建议将其添加到你的工具箱中:
-
在 shell 提示符下,按 Ctrl-R(R 表示反向增量搜索)。
-
开始输入前一个命令的 任意部分 ——开头、中间或结尾。
-
每输入一个字符,shell 显示最近的匹配你输入的历史命令。
-
当你看到想要的命令时,按 Enter 运行它。
假设你一段时间前输入了命令 cd $HOME/Finances/Bank 并希望重新运行它。在 shell 提示符下按 Ctrl-R。提示符会改变以指示进行增量搜索:
(reverse-i-search)`':
开始输入想要的命令。例如,输入 c:
(reverse-i-search)`': c
shell 显示最近包含字符串 c 的命令,突出显示你已输入的内容:
(reverse-i-search)`': less /etc/hosts
输入下一个字母 d:
(reverse-i-search)`': cd
shell 显示最近包含字符串 cd 的命令,再次突出显示你已输入的内容:
(reverse-i-search)`': cd /usr/local
继续输入命令,加上空格和美元符号:
(reverse-i-search)`': cd $
命令行变成了:
(reverse-i-search)`': cd $HOME/Finances/Bank
这就是你想要的命令。按 Enter 运行它,你用了五个快捷键完成了。
我在这里假设 cd $HOME/Finances/Bank 是历史记录中最近的匹配命令。如果不是呢?如果你输入了一大堆包含相同字符串的命令?如果是这样,前面的增量搜索会显示另一个匹配项,比如:
(reverse-i-search)`': cd $HOME/Music
现在怎么办?你可以继续输入更多字符以精确到你想要的命令,但是,你可以再按一次 Ctrl-R。这个按键会导致 shell 跳转到历史记录中的 下一个 匹配命令:
(reverse-i-search)`': cd $HOME/Linux/Books
继续按 Ctrl-R 直到找到所需的命令:
(reverse-i-search)`': cd $HOME/Finances/Bank
并按 Enter 运行它。
这里有几个与增量搜索相关的小技巧:
-
要回忆最近搜索并执行的字符串,请从按两次 Ctrl-R 开始。
-
要停止增量搜索并继续在当前命令上工作,请按 Escape 键,或 Ctrl-J,或任何用于命令行编辑的键(本章的下一个主题),例如左右箭头键。
-
要退出增量搜索并清除命令行,请按 Ctrl-G 或 Ctrl-C。
花时间精通增量搜索。您将很快以惊人的速度定位命令。^(3)
命令行编辑
编辑命令有各种原因,无论是在输入时还是运行后:
-
修正错误
-
逐步创建命令,例如先输入命令的末尾,然后移动到行首输入开头部分
-
要基于先前的命令历史创建新命令(这是构建复杂管道的关键技能,您将在第八章中看到)
在本节中,我将向您展示三种编辑命令的方法,以提升您的技能和速度:
光标定位
再次强调,这是最慢且功能最弱的方法,但学习起来很简单。
插入符号表示法
一种历史扩展形式
Emacs 或 Vim 风格的按键
以强大的方式编辑命令行
与之前一样,我建议您学习所有三种技术以增强灵活性。
光标在命令中的定位
只需按下左箭头和右箭头键,在命令行上前后移动,逐个字符进行操作。使用退格键或删除键删除文本,然后输入所需的更正内容。表 3-1 总结了这些以及其他标准编辑命令行的按键操作。
光标来回移动很简单但效率低下。在更改小而简单时效果最佳。
表 3-1. 简单命令行编辑的光标键
| 按键 | 动作 |
|---|---|
| 左箭头 | 向左移动一个字符 |
| 右箭头 | 向右移动一个字符 |
| Ctrl + 左箭头 | 向左移动一个单词 |
| Ctrl + 右箭头 | 向右移动一个单词 |
| 起始 | 移动到命令行的开头 |
| 结尾 | 移动到命令行的末尾 |
| 退格 | 删除光标前的一个字符 |
| 删除 | 删除光标下的一个字符 |
使用插入符号进行历史扩展
假设您误运行了以下命令,输入jg而不是jpg:
$ md5sum *.jg | cut -c1-32 | sort | uniq -c | sort -nr
md5sum: '*.jg': No such file or directory
要正确运行命令,您可以从命令历史中调用它,将光标移到错误处并修复,但有一种更快的方法来实现您的目标。只需键入旧(错误的)文本、新(修正的)文本和一对插入符号(^),如下所示:
$ ^jg^jpg
按 Enter,正确的命令将显示并运行:
$ ^jg^jpg
md5sum *.jpg | cut -c1-32 | sort | uniq -c | sort -nr
⋮
插入符号语法是历史扩展的一种类型,表示“在上一个命令中,将jg替换为jpg”。请注意,shell 会在执行之前打印新命令,这是历史扩展的标准行为。
此技术仅会更改命令中源字符串(jg)的第一次出现。如果原始命令中 jg 出现多次,只会将第一次更改为 jpg。
Emacs 或 Vim 风格的命令行编辑
使用受启发于文本编辑器 Emacs 和 Vim 的熟悉按键来编辑命令行是最强大的方法。如果你已经熟练掌握其中一种编辑器,你可以立即跳入这种风格的命令行编辑中。如果不熟悉,Table 3-2 将帮助你开始使用最常见的移动和编辑按键。请注意,Emacs 的“Meta”键通常是 Escape(按下并释放)或 Alt(按住)。
Shell 的默认编辑风格是 Emacs 风格,我推荐它因为更易学易用。如果你喜欢 Vim 风格的编辑,运行以下命令(或将其添加到你的 $HOME/.bashrc 文件并使其生效):
$ set -o vi
要使用 Vim 按键编辑命令,请按 Escape 键进入命令编辑模式,然后使用 Table 3-2 中的 Vim 按键。要切换回 Emacs 风格编辑,请执行以下操作:
$ set -o emacs
现在,不断练习,直到这些按键(无论是 Emacs 的还是 Vim 的)成为你的第二天性。相信我,你很快就能因节省的时间而得到回报。
Table 3-2. Emacs 或 Vim 风格编辑的按键^(a)
| 动作 | Emacs | Vim |
|---|---|---|
| 向前移动一个字符 | Ctrl-f | h |
| 向后移动一个字符 | Ctrl-b | l |
| 向前移动一个单词 | Meta-f | w |
| 向后移动一个单词 | Meta-b | b |
| 移动到行首 | Ctrl-a | 0 |
| 移动到行尾 | Ctrl-e | $ |
| 交换两个字符 | Ctrl-t | xp |
| 交换两个单词 | Meta-t | n/a |
| 将下一个单词的首字母大写 | Meta-c | w~ |
| 将下一个单词全部转换为大写 | Meta-u | n/a |
| 将下一个单词全部转换为小写 | Meta-l | n/a |
| 改变当前字符的大小写 | n/a | ~ |
| 直接插入下一个字符,包括控制字符 | Ctrl-v | Ctrl-v |
| 向前删除一个字符 | Ctrl-d | x |
| 向后删除一个字符 | Backspace 或 Ctrl-h | X |
| 向前删除一个单词 | Meta-d | dw |
| 向后删除一个单词 | Meta-Backspace 或 Ctrl-w | db |
| 从光标处剪切到行首 | Ctrl-u | d^ |
| 从光标处剪切到行尾 | Ctrl-k | D |
| 删除整行 | Ctrl-e Ctrl-u | dd |
| 粘贴(yank)最近删除的文本 | Ctrl-y | p |
| 粘贴(yank)下一个被删除的文本(在之前的粘贴后) | Meta-y | n/a |
| 撤销上一个编辑操作 | Ctrl-_ | u |
| 撤销所有编辑操作 | Meta-r | U |
| 从插入模式切换到命令模式 | n/a | Escape |
| 从命令模式切换到插入模式 | n/a | i |
| 中止正在进行的编辑操作 | Ctrl-g | n/a |
| 清除显示 | Ctrl-l | Ctrl-l |
| ^(a) 标记为n/a的操作没有简单的按键组合,但可能通过更长的按键序列实现。 |
要了解更多关于 Emacs 风格编辑的细节,请参阅 GNU 的bash手册中的“可绑定的 Readline 命令”章节。要了解 Vim 风格编辑,请参阅文档“Readline VI 编辑模式速查表”。
总结
练习本章中的技巧,你将大大加快命令行的使用速度。其中三项特别的技术彻底改变了我使用 Linux 的方式,希望它们也能对你有所帮助:
-
删除文件时使用
!$以确保安全 -
使用 Ctrl-R 进行增量搜索
-
Emacs 风格命令行编辑
^(1) 在这里你可以省略尾随的问号—!?grep—但在某些情况下它是必需的,比如 sed 风格的历史扩展(见“使用历史扩展进行更强大的替换”)。
^(2) 我假设在ls步骤后你的背后没有添加或删除匹配的文件。不要依赖这种技术在快速变化的目录中。
^(3) 在撰写本书期间,我经常重新运行版本控制命令,如git add、git commit和git push。增量搜索使重新运行这些命令变得轻而易举。
第四章:浏览文件系统
在 1984 年的经典文化喜剧电影《八维空间的巴克路·班赛历险记》中,英雄主角提供了以下类似禅宗的智慧言论:“记住,无论你走到哪里……那里就是你自己。” 巴克路也可能在谈论 Linux 文件系统:
$ cd /usr/share/lib/etc/bin *No matter where you go...*
$ pwd
/usr/share/lib/etc/bin *...there you are.*
同样的情况也适用于 Linux 文件系统中的任何位置——你当前的目录——最终都会去别的地方(到另一个目录)。你能够更快速、更高效地执行这种导航,你的生产力也就越高。
本章中的技术将帮助你以更少的输入更快速地导航文件系统。它们看起来简单,但是带来的回报却非常大,学习曲线很小,收益很高。这些技术可以分为两大类:
-
快速移动到特定目录
-
快速返回到以前访问过的目录
如果你需要快速查看 Linux 目录,请参阅附录 A(Linux)。如果你使用的是除 bash 外的 shell,请参阅附录 B(Shells)获取额外的注意事项。
高效访问特定目录
如果你问 10 位 Linux 专家命令行中最乏味的部分是什么,其中 7 位会说:“输入长长的目录路径。”^(1) 毕竟,如果你的工作文件在 /home/smith/Work/Projects/Apps/Neutron-Star/src/include,你的财务文件在 /home/smith/Finances/Bank/Checking/Statements,你的视频在 /data/Arts/Video/Collection,重复输入这些路径肯定不是一件有趣的事情。在本节中,你将学习如何高效地导航到指定目录。
跳转到你的主目录
让我们从基础知识开始。无论你在文件系统的哪个位置,通过运行 cd 命令不带参数,都可以返回到你的主目录:
$ pwd
/etc *Start somewhere else*
$ cd *Run cd with no arguments...*
$ pwd
/home/smith *...and you're home again*
从任何文件系统中跳转到你的主目录的子目录时,使用简写而不是绝对路径,比如 /home/smith。一个简写是 shell 变量 HOME:
$ cd $HOME/Work
另一个是波浪号(tilde):
$ cd ~/Work
$HOME 和 ~ 都是 shell 扩展的表达式,你可以通过将它们输出到标准输出来验证这一点:
$ echo $HOME ~
/home/smith /home/smith
波浪号也可以在用户名前面直接表示另一个用户的主目录:
$ echo ~jones
/home/jones
使用 Tab 键更快地移动
当你输入 cd 命令时,通过按 Tab 键自动产生目录名称来节省输入。作为演示,访问一个包含子目录的目录,比如 /usr:
$ cd /usr
$ ls
bin games include lib local sbin share src
假设你想访问子目录 share。输入 sha 并按一次 Tab 键:
$ cd sha*<Tab>*
shell 会自动完成目录名:
$ cd share/
这个方便的快捷方式称为选项卡完成。当您输入的文本匹配单个目录名称时,它立即生效。当文本匹配多个目录名称时,您的 shell 需要更多信息才能完成所需的名称。假设您只输入了s并按了 Tab 键:
$ cd s*<Tab>*
shell 无法完成名称share(尚未完成),因为其他目录名称也以s开头:sbin和src。再次按 Tab 键,shell 会打印出所有可能的完成以指导您:
$ cd s*<Tab><Tab>*
sbin/ share/ src/
并等待您的下一步操作。为了解决歧义,请输入另一个字符h,然后按一次 Tab 键:
$ cd sh*<Tab>*
shell 会为您完成目录名称,从sh到share:
$ cd share/
通常按一次 Tab 键执行尽可能多的完成,或按两次打印所有可能的完成。您输入的字符越多,歧义就越少,匹配效果就越好。
选项卡完成非常适合加快导航速度。不必输入长路径如/home/smith/Projects/Web/src/include,只需输入尽可能少的内容并不断按 Tab 键。通过练习,您很快就能掌握这个技巧。
选项卡完成根据程序而异
选项卡完成不仅适用于cd命令。对于大多数命令,它也适用,尽管行为可能有所不同。当命令是cd时,Tab 键会完成目录名称。对于操作文件的其他命令,如cat、grep和sort,选项卡完成还会扩展文件名。如果命令是ssh(安全外壳),它会完成主机名。如果命令是chown(更改文件所有者),它会完成用户名。您甚至可以为速度创建自己的完成规则,正如我们将在示例 4-1 中看到的那样。还请参阅man bash并阅读其“可编程完成”主题。
使用别名或变量跳转到频繁访问的目录
如果您经常访问远程目录,例如/home/smith/Work/Projects/Web/src/include,请创建一个执行cd操作的别名:
# In a shell configuration file:
alias work="cd $HOME/Work/Projects/Web/src/include"
只需随时运行别名即可到达目的地:
$ work
$ pwd
/home/smith/Work/Projects/Web/src/include
或者,创建一个变量来保存目录路径:
$ work=$HOME/Work/Projects/Web/src/include
$ cd $work
$ pwd
/home/smith/Work/Projects/Web/src/include
$ ls $work/css *Use the variable in other ways*
main.css mobile.css
使用别名编辑经常编辑的文件
有时经常访问目录的原因是编辑特定文件。如果是这种情况,请考虑定义一个别名来通过绝对路径编辑该文件,而不必更改目录。以下别名定义允许您通过运行rcedit编辑$HOME/.bashrc,无论您在文件系统的哪个位置,都不需要cd:
# Place in a shell configuration file and source it:
alias rcedit='$EDITOR $HOME/.bashrc'
如果您经常访问路径很长的许多目录,可以为每个目录创建别名或变量。然而,这种方法也有一些缺点:
-
很难记住所有这些别名/变量。
-
您可能会意外地创建与现有命令同名的别名,从而引起冲突。
另一种方法是创建一个像示例 4-1 中的 shell 函数,我称之为qcd(“快速 cd”)。这个函数接受一个字符串键作为参数,比如work或recipes,然后运行cd到选定的目录路径。
示例 4-1. 一个用于cd到远程目录的函数
# Define the qcd function
qcd () {
# Accept 1 argument that's a string key, and perform a different
# "cd" operation for each key.
case "$1" in
work)
cd $HOME/Work/Projects/Web/src/include
;;
recipes)
cd $HOME/Family/Cooking/Recipes
;;
video)
cd /data/Arts/Video/Collection
;;
beatles)
cd $HOME/Music/mp3/Artists/B/Beatles
;;
*)
# The supplied argument was not one of the supported keys
echo "qcd: unknown key '$1'"
return 1
;;
esac
# Helpfully print the current directory name to indicate where you are
pwd
}
# Set up tab completion
complete -W "work recipes video beatles" qcd
把这个函数存储在一个像$HOME/.bashrc这样的 shell 配置文件中(参见“环境和初始化文件,简明版”),然后 source 它,就可以运行了。输入qcd再加上一个支持的键,可以快速访问相关目录:
$ qcd beatles
/home/smith/Music/mp3/Artists/B/Beatles
作为奖励,脚本的最后一行运行命令complete,这是一个 shell 内置命令,为qcd设置自定义的制表符补全,以便完成这四个支持的键。现在你不必记住qcd的参数!只需输入qcd,后面加一个空格,然后按两次 Tab 键,shell 将打印出所有键供参考,你可以像往常一样完成其中任何一个:
$ qcd *<Tab><Tab>*
beatles recipes video work
$ qcd v*<Tab><Enter>* *Completes 'v' to 'video'*
/data/Arts/Video/Collection
使用 CDPATH 让大型文件系统变得更小
qcd函数只处理你指定的目录。shell 提供了一个更通用的解决方案来进行cd,没有这种缺陷,称为cd 搜索路径。这个 shell 特性改变了我在 Linux 文件系统中导航的方式。
假设你有一个重要的子目录,经常要访问,名为Photos。它位于/home/smith/Family/Memories/Photos。当你在文件系统中穿梭时,每次想要进入Photos目录,可能需要输入一个长路径,比如:
$ cd ~/Family/Memories/Photos
如果你能把这个路径缩短到只是Photos,无论你在文件系统的哪个位置,都能到达你的子目录,那不是很棒吗?
$ cd Photos
通常情况下,这个命令会失败:
bash: cd: Photos: No such file or directory
除非你碰巧在正确的父目录(~/Family/Memories)或其他含有Photos子目录的目录中。好吧,通过一点设置,你可以指示cd在除了当前目录之外的其他位置搜索你的Photos子目录。搜索速度非常快,只在你指定的父目录中查找。例如,你可以指示cd在当前目录之外,还搜索$HOME/Family/Memories。然后,当你从文件系统的其他位置输入cd Photos时,cd会成功:
$ pwd
/etc
$ cd Photos
/home/smith/Family/Memories/Photos
cd 搜索路径工作原理类似于命令搜索路径$PATH,但不是找命令,而是找子目录。用 shell 变量CDPATH配置它,格式与PATH相同:用冒号分隔的目录列表。例如,如果你的CDPATH由这四个目录组成:
$HOME:$HOME/Projects:$HOME/Family/Memories:/usr/local
并且你输入:
$ cd Photos
然后cd将按顺序检查以下目录的存在,直到找到一个或完全失败:
-
当前目录中的Photos
-
$HOME/Photos
-
$HOME/Projects/Photos
-
$HOME/Family/Memories/Photos
-
/usr/local/Photos
在这种情况下,cd在第四次尝试成功,并且将目录更改为\(HOME/Family/Memories/Photos*。如果`\)CDPATH`中有两个子目录命名为Photos*,则较早的父目录胜出。
注意
通常,成功的cd不会打印任何输出。但是,当cd使用你的CDPATH定位到一个目录时,它会在标准输出上打印绝对路径,以通知你新的当前目录:
$ CDPATH=/usr *Set a CDPATH*
$ cd /tmp *No output: CDPATH wasn't consulted*
$ cd bin *cd consults CDPATH...*
/usr/bin *...and prints the new working directory*
填充CDPATH以包含你最重要或最频繁使用的父目录,你可以在文件系统的任何地方进入它们的任何子目录,无论其深度有多深,几乎不用输入路径的大部分。相信我,这很棒,下面的案例研究应该可以证明它。
组织你的主目录以快速导航
让我们使用CDPATH来简化你导航主目录的方式。通过一点配置,你可以使主目录中的许多目录在任何文件系统中的任何地方易于访问,减少输入,效果显著。如果你的主目录组织良好,至少有两个级别的子目录,这种技术效果最佳。图 4-1 展示了一个组织良好的目录布局示例。

图 4-1. /home/smith目录中的两级子目录
设置你的CDPATH的技巧是按顺序包含以下内容:
-
$HOME -
你的主目录的子目录选择
-
一个父目录的相对路径,用两个点(
..)表示
通过包含$HOME,你可以立即跳转到任何它的子目录(Family、Finances、Linux、Music和Work)而不用在文件系统中的任何地方输入前导路径:
$ pwd
/etc *Begin outside your home directory*
$ cd Work
/home/smith/Work
$ cd Family/School *You jumped 1 level below $HOME*
/home/smith/Family/School
通过在你的CDPATH中包含主目录的子目录,你可以一次跳转到它们的子目录:
$ pwd
/etc *Anywhere outside your home directory*
$ cd School
/home/smith/Family/School *You jumped 2 levels below $HOME*
到目前为止,你的CDPATH中的所有目录都是主目录和其子目录中的绝对路径。然而,通过包含相对路径..,你可以在每个目录中赋予新的cd行为。无论你在文件系统中的哪个位置,你都可以通过名称跳转到任何同级目录(../sibling)而不用输入两个点,因为cd将搜索你当前的父目录。例如,如果你在/usr/bin中,想要移动到/usr/lib,你只需要cd lib:
$ pwd
/usr/bin *Your current directory*
$ ls ..
bin include lib src *Your siblings*
$ cd lib
/usr/lib *You jumped to a sibling*
或者,如果你是一个在src、include和docs子目录上工作代码的程序员:
$ pwd
/usr/src/myproject
$ ls
docs include src
你可以简洁地在子目录之间跳转:
$ cd docs *Change your current directory*
$ cd include
/usr/src/myproject/include *You jumped to a sibling*
$ cd src
/usr/src/myproject/src *Again*
一个用于图图 4-1 中的树的CDPATH可能包含六个项目:你的主目录、其四个子目录以及父目录的相对路径:
# Place in a shell configuration file and source it:
export CDPATH=$HOME:$HOME/Work:$HOME/Family:$HOME/Linux:$HOME/Music:..
在配置文件中载入后,你可以在不输入长目录路径的情况下cd到许多重要目录,只需输入短目录名称。太棒了!
如果所有 CDPATH 目录下的子目录都有唯一的名称,则此技术效果最佳。如果有重复的名称,例如 \(HOME/Music* 和 *\)HOME/Linux/Music,你可能无法获得想要的行为。命令 cd Music 将始终先检查 \(HOME*,而不是 *\)HOME/Linux,因此无法找到 $HOME/Linux/Music。
要检查 $HOME 的前两级目录中是否存在重复的子目录名称,请尝试这个大胆的单行命令。它列出 $HOME 的所有子目录和子子目录,用 cut 隔离子子目录名称,排序列表,并用 uniq 计算出现次数:
$ cd
$ ls -d */ && (ls -d */*/ | cut -d/ -f2-) | sort | uniq -c | sort -nr | less
你可能会从 “检测重复文件” 中认识到这种重复检查技术。如果输出显示任何大于 1 的计数,则存在重复项。我意识到这个命令包含了一些我尚未介绍的功能。您将在 “技巧 #1:条件列表” 中了解到双与符号 (&&),以及在 “技巧 #10:显式子 shell” 中的括号。
高效返回目录
你刚刚看到如何有效访问目录。现在我将向你展示,当你需要返回时如何快速重新访问目录。
使用 “cd -” 在两个目录之间切换
假设你在一个深层目录中工作,然后运行 cd 去其他地方:
$ pwd
/home/smith/Finances/Bank/Checking/Statements
$ cd /etc
然后想着,“不,等等,我想回到我刚才所在的 Statements 目录。” 不要重新输入长目录路径。只需运行带有破折号作为参数的 cd:
$ cd -
/home/smith/Finances/Bank/Checking/Statements
此命令将您的 shell 返回到其先前的目录,并帮助打印其绝对路径,以便您知道自己在哪里。
要在一对目录之间来回跳转,重复运行 cd -。当你在单个 shell 中专注于两个目录时,这将节省时间。然而,有一个注意事项:shell 一次只记住一个上一个目录。例如,如果你在 /usr/local/bin 和 /etc 之间切换:
$ pwd
/usr/local/bin
$ cd /etc *The shell remembers /usr/local/bin*
$ cd - *The shell remembers /etc*
/usr/local/bin
$ cd - *The shell remembers /usr/local/bin*
/etc
并且当你运行 cd 而不带参数跳转到你的主目录时:
$ cd *The shell remembers /etc*
shell 现在忘记了 /usr/local/bin 作为上一个目录:
$ cd - *The shell remembers your home directory*
/etc
$ cd - *The shell remembers /etc*
/home/smith
下一个技巧克服了这个限制。
使用 pushd 和 popd 在多个目录之间切换
cd - 命令在两个目录之间切换,但是如果你有三个或更多需要跟踪的目录怎么办?假设你在 Linux 计算机上创建一个本地网站。这通常涉及四个或更多目录:
-
部署的现场网页的位置,例如 /var/www/html
-
Web 服务器配置目录,通常位于 /etc/apache2
-
SSL 证书的位置,通常位于 /etc/ssl/certs
-
你的工作目录,例如 ~/Work/Projects/Web/src
相信我,反复输入以下内容非常乏味:
$ cd ~/Work/Projects/Web/src
$ cd /var/www/html
$ cd /etc/apache2
$ cd ~/Work/Projects/Web/src
$ cd /etc/ssl/certs
如果您有大型窗口显示器,可以通过为每个目录打开单独的 shell 窗口来减轻负担。但是,如果您在单个 shell 中工作(比如通过 SSH 连接),可以利用一个称为目录堆栈的 shell 特性。它让您可以轻松快速地在多个目录之间移动,使用内置的 shell 命令pushd、popd和dirs。学习曲线可能只需 15 分钟,但速度上的巨大回报将持续一生。^(2)
目录堆栈是您在当前 shell 中访问并决定跟踪的目录列表。您通过执行称为推入和弹出的两个操作来操作堆栈。推入目录将其添加到列表的开头,传统上称为堆栈的顶部。弹出则从堆栈中移除顶部目录。^(3) 最初,堆栈仅包含当前目录,但您可以添加(推入)和移除(弹出)目录,并快速使用cd在它们之间切换。
注意
每个正在运行的 shell 都维护其自己的目录堆栈。
我将从基本操作(推入、弹出、查看)开始,然后进入精彩内容。
推送目录到堆栈
命令pushd(“推入目录”缩写)执行以下所有操作:
-
将给定目录添加到堆栈的顶部
-
执行
cd到该目录 -
打印堆栈,从顶部到底部,以便您参考。
我将构建一个包含四个目录的目录堆栈,逐个将它们推入堆栈:
$ pwd
/home/smith/Work/Projects/Web/src
$ pushd /var/www/html
/var/www/html ~/Work/Projects/Web/src
$ pushd /etc/apache2
/etc/apache2 /var/www/html ~/Work/Projects/Web/src
$ pushd /etc/ssl/certs
/etc/ssl/certs /etc/apache2 /var/www/html ~/Work/Projects/Web/src
$ pwd
/etc/ssl/certs
Shell 在每个pushd操作后打印堆栈。当前目录是最左边(顶部)的目录。
查看目录堆栈
使用dirs命令打印 shell 的目录堆栈。它不会修改堆栈:
$ dirs
/etc/ssl/certs /etc/apache2 /var/www/html ~/Work/Projects/Web/src
如果您喜欢从顶部到底部打印堆栈,请使用-p选项:
$ dirs -p
/etc/ssl/certs
/etc/apache2
/var/www/html
~/Work/Projects/Web/src
甚至将输出管道到命令nl以从零开始为行编号:
$ dirs -p | nl -v0
0 /etc/ssl/certs
1 /etc/apache2
2 /var/www/html
3 ~/Work/Projects/Web/src
更简单的方法是运行dirs -v以打印带有编号行的堆栈:
$ dirs -v
0 /etc/ssl/certs
1 /etc/apache2
2 /var/www/html
3 ~/Work/Projects/Web/src
如果您喜欢这种自顶向下的格式,请考虑创建一个别名:
# Place in a shell configuration file and source it:
alias dirs='dirs -v'
从堆栈中弹出一个目录
popd命令(“弹出目录”)是pushd的反向操作。它执行以下所有操作:
-
从堆栈顶部删除一个目录
-
执行
cd到新的顶部目录 -
打印堆栈,从顶部到底部,以便您参考。
例如,如果您的堆栈有四个目录:
$ dirs
/etc/ssl/certs /etc/apache2 /var/www/html ~/Work/Projects/Web/src
然后重复运行popd将从顶部到底部遍历这些目录:
$ popd
/etc/apache2 /var/www/html ~/Work/Projects/Web/src
$ popd
/var/www/html ~/Work/Projects/Web/src
$ popd
~/Work/Projects/Web/src
$ popd
bash: popd: directory stack empty
$ pwd
~/Work/Projects/Web/src
提示
pushd和popd命令是如此节省时间,我建议创建两个字符的别名,可以像输入cd一样快速输入:
# Place in a shell configuration file and source it:
alias gd=pushd
alias pd=popd
交换堆栈上的目录
现在你可以构建和清空目录栈了,让我们专注于实际应用场景。没有参数的pushd交换栈中的前两个目录,并导航到新的顶部目录。通过简单运行pushd在/etc/apache2和你的工作目录之间多次跳转。看看第三个目录/var/www/html作为栈的第一个两个目录交换位置:
$ dirs
/etc/apache2 ~/Work/Projects/Web/src /var/www/html
$ pushd
~/Work/Projects/Web/src /etc/apache2 /var/www/html
$ pushd
/etc/apache2 ~/Work/Projects/Web/src /var/www/html
$ pushd
~/Work/Projects/Web/src /etc/apache2 /var/www/html
注意,pushd的行为类似于cd -命令,可以在两个目录之间切换,但它没有记住仅一个目录的限制。
将一个错误的cd转变为pushd
假设你在几个目录之间使用pushd跳转,意外运行cd而不是pushd丢失了一个目录:
$ dirs
~/Work/Projects/Web/src /var/www/html /etc/apache2
$ cd /etc/ssl/certs
$ dirs
/etc/ssl/certs /var/www/html /etc/apache2
糟糕,意外的cd命令用/etc/ssl/certs替换了栈中的~/Work/Projects/Web/src。但不要担心。你可以将丢失的目录添加回栈中,而不需要输入其长路径。只需运行两次pushd,一次带有破折号参数,一次没有:
$ pushd -
~/Work/Projects/Web/src /etc/ssl/certs /var/www/html /etc/apache2
$ pushd
/etc/ssl/certs ~/Work/Projects/Web/src /var/www/html /etc/apache2
让我们解析为什么这样能行:
-
第一个
pushd返回到你的 shell 的上一个目录~/Work/Projects/Web/src,并将其推送到栈上。pushd和cd一样,接受破折号作为参数表示“返回到我的上一个目录”。 -
第二个
pushd命令交换了栈中的前两个目录,带你回到/etc/ssl/certs。最终结果是,你将~/Work/Projects/Web/src恢复到栈中的第二位置,就像如果你没有犯错一样。
这个“oops, I forgot a pushd”命令非常有用,值得设置一个别名。我称之为slurp,因为在我看来,它可以“吸回”我误操作丢失的目录:
# Place in a shell configuration file and source it:
alias slurp='pushd - && pushd'
深入了解栈
如果你想要在栈中除了顶部两个之外的目录之间进行cd,pushd和popd接受一个正整数或负整数参数以进一步操作栈中的目录。命令:
$ pushd +*N*
将N个目录从栈的顶部移到底部,然后执行cd到新的顶部目录。负数参数(-N)在执行cd之前将目录从底部移到顶部。^(4)
$ dirs
/etc/ssl/certs ~/Work/Projects/Web/src /var/www/html /etc/apache2
$ pushd +1
~/Work/Projects/Web/src /var/www/html /etc/apache2 /etc/ssl/certs
$ pushd +2
/etc/apache2 /etc/ssl/certs ~/Work/Projects/Web/src /var/www/html
以这种方式,你可以用简单的命令跳转到栈中的任何其他目录。然而,如果你的栈很长,可能很难凭眼睛判断目录的数字位置。因此,像在“查看目录栈”中那样打印每个目录的数字位置,使用dirs -v命令:
$ dirs -v
0 /etc/apache2
1 /etc/ssl/certs
2 ~/Work/Projects/Web/src
3 /var/www/html
要将/var/www/html移到栈的顶部(并使其成为当前目录),运行pushd +3。
要跳转到栈底的目录,请运行pushd -0(减零):
$ dirs
/etc/apache2 /etc/ssl/certs ~/Work/Projects/Web/src /var/www/html
$ pushd -0
/var/www/html /etc/apache2 /etc/ssl/certs ~/Work/Projects/Web/src
你还可以使用带有数值参数的popd从栈中移除顶部目录之外的目录。命令:
$ popd +*N*
从堆栈中移除位置为N的目录,从顶部开始计数。负参数(-N)则从堆栈底部开始计数。计数从零开始,因此popd +1将移除第二个顶部目录:
$ dirs
/var/www/html /etc/apache2 /etc/ssl/certs ~/Work/Projects/Web/src
$ popd +1
/var/www/html /etc/ssl/certs ~/Work/Projects/Web/src
$ popd +2
/var/www/html /etc/ssl/certs
总结
这一章中的所有技巧只需稍加练习即可轻松掌握,能够节省大量时间和输入。我发现特别改变生活的技巧有:
-
用于快速导航的
CDPATH -
用于快速返回的
pushd和popd -
偶尔使用的
cd -命令
^(1) 我虽然是编造的,但这肯定是真实的。
^(2) 另一种方法是使用screen和tmux等命令行程序打开多个虚拟显示,它们被称为终端复用器。学习成本比目录堆栈更高,但也值得一试。
^(3) 如果你了解计算机科学中的堆栈,目录堆栈正是一组目录名的堆栈。
^(4) 程序员可能会认出这些操作是堆栈的旋转。
第二部分:下一级技能
现在你已经了解了命令、管道、shell 和导航的基础知识,是时候迈出下一步了。在接下来的五章中,我将介绍大量新的 Linux 程序和一些重要的 shell 概念。你将运用它们来构建复杂的命令,并在 Linux 计算机上应对现实情况。
第五章:扩展您的工具箱
Linux 系统自带数千个命令行程序。经验丰富的用户通常依赖更小的子集,一种工具箱,他们一再返回。第一章向您的工具箱中添加了六个非常有用的命令,现在我将向您介绍大约十几个更多的命令。我将简要描述每个命令并展示一些示例用法。(要查看所有可用选项,请查看命令的 man 页。)我还将介绍两个强大的命令,它们学习起来更困难但却非常值得,称为awk和sed。总的来说,本章中的命令满足流水线和其他复杂命令的四个常见实际需求:
生成文本
打印日期、时间、数字和字母序列、文件路径、重复字符串以及其他文本,以启动您的流水线。
隔离文本
使用grep、cut、head、tail和awk的一个方便功能来提取文本文件的任何部分。
合并文本
从上到下使用cat和tac合并文件,或使用echo和paste并排合并文件。您还可以使用paste和diff交错文件。
转换文本
使用简单命令(如tr和rev)或更强大命令(如awk和sed)将文本转换为其他文本。
本章是一个快速概述。后续章节将展示这些命令的实际应用。
生成文本
每个流水线从打印到标准输出的简单命令开始。有时候是像grep或cut这样的命令,从文件中选择数据:
$ cut -d: -f1 /etc/passwd | sort *Print all usernames and sort them*
或者甚至cat,方便地将多个文件的完整内容通过管道传递给其他命令:
$ cat *.txt | wc -l *Total the number of lines*
其他时候,管道中的初始文本来自其他来源。您已经知道这样的一个命令,即ls,它打印文件和目录名称及相关信息。让我们来看看其他产生文本的命令和技术:
date
以各种格式打印日期和时间
seq
打印一系列数字
大括号扩展
一个 Shell 功能,用于打印一系列数字或字符
find
打印文件路径
yes
重复打印相同的行
日期命令
date命令以各种格式打印当前日期和/或时间:
$ date *Default formatting*
Mon Jun 28 16:57:33 EDT 2021
$ date +%Y-%m-%d *Year-Month-Day format*
2021-06-28
$ date +%H:%M:%S *Hour:Minute:Seconds format*
16:57:33
要控制输出格式,请提供以加号(+)开头的参数,后跟任意文本。文本可能包含以百分号(%)开头的特殊表达式,例如%Y表示当前四位数年份,%H表示当前 24 小时制的小时数。日期的 man 页上有完整的表达式列表。
$ date +"I cannot believe it's already %A!" *Day of week*
I cannot believe it's already Tuesday!
seq 命令
seq命令在范围内打印一系列数字。提供两个参数,范围的低值和高值,seq将打印整个范围:
$ seq 1 5 *Print all integers from 1 to 5, inclusive*
1
2
3
4
5
如果提供了三个参数,则第一个和第三个定义范围,中间的数字是增量:
$ seq 1 2 10 *Increment by 2 instead of 1*
1
3
5
7
9
使用负增量如-1生成一个降序序列:
$ seq 3 -1 0
3
2
1
0
或者使用小数增量生成浮点数:
$ seq 1.1 0.1 2 *Increment by 0.1*
1.1
1.2
1.3
⋮
2.0
默认情况下,值由换行符分隔,但您可以使用 -s 选项后跟任意字符串来更改分隔符:
$ seq -s/ 1 5 *Separate values with forward slashes*
1/2/3/4/5
使用 -w 选项使所有值具有相同的宽度(以字符为单位),如有必要添加前导零:
$ seq -w 8 10
08
09
10
seq 可以生成许多其他格式的数字(请参阅手册页),但我的示例代表了最常见的用法。
大括号扩展(一种 Shell 特性)
shell 提供了一种自己的打印数字序列的方式,称为 大括号扩展。从左大括号开始,添加两个用两个点分隔的整数,以右大括号结束:
$ echo {1..10} *Forward from 1*
1 2 3 4 5 6 7 8 9 10
$ echo {10..1} *Backward from 10*
10 9 8 7 6 5 4 3 2 1
$ echo {01..10} *With leading zeros (equal width)*
01 02 03 04 05 06 07 08 09 10
更一般地,shell 表达式 {x..y..z} 生成从 x 到 y 的值,以 z 递增:
$ echo {1..1000..100} *Count by hundreds from 1*
1 101 201 301 401 501 601 701 801 901
$ echo {1000..1..100} *Backward from 1000*
1000 900 800 700 600 500 400 300 200 100
$ echo {01..1000..100} *With leading zeros*
0001 0101 0201 0301 0401 0501 0601 0701 0801 0901
大括号与方括号的区别
方括号是用于文件名的模式匹配运算符(第二章)。另一方面,大括号扩展不依赖于文件名。它只是评估为一个字符串列表。您可以使用大括号扩展来打印文件名,但不会进行模式匹配:
$ ls
file1 file2 file4
$ ls file[2-4] *Matches existing filenames*
file2 file4
$ ls file{2..4} *Evaluates to: file2 file3 file4*
ls: cannot access 'file3': No such file or directory
file2 file4
大括号扩展还可以生成字母序列,而 seq 不能实现:
$ echo {A..Z}
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
大括号扩展总是在单行上生成以空格字符分隔的输出。通过将输出传输到其他命令,如 tr(参见“tr 命令”),可以改变这一点:
$ echo {A..Z} | tr -d ' ' *Delete spaces*
ABCDEFGHIJKLMNOPQRSTUVWXYZ
$ echo {A..Z} | tr ' ' '\n' *Change spaces into newlines*
A
B
C
⋮
Z
创建一个别名,用于打印英文字母表的第 n 个字母:
$ alias nth="echo {A..Z} | tr -d ' ' | cut -c"
$ nth 10
J
find 命令
find 命令递归列出目录中的文件,进入子目录并打印完整路径。^(1) 结果不是按字母顺序的(如果需要,将输出管道传输到 sort):
$ find /etc -print *List all of /etc recursively*
/etc
/etc/issue.net
/etc/nanorc
/etc/apache2
/etc/apache2/sites-available
/etc/apache2/sites-available/default.conf
⋮
find 具有许多可以组合的选项。以下是一些非常有用的选项。仅限制输出为文件或目录的 -type 选项:
$ find . -type f -print *Files only*
$ find . -type d -print *Directories only*
通过 -name 选项将输出限制为与文件名模式匹配的名称。引用或转义模式,以防止 shell 首先对其进行评估:
$ find /etc -type f -name "*.conf" -print *Files ending with .conf*
/etc/logrotate.conf
/etc/systemd/logind.conf
/etc/systemd/timesyncd.conf
⋮
使用 -iname 选项使名称匹配不区分大小写:
$ find . -iname "*.txt" -print
find 还可以使用 -exec 在输出中的每个文件路径上执行 Linux 命令。语法有些奇怪:
-
构造一个
find命令并省略-print。 -
在
-exec后面添加要执行的命令。使用表达式{}指示文件路径应该出现在命令中的位置。 -
以引号或转义分号结束,例如
";"或\;。
下面是一个玩具示例,打印文件路径两侧的 @ 符号:
$ find /etc -exec echo @ {} @ ";"
@ /etc @
@ /etc/issue.net @
@ /etc/nanorc @
⋮
更实际的例子是对 /etc 及其子目录中所有 .conf 文件执行长列表(ls -l):
$ find /etc -type f -name "*.conf" -exec ls -l {} ";"
-rw-r--r-- 1 root root 703 Aug 21 2017 /etc/logrotate.conf
-rw-r--r-- 1 root root 1022 Apr 20 2018 /etc/systemd/logind.conf
-rw-r--r-- 1 root root 604 Apr 20 2018 /etc/systemd/timesyncd.conf
⋮
find -exec 非常适合于整个目录结构中的大量文件删除(但要小心!)。让我们删除目录 $HOME/tmp 及其子目录中以波浪符(~)结尾的文件。为了安全起见,首先运行命令 echo rm 查看将要删除的文件,然后删除 echo 以实际删除:
$ find $HOME/tmp -type f -name "*~" -exec echo rm {} ";" *echo for safety*
rm /home/smith/tmp/file1~
rm /home/smith/tmp/junk/file2~
rm /home/smith/tmp/vm/vm-8.2.0b/lisp/vm-cus-load.el~
$ find $HOME/tmp -type f -name "*~" -exec rm {} ";" *Delete for real*
yes 命令
yes 命令打印相同的字符串,直到终止:
$ yes *Repeats "y" by default*
y
y
y ^C *Kill the command with Ctrl-C*
$ yes woof! *Repeat any other string*
woof!
woof!
woof! ^C
这种奇特行为有什么用处?yes可以为交互式程序提供输入,使其无人值守运行。例如,程序fsck检查 Linux 文件系统错误时,可能会提示用户继续并等待y或n的响应。将yes命令的输出通过管道传递给fsck,它将代表您回答每个提示,因此您可以离开让fsck完成运行。^(2)
对于我们的目的,yes的主要用途是通过将yes管道传递给head来多次打印一个字符串(您将在“生成测试文件”中看到一个实际示例):
$ yes "Efficient Linux" | head -n3 *Print a string 3 times*
Efficient Linux
Efficient Linux
Efficient Linux
隔离文本
当你只需要文件的一部分时,最简单的组合和运行命令是grep、cut、head和tail。你已经在第一章中看到了前三者的使用:grep打印匹配字符串的行,cut打印文件的列,head打印文件的前几行。新命令tail则是head的相反,打印文件的最后几行。图 5-1 展示了这四个命令如何一起工作。

图 5-1. head、grep和tail提取行,cut提取列。在这个例子中,grep匹配包含字符串“blandit”的行。
在本节中,我更深入地探讨了grep,它不仅仅匹配普通字符串,还解释了更正式的tail用法。我还预览了用于提取列的awk命令的一个功能,这是cut无法做到的。这五个命令的组合可以使用单一管道隔离几乎任何文本。
grep:深入探讨
你已经看到grep从文件中打印出匹配给定字符串的行:
$ cat frost
Whose woods these are I think I know.
His house is in the village though;
He will not see me stopping here
To watch his woods fill up with snow.
This is not the end of the poem.
$ grep his frost *Print lines containing "his"*
To watch his woods fill up with snow.
This is not the end of the poem. *"This" matches "his"*
grep还有一些非常有用的选项。使用-w选项仅匹配完整单词:
$ grep -w his frost *Match the word "his" exactly*
To watch his woods fill up with snow.
使用-i选项忽略大小写:
$ grep -i his frost
His house is in the village though; *Matches "His"*
To watch his woods fill up with snow. *Matches "his"*
This is not the end of the poem. *"This" matches "his"*
使用-l选项仅打印包含匹配行的文件名,而不包括匹配行本身:
$ grep -l his * *Which files contain the string "his"?*
frost
然而,grep的真正威力体现在超越匹配简单字符串,而是匹配称为正则表达式的模式时。^(3) 语法与文件名模式不同;部分描述在表 5-1 中。
表 5-1. grep、awk和sed共享的一些正则表达式语法^(a)
| 匹配此内容: | 使用此语法: | 示例 |
|---|---|---|
| 行的开头 | ^ |
^a = 以a开头的行 |
| 行尾 | ` | 匹配此内容: |
| --- | --- | --- |
| 行的开头 | ^ |
^a = 以a开头的行 |
| !$ = 以感叹号结尾的行 |
||
| 任意单个字符(换行符除外) | . |
… = 任意三个连续字符 |
字面上的插入符号、美元符号或任何其他特殊字符 c |
\c |
\$ = 字面上的美元符号 |
| 表达式 E 的零个或多个出现 | E* |
_* = 零个或多个下划线 |
| 集合中的任意单个字符 | [characters] |
[aeiouAEIOU] = 任意元音 |
| 不在集合中的任意单个字符 | [^characters] |
[^aeiouAEIOU] = 任意非元音 |
| 在给定范围内的任意字符 | [c[1]-c[2]] |
[0-9] = 任意数字 |
| 不在给定范围内的任意字符 | [^c[1]-c[2]] |
[⁰-9] = 任意非数字 |
两个表达式E[1]或E[2]中的任意一个 |
E[1]\|E[2] 用于 grep 和 sed |
one\|two = 要么是one要么是two |
E[1]\|E[2] 用于 awk |
one\|two = 要么是one要么是two |
|
分组表达式E用于优先级 |
\(E\) 用于 grep 和 sed ^(b) |
\(one|two\)* = 零个或多个one或two |
(one\|two)* 用于 awk |
(one\|two)* = 零个或多个one或two |
|
^(a) 这三个命令在处理正则表达式时也有所不同;表 5-1 展示了部分列表。^(b) 对于sed,此语法不仅仅是分组;详见“在 sed 中匹配子表达式”。 |
这里有一些带有正则表达式的示例grep命令。匹配所有以大写字母开头的行:
$ grep '^[A-Z]' myfile
匹配所有非空行(即匹配空行并使用-v排除它们):
$ grep -v '^$' myfile
匹配包含cookie或cake的所有行:
$ grep 'cookie\|cake' myfile
匹配至少五个字符长的所有行:
$ grep '.....' myfile
匹配所有包含小于号在大于号之前的行,例如 HTML 代码行:
$ grep '<.*>' page.html
正则表达式很强大,但有时候它们会妨碍你。假设你想要搜索frost文件中包含w后跟句点的两行。以下命令会产生错误结果,因为句点是正则表达式中的特殊字符,表示“任意字符”:
$ grep w. frost
Whose woods these are I think I know.
He will not see me stopping here
To watch his woods fill up with snow.
要解决这个问题,可以转义特殊字符:
$ grep 'w\.' frost
Whose woods these are I think I know.
To watch his woods fill up with snow.
但是,如果要转义许多特殊字符,这种解决方案将变得繁琐。幸运的是,你可以强制grep忘记正则表达式,并在输入中按字面意义搜索每个字符,方法是使用-F(“固定”)选项;或者,为了获得等效结果,可以使用fgrep代替grep:
$ grep -F w. frost
Whose woods these are I think I know.
To watch his woods fill up with snow.
$ fgrep w. frost
Whose woods these are I think I know.
To watch his woods fill up with snow.
grep还有许多其他选项;我将只介绍解决常见问题的一个。使用-f选项(小写;不要与-F混淆)来匹配一组字符串而不是单个字符串。作为实际示例,让我们列出文件/etc/passwd中找到的所有 shell,这是我在“命令#5:排序”中介绍过的。如你所记得的那样,/etc/passwd中的每行包含有关用户的信息,以冒号分隔的字段组织。每行的最后一个字段是用户登录时启动的程序。这个程序通常但并非总是一个 shell:
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash *7th field is a shell*
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin *7th field is not a shell*
⋮
如何确定一个程序是否是一个 shell?嗯,文件 /etc/shells 列出了 Linux 系统上所有有效的登录 shell:
$ cat /etc/shells
/bin/sh
/bin/bash
/bin/csh
因此,你可以通过提取第七个字段与 cut 结合,消除重复项并与 /etc/shells 中的结果进行比对,使用 grep -f 检查。我还添加了 -F 选项以提高谨慎性,因此 /etc/shells 中的所有行都会被字面量接受,即使它们包含特殊字符:
$ cut -d: -f7 /etc/passwd | sort -u | grep -f /etc/shells -F
/bin/bash
/bin/sh
tail 命令
tail 命令打印文件的最后几行,默认是 10 行。它是 head 命令的配对命令。假设你有一个名为 alphabet 的文件,包含着 26 行,每行一个字母:
$ cat alphabet
A is for aardvark
B is for bunny
C is for chipmunk
⋮
X is for xenorhabdus
Y is for yak
Z is for zebu
用 tail 打印最后三行。选项 -n 设置要打印的行数,就像 head 一样:
$ tail -n3 alphabet
X is for xenorhabdus
Y is for yak
Z is for zebu
如果在数字前加上加号 (+),则从该行号开始打印并继续到文件末尾。以下命令从文件的第 25 行开始打印:
$ tail -n+25 alphabet
Y is for yak
Z is for zebu
结合 tail 和 head 从文件中打印任何行范围。例如,要单独打印第四行,请提取前四行并单独隔离最后一行:
$ head -n4 alphabet | tail -n1
D is for dingo
通常,要打印第 M 至第 N 行,请提取前 N 行并用 tail 隔离最后 N-M+1 行。打印 alphabet 文件的第六至第八行:
$ head -n8 alphabet | tail -n3
F is for falcon
G is for gorilla
H is for hawk
提示
head 和 tail 都支持一个更简单的语法,用来指定行数而不需要 -n。这种语法已经非常古老,未记录并且已经被弃用,但很可能会一直被支持:
$ head -4 alphabet *Same as head -n4 alphabet*
$ tail -3 alphabet *Same as tail -n3 alphabet*
$ tail +25 alphabet *Same as tail -n+25 alphabet*
awk 的 {print} 命令
命令 awk 是一个通用的文本处理器,有着数百种用途。让我们预览一个小功能 print,它可以从文件中提取列,这是 cut 无法做到的。考虑系统文件 /etc/hosts,其中包含以任意数量的空白分隔的 IP 地址和主机名:
$ less /etc/hosts
127.0.0.1 localhost
127.0.1.1 myhost myhost.example.com
192.168.1.2 frodo
192.168.1.3 gollum
192.168.1.28 gandalf
假设你想通过打印每行的第二个单词来隔离主机名。挑战在于每个主机名之前都有任意数量的空白。cut 需要通过列号 (-c) 或由单一一致字符分隔的列 (-f) 来操作它的列。你需要一个命令来打印每行的第二个单词,awk 可以轻松提供:
$ awk '{print $2}' /etc/hosts
localhost
myhost
frodo
gollum
gandalf
awk 通过美元符号加上列号来引用任何列:例如,第七列为 $7。如果列号有多位数,请用括号括起来,例如 $(25)。要引用最后一个字段,请使用 $NF(“字段数量”)。要引用整行,请使用 $0。
awk 默认情况下不会在值之间打印空白。如果需要空白,请用逗号分隔值:
$ echo Efficient fun Linux | awk '{print $1 $3}' *No whitespace*
EfficientLinux
$ echo Efficient fun Linux | awk '{print $1, $3}' *Whitespace*
Efficient Linux
awk 的 print 语句非常适合处理超出整齐列的命令输出。例如 df,它打印 Linux 系统上的空闲和已用磁盘空间数量:
$ df / /data
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda1 1888543276 902295944 890244772 51% /
/dev/sda2 7441141620 1599844268 5466214400 23% /data
根据 Filesystem 路径的长度、磁盘大小和您传递给 df 的选项,列位置可能会有所不同,因此您不能可靠地使用 cut 提取值。然而,使用 awk,您可以轻松地隔离(比如)每行的第四个值,表示可用磁盘空间:
$ df / /data | awk '{print $4}'
Available
890244772
5466214400
并且还可以同时使用一点 awk 魔法删除第一行(标题),仅打印大于 1 的行号:
$ df / /data | awk 'FNR>1 {print $4}'
890244772
5466214400
如果遇到除空格字符外的其他分隔符的输入,awk 可以使用 -F 选项将其字段分隔符更改为任何正则表达式:
$ echo efficient:::::linux | awk -F':*' '{print $2}' *Any number of colons*
linux
您将在 “awk essentials” 中了解更多有关 awk 的详细信息。
结合文本
您已经了解了几个从不同文件组合文本的命令。首先是 cat,它将多个文件的内容打印到 stdout。它是文件自顶向下的连接器。这就是它的名字的由来——它连接文件:
$ cat poem1
It is an ancient Mariner,
And he stoppeth one of three.
$ cat poem2
'By thy long grey beard and glittering eye,
$ cat poem3
Now wherefore stopp'st thou me?
$ cat poem1 poem2 poem3
It is an ancient Mariner,
And he stoppeth one of three.
'By thy long grey beard and glittering eye,
Now wherefore stopp'st thou me?
您已经看到的第二个组合文本的命令是 echo,它是打印您给它的任何参数,以单个空格字符分隔的 shell 内建。它将字符串并排组合在一起:
$ echo efficient linux in $HOME
efficient linux in /home/smith
让我们再看一些结合文本的命令:
tac
文本文件的自底向上组合器
paste
文本文件的并排组合器
diff
通过打印它们的差异,交错来自两个文件的文本的命令
tac 命令
tac 命令逐行反转文件。它的名称是 cat 的倒写。
$ cat poem1 poem2 poem3 | tac
Now wherefore stopp'st thou me?
'By thy long grey beard and glittering eye,
And he stoppeth one of three.
It is an ancient Mariner,
请注意,在反转文本之前,我将三个文件串联起来。如果我向 tac 提供多个文件作为参数,它会依次反转每个文件的行,生成不同的输出:
$ tac poem1 poem2 poem3
And he stoppeth one of three. *First file reversed*
It is an ancient Mariner,
'By thy long grey beard and glittering eye, *Second file*
Now wherefore stopp'st thou me? *Third file*
tac 非常适合处理已按时间顺序排列但无法使用 sort -r 命令反转的数据。典型情况是反转 Web 服务器日志文件,以处理其从最新到最旧的行。
192.168.1.34 - - [30/Nov/2021:23:37:39 -0500] "GET / HTTP/1.1" ...
192.168.1.10 - - [01/Dec/2021:00:02:11 -0500] "GET /notes.html HTTP/1.1" ...
192.168.1.8 - - [01/Dec/2021:00:04:30 -0500] "GET /stuff.html HTTP/1.1" ...
⋮
行按时间戳的顺序排列,但不是按字母或数字顺序排列,因此 sort -r 命令无法帮助。tac 命令可以反转这些行,而无需考虑时间戳。
paste 命令
paste 命令将文件以单个制表符分隔的列侧边合并在一起。它是 cut 命令的伙伴,后者从制表符分隔的文件中提取列:
$ cat title-words1
EFFICIENT
AT
COMMAND
$ cat title-words2
linux
the
line
$ paste title-words1 title-words2
EFFICIENT linux
AT the
COMMAND line
$ paste title-words1 title-words2 | cut -f2 *cut & paste are complementary*
linux
the
line
使用 -d 选项(意为“分隔符”),将分隔符更改为另一个字符,如逗号:
$ paste -d, title-words1 title-words2
EFFICIENT,linux
AT,the
COMMAND,line
使用 -s 选项转置输出,生成粘贴的行而不是粘贴的列:
$ paste -d, -s title-words1 title-words2
EFFICIENT,AT,COMMAND
linux,the,line
paste 还可以将来自两个或多个文件的数据交错,如果将分隔符更改为换行符(\n):
$ paste -d "\n" title-words1 title-words2
EFFICIENT
linux
AT
the
COMMAND
line
diff 命令
diff 逐行比较两个文件,并打印它们之间差异的简洁报告:
$ cat file1
Linux is all about efficiency.
I hope you will enjoy this book.
$ cat file2
MacOS is all about efficiency.
I hope you will enjoy this book.
Have a nice day.
$ diff file1 file2
1c1
< Linux is all about efficiency.
---
> MacOS is all about efficiency.
2a3
> Have a nice day.
符号 1c1 表示文件之间的变化或差异。它表示第一个文件中的第一行与第二个文件中的第一行不同。此符号后跟来自 file1 的相关行,一个三个破折号分隔符 (---),以及来自 file2 的相关行。前导符号 < 总是表示第一个文件的行,而 > 表示第二个文件的行。
符号 2a3 表示添加。它表示 file2 在第二行之后有一个第三行。此符号后跟来自 file2 的额外行,“祝您今天愉快。”
diff 输出可能包含其他符号,并且可以采用其他形式。然而,对于我们的主要目的而言,这个简短的解释已经足够,即将 diff 作为一个交错两个文件行的文本处理器使用。许多用户并不将 diff 看作这样的工具,但它非常适合形成管道以解决某些类型的问题。例如,您可以使用 grep 和 cut 隔离不同的行:
$ diff file1 file2 | grep '^[<>]'
< Linux is all about efficiency.
> MacOS is all about efficiency.
> Have a nice day.
$ diff file1 file2 | grep '^[<>]' | cut -c3-
Linux is all about efficiency.
MacOS is all about efficiency.
Have a nice day.
您将在 “技巧#4:过程替换” 和 “检查匹配的文件对” 中看到实际示例。
转换文本
第一章 介绍了几个从 stdin 读取文本并将其转换为 stdout 中其他内容的命令。wc 打印行数、单词数和字符数;sort 按字母或数字顺序排列行;uniq 合并重复行。让我们讨论另外几个将其输入转换的命令:
tr
将字符转换为其他字符
rev
反转行中的字符
awk 和 sed
通用转换器
tr 命令
tr 将一个字符集转换为另一个字符集。在 第二章 中,我展示了一个将冒号转换为换行符以打印 shell 的 PATH 的示例:
$ echo $PATH | tr : "\n" *Translate colons into newlines*
/home/smith/bin
/usr/local/bin
/usr/bin
/bin
/usr/games
/usr/lib/java/bin
tr 接受两组字符作为参数,并将第一组的成员转换为第二组的对应成员。常见用途是将文本转换为大写或小写:
$ echo efficient | tr a-z A-Z *Translate a into A, b into B, etc.*
EFFICIENT
$ echo Efficient | tr A-Z a-z
efficient
将空格转换为换行符:
$ echo Efficient Linux | tr " " "\n"
Efficient
Linux
并使用 -d(删除)选项删除空白:
$ echo efficient linux | tr -d ' \t' *Remove spaces and tabs*
efficientlinux
rev 命令
rev 命令反转每行输入的字符:^(4)
$ echo Efficient Linux! | rev
!xuniL tneiciffE
除了明显的娱乐价值外,rev 对于从文件中提取棘手信息非常有用。假设您有一个名人姓名文件:
$ cat celebrities
Jamie Lee Curtis
Zooey Deschanel
Zendaya Maree Stoermer Coleman
Rihanna
如果您想从每行提取最后一个单词(Curtis、Deschanel、Coleman、Rihanna)。如果每行具有相同数量的字段,则使用 cut -f 很容易实现,但字段数量会有所变化。使用 rev,您可以反转所有行,剪切第一个字段,然后再次反转以实现您的目标:^(5)
$ rev celebrities
sitruC eeL eimaJ
lenahcseD yeooZ
nameloC remreotS eeraM ayadneZ
annahiR
$ rev celebrities | cut -d' ' -f1
sitruC
lenahcseD
nameloC
annahiR
$ rev celebrities | cut -d' ' -f1 | rev
Curtis
Deschanel
Coleman
Rihanna
awk 和 sed 命令
awk 和 sed 是处理文本的通用“超级命令”。它们可以完成本章中其他命令的大部分功能,但语法看起来更加神秘。举个简单的例子,它们可以像 head 一样打印文件的前 10 行:
$ sed 10q myfile *Print 10 lines and quit (q)*
$ awk 'FNR<=10' myfile *Print while line number is ≤ 10*
它们还可以执行我们其他命令无法执行的操作,比如替换或交换字符串:
$ echo image.jpg | sed 's/\.jpg/.png/' *Replace .jpg by .png*
image.png
$ echo "linux efficient" | awk '{print $2, $1}' *Swap two words*
efficient linux
awk 和 sed 比我讲过的其他命令更难学习,因为它们每个都内置了一个微型编程语言。它们有很多功能,以至于整本书都有人写给它们。(参见 ^(6)我强烈建议花时间学习这两个命令(或至少其中一个)。要开始你的旅程,我介绍了每个命令的基本原理,并演示了一些常见用途。我还推荐了几个在线教程,以深入了解这些强大而关键的命令。
不要担心记住每一个awk或sed的功能。成功使用这些命令真正意味着:
-
理解它们可以实现的转换类型,这样你可以想到,“啊!这是
awk(或sed)的工作!”并在需要时应用它们。 -
学会阅读它们的 man 页面,并在 Stack Exchange 和其他在线资源上找到完整的解决方案
awk 的基本要素
awk 将文件(或 stdin)的文本行转换为任何其他文本,使用称为awk 程序的一系列指令。^(7) 你在编写 awk 程序方面的技能越高,就越能灵活地操纵文本。你可以在命令行上提供 awk 程序:
$ awk *program* *input-files*
你还可以将一个或多个 awk 程序存储在文件中,并使用 -f 选项引用它们,程序将按顺序运行:
$ awk -f *program-file1* -f *program-file2* -f *program-file3* *input-files*
一个 awk 程序包括一个或多个操作,比如计算值或打印文本,在输入行匹配模式时运行。程序中的每条指令的形式为:
*pattern* {*action*}
典型的模式包括:
单词 BEGIN
它的操作只运行一次,在 awk 处理任何输入之前。
单词 END
它的操作只运行一次,在awk处理完所有输入后。
用斜杠括起来的正则表达式(见 表 5-1)
一个例子是 /^[A-Z]/ 用于匹配以大写字母开头的行。
其他特定于 awk 的表达式
例如,要检查输入行的第三个字段($3)是否以大写字母开头,可以使用模式 $3~/^[A-Z]/。另一个例子是 FNR>5,它告诉 awk 跳过输入的前五行。
没有模式的操作对每一行输入运行一次。(在 “awk {print} 命令” 中有几个这种类型的 awk 程序。)例如,awk 通过直接打印每行的最后一个词,优雅地解决了来自 “rev 命令” 的“打印名人的姓氏”问题:
$ awk '{print $NF}' celebrities
Curtis
Deschanel
Coleman
Rihanna
提示
在命令行上提供一个awk程序时,用引号括起来以防止 shell 评估awk的特殊字符。根据需要使用单引号或双引号。
没有任何操作的模式运行默认操作{print},只是打印任何匹配的输入行,不做任何更改:
$ echo efficient linux | awk '/efficient/'
efficient linux
为了更充分地演示,处理来自示例 1-1 的制表符分隔文件animals.txt,以生成整洁的参考书目,将行从这种格式转换为:
python Programming Python 2010 Lutz, Mark
到这个格式:
Lutz, Mark (2010). "Programming Python"
这需要重新排列三个字段,并添加一些字符如括号和双引号。以下awk程序可以做到这一点,使用选项-F将输入分隔符从空格更改为制表符(\t):
$ awk -F'\t' '{print $4, "(" $3 ").", "\"" $2 "\""}' animals.txt
Lutz, Mark (2010). "Programming Python"
Barrett, Daniel (2005). "SSH, The Secure Shell"
Schwartz, Randal (2012). "Intermediate Perl"
Bell, Charles (2014). "MySQL High Availability"
Siever, Ellen (2009). "Linux in a Nutshell"
Boney, James (2005). "Cisco IOS in a Nutshell"
Roman, Steven (1999). "Writing Word Macros"
添加一个正则表达式来处理只有“horse”书籍:
$ awk -F'\t' '/^horse/{print $4, "(" $3 ").", "\"" $2 "\""}' animals.txt
Siever, Ellen (2009). "Linux in a Nutshell"
或者仅处理 2010 年或之后的书籍,通过测试字段$3是否匹配²⁰¹:
$ awk -F'\t' '$3~/²⁰¹/{print $4, "(" $3 ").", "\"" $2 "\""}' animals.txt
Lutz, Mark (2010). "Programming Python"
Schwartz, Randal (2012). "Intermediate Perl"
Bell, Charles (2014). "MySQL High Availability"
最后,添加一个BEGIN指令打印友好的标题,一些短划线作为缩进,和一个END指令引导读者进一步了解信息:
$ awk -F'\t' \
'BEGIN {print "Recent books:"} \
$3~/²⁰¹/{print "-", $4, "(" $3 ").", "\"" $2 "\""} \
END {print "For more books, search the web"}' \
animals.txt
Recent books:
- Lutz, Mark (2010). "Programming Python"
- Schwartz, Randal (2012). "Intermediate Perl"
- Bell, Charles (2014). "MySQL High Availability"
For more books, search the web
awk不仅可以打印,它还可以执行计算,比如对 1 到 100 的数字求和:
$ seq 1 100 | awk '{s+=$1} END {print s}'
5050
要了解awk超出几页书可以覆盖的内容,可以在tutorialspoint.com/awk或riptutorial.com/awk上参加awk教程,或者搜索“awk 教程”。你会感到高兴的。
改进重复文件检测器
在“检测重复文件”中,您构建了一个通过检验检验和检测和计数重复 JPEG 文件的管道,但它不足以打印文件名:
$ md5sum *.jpg | cut -c1-32 | sort | uniq -c | sort -nr | grep -v " 1 "
3 f6464ed766daca87ba407aede21c8fcc
2 c7978522c58425f6af3f095ef1de1cd5
2 146b163929b6533f02e91bdf21cb9563
现在你已经了解了awk,你可以打印文件名。让我们构建一个新命令,读取md5sum输出的每一行:
$ md5sum *.jpg
146b163929b6533f02e91bdf21cb9563 image001.jpg
63da88b3ddde0843c94269638dfa6958 image002.jpg
146b163929b6533f02e91bdf21cb9563 image003.jpg
⋮
不仅计算每个检验和的出现次数,还存储文件名以供打印。您将需要两个名为数组和循环的附加awk功能。
一个数组是一个保存值集合的变量。如果数组命名为A并保存了七个值,则可以访问值A[1]、A[2]、A[3],一直到A[7]。1 到 7 的值称为数组的键,而A[1]到A[7]称为数组的元素。你可以创建任何你想要的键。如果你更愿意使用迪士尼角色的名字访问你的数组的七个元素,那就这样命名它们A["Doc"]、A["Grumpy"]、A["Bashful"],一直到A["Dopey"]。
要计算重复图像,创建一个名为 counts 的数组,其中每个校验和有一个元素。每个数组键都是一个校验和,关联的元素包含输入中该校验和出现的次数。例如,数组元素 counts["f6464ed766daca87ba407aede21c8fcc"] 可能有值 3. 以下 awk 脚本检查 md5sum 输出的每行,隔离校验和 ($1),并将其用作 counts 数组的键。运算符 ++ 每次 awk 遇到其关联的校验和时将其元素增加 1:
$ md5sum *.jpg | awk '{counts[$1]++}'
到目前为止,awk 脚本并不生成任何输出 — 它只是计算每个校验和并退出。要打印计数,需要使用 awk 的第二个特性,称为 for 循环。for 循环逐个键步进数组,并依次处理每个元素,语法如下:
for (*variable* in *array*) *do something with array[variable]*
例如,按其键打印每个数组元素:
for (key in counts) print array[key]
将此循环放入 END 指令中,以便在计算完所有计数后运行。
$ md5sum *.jpg \
| awk '{counts[$1]++} \
END {for (key in counts) print counts[key]}'
1
2
2
⋮
接下来,将校验和添加到输出中。每个数组键都是一个校验和,因此只需在计数后打印键即可:
$ md5sum *.jpg \
| awk '{counts[$1]++} \
END {for (key in counts) print counts[key] " " key}'
1 714eceeb06b43c03fe20eb96474f69b8
2 146b163929b6533f02e91bdf21cb9563
2 c7978522c58425f6af3f095ef1de1cd5
⋮
要收集并打印文件名,还需使用第二个数组 names,其中同样以校验和为键。当 awk 处理每行输出时,将文件名 ($2) 附加到 names 数组的相应元素后,使用空格作为分隔符。在 END 循环中,在打印校验和 (key) 后,打印冒号和该校验和对应的收集文件名:
$ md5sum *.jpg \
| awk '{counts[$1]++; names[$1]=names[$1] " " $2} \
END {for (key in counts) print counts[key] " " key ":" names[key]}'
1 714eceeb06b43c03fe20eb96474f69b8: image011.jpg
2 146b163929b6533f02e91bdf21cb9563: image001.jpg image003.jpg
2 c7978522c58425f6af3f095ef1de1cd5: image019.jpg image020.jpg
⋮
以 1 开头的行表示仅出现一次的校验和,因此它们不是重复项。将输出管道传递给 grep -v 以删除这些行,然后使用 sort -nr 对结果进行数值排序,从高到低,即可得到所需的输出:
$ md5sum *.jpg \
| awk '{counts[$1]++; names[$1]=names[$1] " " $2} \
END {for (key in counts) print counts[key] " " key ":" names[key]}' \
| grep -v '¹ ' \
| sort -nr
3 f6464ed766daca87ba407aede21c8fcc: image007.jpg image012.jpg image014.jpg
2 c7978522c58425f6af3f095ef1de1cd5: image019.jpg image020.jpg
2 146b163929b6533f02e91bdf21cb9563: image001.jpg image003.jpg
sed 基础知识
sed,像 awk 一样,可以将文件(或标准输入)中的文本转换为任何其他文本,使用一系列称为 sed 脚本 的指令。^(8) 初看起来 sed 脚本非常神秘。例如,s/Windows/Linux/g 表示将每个 Windows 字符串替换为 Linux。这里的 脚本 不是指文件(如 shell 脚本),而是指一个字符串。^(9) 在命令行上调用 sed 时,只需使用单个脚本:
$ sed **script** *input-files*
或者使用 -e 选项提供多个按顺序处理输入的脚本:
$ sed -e **script1** -e **script2** -e **script3** *input-files*
您还可以将 sed 脚本存储在文件中,并使用 -f 选项引用它们,它们按顺序运行:
$ sed -f *script-file1* -f *script-file2* -f *script-file3* *input-files*
与 awk 一样,sed 的实用性取决于您在创建 sed 脚本方面的技能。最常见的脚本类型是替换脚本,它用一个字符串替换另一个字符串。语法如下:
s/*regexp*/*replacement*/
其中 regexp 是要与每个输入行匹配的正则表达式(见 表 5-1),replacement 是要替换匹配文本的字符串。例如,将一个单词更改为另一个简单的示例:
$ echo Efficient Windows | sed "s/Windows/Linux/"
Efficient Linux
提示
当在命令行上提供一个 sed 脚本时,用引号括起来,以防止 shell 评估 sed 的特殊字符。根据需要使用单引号或双引号。
sed可以通过正则表达式轻松解决来自“rev 命令”的“打印名人姓氏”的问题。只需匹配所有字符(.*)直到最后一个空格,并将它们替换为空:
$ sed 's/.* //' celebrities
Curtis
Deschanel
Coleman
Rihanna
替换和斜杠
替换中的斜杠可以用任何其他方便的字符替换。当正则表达式本身包含斜杠时(否则需要转义),这将非常有帮助。这三个sed脚本是等效的:
s/one/two/ s_one_two_ s@one@two@
您可以在替换之后跟随几个选项以影响其行为。选项i使匹配不区分大小写:
$ echo Efficient Stuff | sed "s/stuff/linux/" *Case sensitive; no match*
Efficient Stuff
$ echo Efficient Stuff | sed "s/stuff/linux/i" *Case-insensitive match*
Efficient linux
选项g(“全局”)替换所有匹配正则表达式的实例,而不仅仅是第一个:
$ echo efficient stuff | sed "s/f/F/" *Replaces just the first "f"*
eFficient stuff
$ echo efficient stuff | sed "s/f/F/g" *Replaces all occurrences of "f"*
eFFicient stuFF
另一种常见的sed脚本类型是删除脚本。它根据它们的行号删除行:
$ seq 10 14 | sed 4d *Remove the 4th line*
10
11
12
14
或者匹配正则表达式的行:
$ seq 101 200 | sed '/[13579]$/d' *Delete lines ending in an odd digit*
102
104
106
⋮
200
使用 sed 匹配子表达式
假设你有一些文件名:
$ ls
image.jpg.1 image.jpg.2 image.jpg.3
并且想要生成新的名称,image1.jpg,image2.jpg和image3.jpg。sed可以将文件名拆分为部分并通过子表达式重新排列它们。首先,创建一个匹配文件名的正则表达式:
image\.jpg\.[1-3]
您希望将最终数字移动到文件名的前面,因此用符号\(和\)将该数字孤立出来。这定义了一个子表达式——正则表达式的指定部分:
image\.jpg\.\([1-3]\)
sed可以按编号引用子表达式并操作它们。您只创建了一个子表达式,所以它的名称是\1。第二个子表达式将是\2,依此类推,最多可以到\9。您的新文件名将具有形式image\1.jpg。因此,您的 sed 脚本将是:
$ ls | sed "s/image\.jpg\.\([1-3]\)/image\1.jpg/"
image1.jpg
image2.jpg
image3.jpg
为了使事情更加复杂,假设文件名有更多变化,由小写单词组成:
$ ls
apple.jpg.1 banana.png.2 carrot.jpg.3
创建三个子表达式以捕获基本文件名、扩展名和最终数字:
\([a-z][a-z]*\) *\1 = Base filename of one letter or more*
\([a-z][a-z][a-z]\) *\2 = File extension of three letters*
\([0-9]\) *\3 = A digit*
用转义点(\.)将它们连接起来形成这个正则表达式:
\([a-z][a-z]*\)\.\([a-z][a-z][a-z]\)\.\([0-9]\)
将新转换后的文件名表示为\1\3.\2,最终与sed的替换变成:
$ ls | sed "s/\([a-z][a-z]*\)\.\([a-z][a-z][a-z]\)\.\([0-9]\)/\1\3.\2/"
apple1.jpg
banana2.png
carrot3.jpg
此命令不会重命名文件——它只是打印新名称。章节“在序列中插入文件名”展示了一个类似的示例,它还执行了重命名操作。
要深入学习sed,超出了几页书籍的内容,可以在tutorialspoint.com/sed或grymoire.com/Unix/Sed.html上参加sed教程,或在网络上搜索“sed 教程”。
朝着一个更大的工具箱前进
大多数 Linux 系统都附带成千上万个命令行程序,它们中的大多数都有许多选项可以改变其行为。您不太可能学习并记住它们全部。因此,在需要时,如何找到一个新程序或调整您已经了解的程序以实现您的目标?
您的第一(显而易见的)步骤是使用网络搜索引擎。例如,如果您需要一个命令来限制文本文件中行的宽度,自动换行任何太长的行,请搜索“Linux 命令 wrap lines”,您将会找到 fold 命令:
$ cat title.txt
This book is titled "Efficient Linux at the Command Line"
$ fold -w40 title.txt
This book is titled "Efficient Linux at
the Command Line"
要发现已安装在您的 Linux 系统上的命令,运行命令 man -k(或者等价地,apropos)。给定一个单词,man -k 在 man 手册顶部的简短描述中搜索该单词:
$ man -k width
DisplayWidth (3) - image format functions and macros
DisplayWidthMM (3) - image format functions and macros
fold (1) - wrap each input line to fit in specified width
⋮
man -k 接受 awk 风格的正则表达式作为搜索字符串(参见 表 5-1):
$ man -k "wide|width"
在您的系统上未安装的命令可能仍然可以通过系统的软件包管理器安装。软件包管理器是用于安装适用于您系统的 Linux 程序的软件。一些流行的软件包管理器包括 apt、dnf、emerge、pacman、rpm、yum 和 zypper。使用 man 命令来确定您系统上安装了哪个软件包管理器,并了解如何搜索未安装的软件包。通常是一个两条命令的序列:一条命令将来自互联网的最新可用软件包数据(“元数据”)复制到您的系统上,另一条命令用于搜索元数据。例如,对于基于 Ubuntu 或 Debian 的 Linux 系统,命令如下:
$ sudo apt update *Download the latest metadata*
$ apt-file search *string* *Search for a string*
如果长时间搜索后仍无法找到或构建一个满足您需求的适当命令,请考虑在在线论坛上寻求帮助。提问的一个很好的起点是 Stack Overflow 的 “如何提出一个好问题?”帮助页面。总的来说,请尊重他人的时间来提问问题,专家们将更倾向于回答。这意味着让您的问题简短明了,包括任何错误消息或其他输出文字,并解释您自己已经尝试了什么。花时间质问质答:这不仅会增加获得有帮助答案的机会,而且如果论坛是公开可搜索的,清晰的问题和答案可能会帮助其他有类似问题的人。
摘要
您现在已经超越了 第 1 章 中的迷你工具箱,并准备在命令行中解决更具挑战性的业务问题。接下来的章节中充满了在各种情况下使用新命令的实际示例。
^(1) 相关命令 ls -R 生成的输出格式不太适合用于管道操作。
^(2) 现在,一些 fsck 的实现具有选项 -y 和 -n,分别对每个提示回答是或否,因此此处不需要 yes 命令。
^(3) grep 的名称缩写为 “get regular expression and print”。
^(4) 测验:管道 rev myfile | tac | rev | tac 做什么?
^(5) 不久你将看到使用 awk 和 sed 的简单解决方案,但这种双rev技巧很实用。
^(6) 包括 O'Reilly 出版的书籍 sed & awk。
^(7) awk 的名字是由程序的创作者 Aho、Weinberger 和 Kernighan 的首字母组成的缩写。
^(8) sed 的名字缩写来自于“流编辑器(stream editor)”,因为它编辑文本流。
^(9) 如果你熟悉编辑器 vi、vim、ex 或者 ed,那么 sed 脚本语法可能看起来很熟悉。
第六章:父进程、子进程和环境
shell 的目的——运行命令——对于 Linux 来说是如此基本,以至于您可能认为 shell 是以某种特殊方式内置到 Linux 中的。事实并非如此。shell 就像ls或cat一样是一个普通程序。它被编程为一遍又一遍地重复以下步骤……
-
打印提示符。
-
从 stdin 中读取命令。
-
评估并运行该命令。
Linux 在很大程度上隐藏了 shell 是一个普通程序的事实。当您登录时,Linux 会自动为您运行一个 shell 实例,称为您的登录 shell。它的启动如此无缝,以至于看起来它是 Linux,但实际上它只是一个代表您启动的程序,用于与 Linux 交互。
您的登录 shell 在哪里?
如果您在非图形终端上登录,例如使用 SSH 客户端程序,登录 shell 就是您与之交互的初始 shell。它打印第一个提示符并等待您的命令。
或者,如果您在计算机的控制台上有图形显示,您的登录 shell 会在幕后运行。它启动桌面环境,如 GNOME、Unity、Cinnamon 或 KDE Plasma。然后您可以打开终端窗口来运行额外的交互式 shell。
对 shell 的理解越多,您与 Linux 的有效工作能力就越高,对其内部工作原理的迷信就越少。本章比第二章更深入地探讨了以下 shell 的奥秘:
-
shell 程序的位置
-
不同的 shell 实例之间如何关联
-
为什么不同的 shell 实例可能具有相同的变量、值、别名和其他上下文
-
如何通过编辑配置文件更改 shell 的默认行为
最后,希望您会发现,这些神秘并不那么神秘。
Shell 是可执行文件
大多数 Linux 系统上的默认 shell 是bash,^(1) 它只是一个普通程序——一个可执行文件——位于系统目录/bin,与cat、ls、grep和其他熟悉的命令一起:
$ cd /bin
$ ls -l bash cat ls grep
-rwxr-xr-x 1 root root 1113504 Jun 6 2019 bash
-rwxr-xr-x 1 root root 35064 Jan 18 2018 cat
-rwxr-xr-x 1 root root 219456 Sep 18 2019 grep
-rwxr-xr-x 1 root root 133792 Jan 18 2018 ls
bash 也不一定是您系统上唯一的 shell。通常在文件/etc/shells中列出有效的 shell,每行一个:
$ cat /etc/shells
/bin/sh
/bin/bash
/bin/csh
/bin/zsh
要查看您正在运行的 shell,请echo shell 变量SHELL:
$ echo $SHELL
/bin/bash
理论上,Linux 系统可以将任何程序视为有效的登录 shell,如果用户帐户配置为在登录时调用它,并且它在/etc/shells中列出(如果在您的系统上需要)。使用超级用户权限,您甚至可以编写和安装自己的 shell,就像示例 6-1 中的脚本一样。它读取任何命令并回应:“对不起,我不能做到。”这个自定义 shell 故意愚蠢,但它证明了其他程序可以像/bin/bash一样成为合法的 shell。
示例 6-1。halshell:一个拒绝运行您命令的 shell
#!/bin/bash
# Print a prompt
echo -n '$ '
# Read the user's input in a loop. Exit when the user presses Ctrl-D.
while read line; do
# Ignore the input $line and print a message
echo "I'm sorry, I'm afraid I can't do that"
# Print the next prompt
echo -n '$ '
done
由于bash只是一个程序,您可以像运行其他任何命令一样手动运行它:
$ bash
如果你这样做,你只会看到另一个提示符,就好像你的命令没有产生任何效果:
$
但实际上,你已经运行了一个新的 bash 实例。这个新实例会打印一个提示符,并等待你的命令。为了使新实例更加可见,通过设置 shell 变量 PS1 将其提示符(比如改为 %%),并运行一些命令:
$ PS1="%% "
%% ls *The prompt has changed*
animals.txt
%% echo "This is a new shell"
This is a new shell
现在运行 exit 来终止新的 bash 实例。你会回到原始的 shell,它有一个美元符号提示符:
%% exit
$
我必须强调,从%%切换回$并不是一个提示符的改变。这是整个 shell 的变化。新的 bash 实例已经结束,所以原始 shell 会提示你输入下一个命令。
手动运行 bash 不仅仅是为了娱乐价值。你会在第七章中利用手动调用的 shell。
父进程和子进程
当一个 shell 实例调用另一个 shell 实例时,就像我刚才演示的那样,原始 shell 被称为父进程,新实例称为子进程。对于任何一个 Linux 程序调用另一个 Linux 程序也是如此。调用的程序是父进程,被调用的程序是它的子进程。运行中的 Linux 程序称为进程,因此你也会看到父进程和子进程这些术语。一个进程可以调用任意数量的子进程,但每个子进程只有一个父进程。
每个进程都有自己的环境。一个环境,你可能还记得来自“环境和初始化文件简介”,包括当前目录,搜索路径,shell 提示符和其他重要信息保存在 shell 变量中。当创建一个子进程时,其环境主要是父进程环境的副本。(我将在“环境变量”中进一步解释。)
每次运行简单命令时,你都创建一个子进程。这是理解 Linux 的一个非常重要的观点,我会再说一遍:即使你运行像ls这样的简单命令,该命令实际上是在一个新的子进程中以其自己(复制的)环境中运行的。这意味着你对子进程所做的任何更改,比如在子 shell 中更改提示变量 PS1,只影响子进程,当子进程退出时这些更改都会丢失。同样地,对父进程的任何更改都不会影响已经运行的子进程。然而,父进程的更改可以影响将来的子进程,因为每个子进程的环境都是从其父进程的环境中复制过来的。
为什么在子进程中运行命令很重要?首先,这意味着你运行的任何程序都可以在整个文件系统中进行cd,但退出后,你当前的 shell(即父进程)的当前目录并未改变。下面是一个快速实验来证明这一点。在你的主目录中创建一个名为cdtest的 shell 脚本,其中包含一个cd命令:
#!/bin/bash
cd /etc
echo "Here is my current directory:"
pwd
让它变得可执行:
$ chmod +x cdtest
打印当前目录名称,然后运行脚本:
$ pwd
/home/smith
$ ./cdtest
Here is my current directory:
/etc
现在检查你的当前目录:
$ pwd
/home/smith
您当前的目录并没有改变,即使cdtest脚本已经移动到/etc目录。这是因为cdtest在具有自己环境的子进程中运行。子进程的环境更改不会影响父进程的环境,因此父进程的当前目录没有改变。当您运行像cat或grep这样的可执行程序时也是一样——它们在运行在子进程中,在程序终止后退出,带走任何环境更改。
为何 cd 必须是 Shell 内建功能
如果 Linux 程序无法更改您的 Shell 当前目录,那么cd命令如何管理更改呢?嗯,cd并不是一个程序。它是 Shell 的内建特性(即 Shell 内建)。如果cd是 Shell 外部的程序,目录更改将是不可能的——它们将在子进程中运行,并且无法影响父进程。
管道会启动多个子进程:每个管道中的命令一个子进程。此命令来自“命令#6:uniq”部分,启动了六个子进程:
$ cut -f1 grades | sort | uniq -c | sort -nr | head -n1 | cut -c9
环境变量
您已经了解到,每个 Shell 实例都有一组变量,如您在“变量评估”中所学。某些变量是局部变量,限于单个 Shell。它们称为本地变量。其他变量会自动从给定 Shell 复制到它调用的每个子 Shell 中。这些变量称为环境变量,并且它们共同形成 Shell 的环境。环境变量及其用途的一些示例包括:
HOME
指向您的主目录的路径。登录时,您的登录 Shell 会自动设置其值。像vim和emacs这样的文本编辑器会读取HOME变量以定位和读取它们的配置文件(\(HOME/.vim* 和 *\)HOME/.emacs)。
PWD
您的 Shell 当前目录。每次使用cd切换到另一个目录时,Shell 会自动设置和维护其值。pwd命令读取PWD变量以打印 Shell 当前目录的名称。
EDITOR
您首选文本编辑器的名称(或路径)。通常由您在 Shell 配置文件中设置其值。其他程序会读取此变量以代表您启动适当的编辑器。
使用printenv命令查看 Shell 的环境变量。输出结果每行一个变量,未排序,可能非常长,因此可以通过sort和less进行友好查看:^(2)
$ printenv | sort -i | less
⋮
DISPLAY=:0
EDITOR=emacs
HOME=/home/smith
LANG=en_US.UTF-8
PWD=/home/smith/Music
SHELL=/bin/bash
TERM=xterm-256color
USER=smith
⋮
本地变量不会出现在printenv的输出中。通过在变量名前加上美元符号并使用echo打印结果来显示它们的值:
$ title="Efficient Linux"
$ echo $title
Efficient Linux
$ printenv title *(produces no output)*
创建环境变量
要将本地变量转换为环境变量,请使用export命令:
$ MY_VARIABLE=10 *A local variable*
$ export MY_VARIABLE *Export it to become an environment variable*
$ export ANOTHER_VARIABLE=20 *Or, set and export in a single command*
export命令指定变量及其值将从当前 Shell 复制到任何未来的子 Shell 中。本地变量不会复制到未来的子 Shell 中:
$ export E="I am an environment variable" *Set an environment variable*
$ L="I am just a local variable" *Set a local variable*
$ echo $E
I am an environment variable
$ echo $L
I am just a local variable
$ bash *Run a child shell*
$ echo $E *Environment variable was copied*
I am an environment variable
$ echo $L *Local variable was not copied*
*Empty string is printed*
$ exit *Exit the child shell*
请记住,子进程的变量是副本。对副本的任何更改都不会影响父 shell:
$ export E="I am the original value" *Set an environment variable*
$ bash *Run a child shell*
$ echo $E
I am the original value *Parent's value was copied*
$ E="I was modified in a child" *Change the child's copy*
$ echo $E
I was modified in a child
$ exit *Exit the child shell*
$ echo $E
I am the original value *Parent's value is unchanged*
随时启动一个新的 shell 并更改其环境中的任何内容,当您退出 shell 时所有更改都会消失。这意味着您可以安全地尝试 shell 功能 - 只需手动运行一个 shell,创建一个子进程,并在完成时终止它。
超级警告: “全局” 变量
有时 Linux 将其内部工作隐藏得太好了。一个很好的例子是环境变量的行为。以某种魔法的方式,像HOME和PATH这样的变量在所有的 shell 实例中都有一个一致的值。它们在某种意义上看起来像是“全局变量”(我甚至在 O'Reilly 之外的其他 Linux 书籍中见过这种说法)。但环境变量并不是全局的。每个 shell 实例都有自己的副本。在一个 shell 中修改一个环境变量不会改变其他正在运行的 shell 中的值。修改只影响该 shell 未来的子进程(尚未调用的)。
如果是这样,像HOME或PATH这样的变量如何在所有的 shell 实例中保持其值?有两种方法可以实现这一点,这在图 6-1 中有所说明。简而言之:
子进程从其父进程复制。
对于像HOME这样的变量,其值通常由您的登录 shell 设置和导出。所有未来的 shell(直到您退出登录)都是登录 shell 的子进程,因此它们会收到变量及其值的副本。在现实世界中,这些系统定义的环境变量很少被修改,它们看起来像是全局的,但它们只是按照普通规则运行的普通变量。(您甚至可以在运行中的 shell 中更改它们的值,但这可能会扰乱该 shell 和其他程序的预期行为。)
不同的实例读取相同的配置文件。
本地变量,这些变量不会复制到子进程中,可以在 Linux 配置文件中设置其值,比如$HOME/.bashrc(详细信息请参见“配置您的环境”)。每次调用时,shell 实例都会读取和执行相应的配置文件。因此,这些本地变量看起来会从一个 shell 复制到另一个 shell。对于其他非导出的 shell 功能,如别名,情况也是如此。
这种行为使一些用户认为export命令创建了一个全局变量。实际上不是这样。命令export WHATEVER只是声明变量WHATEVER将从当前 shell 复制到任何未来的子进程中。

图 6-1. Shell 可能通过导出或读取相同的配置文件共享变量和值
子 shell 与子 shell
子进程是其父进程的部分副本。例如,它包括其父进程的环境变量的副本,但不包括其父进程的本地(未导出)变量或别名:
$ alias *List aliases*
alias gd='pushd'
alias l='ls -CF'
alias pd='popd'
$ bash --norc *Run a child shell and ignore bashrc files*
$ alias *List aliases - none are known*
$ echo $HOME *Environment variables are known*
/home/smith
$ exit *Exit the child shell*
如果您曾经想知道为什么您的别名在 shell 脚本中不可用,现在您知道了。Shell 脚本在子进程中运行,不会接收到父进程的别名副本。
与父 shell 完全相同的子 shell,不同的是它包括父 shell 的所有变量、别名、函数等。要在子 shell 中启动命令,请使用括号括起命令:
$ (ls -l) *Launches ls -l in a subshell*
-rw-r--r-- 1 smith smith 325 Oct 13 22:19 animals.txt
$ (alias) *View aliases in a subshell*
alias gd=*pushd*
alias l=*ls -CF*
alias pd=*popd*
⋮
$ (l) *Run an alias from the parent*
animals.txt
要检查 shell 实例是否为子 shell,请打印变量BASH_SUBSHELL。在子 shell 中,值为非零;否则为零:
$ echo $BASH_SUBSHELL *Check the current shell*
0 *Not a subshell*
$ bash *Run a child shell*
$ echo $BASH_SUBSHELL *Check the child shell*
0 *Not a subshell*
$ exit *Exit the child shell*
$ (echo $BASH_SUBSHELL) *Run an explicit subshell*
1 *Yes, it's a subshell*
我将介绍一些子 shell 的实际用途在“技术 #10:显式子 shell”。目前,只需知道您可以创建它们并复制父 shell 的别名即可。
配置您的环境
当bash运行时,通过读取一系列文件(称为配置文件)并执行它们的内容来配置自己。这些文件定义变量、别名、函数和其他 shell 特性,并且可以包含任何 Linux 命令。(它们就像配置 shell 的 shell 脚本。)一些配置文件由系统管理员定义,适用于系统中的所有用户。它们位于/etc目录中。其他配置文件由个别用户拥有和更改。它们位于用户的主目录中。表 6-1 列出了标准bash配置文件。它们有几种类型:
启动文件
仅在您登录时自动执行的配置文件,即仅适用于您的登录 shell。例如,此文件中的一个命令可能会设置并导出一个环境变量。但是,在此文件中定义别名则不太有用,因为别名不会复制给子进程。
初始化文件
对于每个非登录 shell 实例执行的配置文件,例如,手动运行互动式 shell 或(非互动式)shell 脚本时。例如,初始化文件中的一个示例命令可能会设置一个变量或定义一个别名。
清理文件
在您的登录 shell 退出之前立即执行的配置文件。此文件中的示例命令可能是clear,用于在注销时清空屏幕。
表 6-1. bash 所使用的标准配置文件
| 文件类型 | 被以下运行 | 系统范围的位置 | 个人文件位置(按调用顺序) |
|---|---|---|---|
| 启动文件 | 登录 shell,在启动时 | /etc/profile | \(HOME/.bash_profile*、*\)HOME/.bash_login 和 $HOME/.profile |
| 初始化文件 | 互动式 shell(非登录时)启动时 | /etc/bash.bashrc | $HOME/.bashrc |
| Shell 脚本,在启动时 | 将变量BASH_ENV设置为初始化文件的绝对路径(示例:BASH_ENV=/usr/local/etc/bashrc) |
将变量BASH_ENV设置为初始化文件的绝对路径(示例:BASH_ENV=/usr/local/etc/bashrc) |
|
| 清理文件 | 登录 shell,在退出时 | /etc/bash.bash_logout | $HOME/.bash_logout |
注意你的家目录中有三个个人启动文件选择(.bash_profile、.bash_login 和 .profile)。 大多数用户可以选择其中一个并坚持使用。 你的 Linux 发行版可能已经提供了其中一个,里面预置了(理想情况下)有用的命令。 如果你运行其他如 Bourne shell(/bin/sh)和 Korn shell(/bin/ksh)的 shell,情况会有所不同。 这些 shell 也会读取 .profile,并且如果传递了 bash 特定的命令执行可能会失败。 将 bash 特定的命令放在 .bash_profile 或 .bash_login 中(再次,只需选择一个)。
用户有时会发现个人启动文件与个人初始化文件的分离令人困惑。 为什么要让你的登录 shell 与在多个窗口中打开的其他 shell 行为不同呢? 答案是,在许多情况下,你不需要它们行为不同。 你的个人启动文件可能只做一点事情,就是源自你的个人初始化文件 $HOME/.bashrc,所以所有交互式 shell(登录或非登录)都会有基本相同的配置。
在其他情况下,你可能更喜欢在你的启动和初始化文件之间分配责任。 例如,你的个人启动文件可能设置和导出你的环境变量以供将来的子进程复制,而 $HOME/.bashrc 可能定义所有你的别名(这些别名不会被复制到子进程)。
另一个考虑因素是你是否登录到图形窗口桌面环境(GNOME、Unity、KDE Plasma 等),在这种情况下,你的登录 shell 可能会被隐藏。 这种情况下,你可能不关心登录 shell 的行为,因为你只与其子进程交互,所以你可能会把大部分或全部配置放在 $HOME/.bashrc 中。^(4) 另一方面,如果你主要从非图形终端程序如 SSH 客户端登录,那么你直接与登录 shell 交互,因此它的配置非常重要。
在这些情况下,让你的个人启动文件源自你的个人初始化文件通常是值得的:
# Place in $HOME/.bash_profile or other personal startup file
if [ -f "$HOME/.bashrc" ]
then
source "$HOME/.bashrc"
fi
无论你做什么,尽量不要在两个不同的配置文件中放置相同的配置命令。 这是一种造成混乱的方法,而且很难维护,因为你在一个文件中做的任何更改都必须记住在另一个文件中复制(而且你会忘记,相信我)。 反而,像我展示的那样,从一个文件中源另一个文件。
重新读取配置文件
当你更改任何启动或初始化文件时,你可以通过源文件强制运行的 shell 重新读取它,就像在“环境和初始化文件简短版本”中解释的那样。
$ source ~/.bash_profile *Uses the builtin "source" command*
$ . ~/.bash_profile *Uses a dot*
为什么存在 source 命令
为什么要使用配置文件而不是使用 chmod 使其可执行,然后像运行 shell 脚本一样运行它?因为脚本在子进程中运行。脚本中的任何命令都不会影响您预期的(父)shell。它们只会影响子进程,子进程退出后,您将一无所获。
在您的环境中旅行
如果您在多个位置使用许多 Linux 机器,某个时候您可能希望在多台机器上安装精心制作的配置文件。不要从一台机器复制单个文件到另一台机器 - 这种方法最终会导致混乱。相反,将文件存储和维护在GitHub或类似的软件开发服务中,具有版本控制。然后,您可以在任何 Linux 机器上方便且一致地下载、安装和更新配置文件。如果在编辑配置文件时出现错误,您可以通过发出一两个命令回滚到先前的版本。版本控制超出了本书的范围;请参阅“将版本控制应用于日常文件”以了解更多信息。
如果您不熟悉 Git 或 Subversion 等版本控制系统,请将配置文件存储在像 Dropbox、Google Drive 或 OneDrive 这样的简单文件服务上。更新配置文件将不太方便,但至少文件将很容易可用于复制到其他 Linux 系统。
总结
我遇到了许多对 Linux 用户感到困惑(或不知情)的人,他们不理解父进程和子进程、环境以及许多 shell 配置文件的目的。阅读本章后,我希望您对所有这些事情有了更清晰的认识。它们在第七章中作为运行命令的强大工具发挥作用。
^(1) 如果您使用不同的 shell,请参阅附录 B。
^(2) 我选择性地削减输出以显示常见的环境变量。您的输出可能更长,充满了晦涩的变量名。
^(3) 它完整,除了陷阱,“在启动时从其父进程继承的值” (man bash)。我在本书中不再进一步讨论陷阱。
^(4) 为了让事情稍微更加混乱,一些桌面环境有它们自己的 shell 配置文件。例如,GNOME 有 \(HOME/.gnomerc*,底层的 X 窗口系统有 *\)HOME/.xinitrc。
第七章:11 种更多运行命令的方式
现在你的工具箱中有很多命令,并且对 shell 有了深入的理解,是时候学习……如何运行命令了。等一下,你自本书开始就一直在运行命令吧?是的,但只有两种方式。第一种是普通执行简单命令:
$ grep Nutshell animals.txt
第二种是简单命令的管道,如第一章中所述:
$ cut -f1 grades | sort | uniq -c | sort -nr
在本章中,我将展示 11 种更多运行命令的方式以及为什么你应该学习它们。每种技术都有其利弊,你掌握的技术越多,与 Linux 的互动就越灵活、高效。我现在将专注于每种技术的基础知识;在接下来的两章中,你将看到更复杂的例子。
技术清单
一个列表是单个命令行上的命令序列。你已经见过一种列表类型——管道,但 shell 支持其他具有不同行为的列表:
条件列表
每个命令依赖于前一个命令的成功或失败。
无条件列表
命令只是一个接着一个运行。
技术 #1:条件列表
假设你想在目录 dir 中创建一个文件 new.txt。一个典型的命令序列可能是:
$ cd dir *Enter the directory*
$ touch new.txt *Make the file*
注意第二个命令依赖于第一个命令的成功。如果目录 dir 不存在,则运行 touch 命令没有意义。Shell 允许你明确地表达这种依赖关系。如果在单行上的两个命令之间放置 && 运算符(读作“and”):
$ cd dir && touch new.txt
接着第二个命令(touch)只有在第一个命令(cd)成功后才会运行。上面的例子是两个命令的条件列表。(要了解命令“成功”的含义,请参见“退出代码表示成功或失败”。)
很可能,你每天都在运行依赖于之前命令的命令。例如,你曾经为了备份文件而修改原文件并在完成后删除备份吗?
$ cp myfile.txt myfile.safe *Make a backup copy*
$ nano myfile.txt *Change the original*
$ rm myfile.safe *Delete the backup*
每个命令仅在前一个命令成功时才有意义。因此,这个序列适合作为条件列表:
$ cp myfile.txt myfile.safe && nano myfile.txt && rm myfile.safe
再举个例子,如果你使用版本控制系统 Git 来维护文件,你可能熟悉在修改文件后的以下命令序列:运行 git add 准备提交的文件,然后 git commit,最后 git push 来分享你的提交变更。如果其中任何一个命令失败,你将不会运行余下的命令(直到修复失败的原因)。因此,这三个命令很好地作为一个条件列表:
$ git add . && git commit -m"fixed a bug" && git push
就像 && 运算符仅在第一个命令成功时运行第二个命令一样,相关的运算符 ||(读作“or”)仅在第一个命令失败时运行第二个命令。例如,以下命令尝试进入 dir,如果失败,则创建 dir:^(1)
$ cd dir || mkdir dir
你会经常在脚本中看到||运算符,如果发生错误,脚本会退出:
# If a directory can't be entered, exit with an error code of 1
cd dir || exit 1
结合&&和||运算符来设置更复杂的成功和失败操作。以下命令尝试进入目录dir,如果失败,则创建该目录并进入。如果全部失败,则打印失败消息:
$ cd dir || mkdir dir && cd dir || echo "I failed"
条件列表中的命令不必是简单命令;它们也可以是管道和其他组合命令。
技巧 #2:无条件列表
列表中的命令不必彼此依赖。如果用分号分隔命令,它们只是顺序运行。命令的成功或失败不会影响后续的命令。
我喜欢无条件列表,在我下班后启动临时命令。这是一个睡眠(什么都不做)两个小时(7,200 秒),然后备份我的重要文件的例子:
$ sleep 7200; cp -a ~/important-files /mnt/backup_drive
这是一个类似的命令,作为一个简单的提醒系统,睡眠五分钟,然后发送给我一封电子邮件:^(3)
$ sleep 300; echo "remember to walk the dog" | mail -s reminder $USER
无条件列表是一个便利特性:它们产生与逐个输入命令并按 Enter 键相同的结果(大多数情况下)。唯一显著的区别与退出代码有关。在无条件列表中,单个命令的退出代码被丢弃,除了最后一个。只有列表中最后一个运行的命令的退出代码被分配给 shell 变量?:
$ mv file1 file2; mv file2 file3; mv file3 file4
$ echo $?
0 *The exit code for "mv file3 file4"*
替换技术
替换意味着自动用其他文本替换命令的文本。我将向你展示两种具有强大可能性的类型:
命令替换
一个命令被其输出替换。
进程替换
一个命令被一个文件(某种程度上)替换。
技巧 #3:命令替换
假设你有几千个文本文件,代表歌曲。每个文件包括歌曲标题,艺术家名,专辑标题和歌词:
Title: Carry On Wayward Son
Artist: Kansas
Album: Leftoverture
Carry on my wayward son
There'll be peace when you are done
⋮
你想按艺术家将文件组织到子目录中。为了手动执行此任务,你可以使用grep搜索所有 Kansas 的歌曲文件:
$ grep -l "Artist: Kansas" *.txt
carry_on_wayward_son.txt
dust_in_the_wind.txt
belexes.txt
然后将每个文件移动到一个目录kansas中:
$ mkdir kansas
$ mv carry_on_wayward_son.txt kansas
$ mv dust_in_the_wind.txt kansas
$ mv belexes.txt kansas
很乏味,对吧?如果你能告诉 Shell:“移动所有包含字符串Artist: Kansas的文件到目录kansas”,那不是很棒吗?用 Linux 术语来说,你想从前面的grep -l命令中获取名称列表,并将其交给mv。好吧,你可以通过一个称为命令替换的 Shell 特性轻松地实现这一点:
$ mv $(grep -l "Artist: Kansas" *.txt) kansas
语法:
$(*any command here*)
执行括号内的命令,并用其输出替换命令。因此,在前述命令行上,grep -l命令被打印的文件名列表替换,就好像你已经像这样输入文件名一样:
$ mv carry_on_wayward_son.txt dust_in_the_wind.txt belexes.txt kansas
每当你发现自己在将一个命令的输出复制到后续命令行中时,通常可以通过命令替换节省时间。甚至可以在命令替换中包含别名,因为它的内容在一个子 shell 中运行,其中包括其父 shell 的别名的副本。
特殊字符和命令替换
带有 grep -l 的前面的示例对大多数 Linux 文件名效果很好,但对包含空格或其他特殊字符的文件名则不适用。在输出交给 mv 之前,shell 会评估这些字符,可能会产生意外的结果。例如,如果 grep -l 打印了 dust in the wind.txt,shell 会将空格视为分隔符,mv 将尝试移动四个名为 dust、in、the 和 wind.txt 的不存在文件。
这里是另一个例子。假设你有几年的银行对账单以 PDF 格式下载。下载的文件名包含对账单的年份、月份和日期,例如 eStmt_2021-08-26.pdf 表示 2021 年 8 月 26 日的对账单。^(4) 你想在当前目录中查看最近的对账单。你可以手动完成:列出目录,找到最新日期的文件(这将是列表中的最后一个文件),并使用 Linux PDF 查看器如 okular 显示它。但为什么要做所有这些手动工作呢?让命令替换为你省时。创建一个命令来打印目录中最新的 PDF 文件名:
$ ls eStmt*pdf | tail -n1
并使用命令替换将其提供给 okular:
$ okular $(ls eStmt*pdf | tail -n1)
ls 命令列出所有的声明文件,tail 仅打印最后一个,例如 eStmt_2021-08-26.pdf。命令替换将这个单个文件名直接放在命令行上,就像你输入了 okular eStmt_2021-08-26.pdf。
注意
命令替换的原始语法是反引号(backticks)。以下两个命令是等效的:
$ echo Today is $(date +%A).
Today is Saturday.
$ echo Today is `date +%A`.
Today is Saturday.
大多数 shell 支持反引号(backticks)。$() 语法更容易嵌套,然而:
$ echo $(date +%A) | tr a-z A-Z *Single*
SATURDAY
echo Today is $(echo $(date +%A) | tr a-z A-Z)! *Nested*
Today is SATURDAY!
在脚本中,命令替换的常见用途是将命令的输出存储在变量中:
*VariableName*=$(*some command here*)
例如,要获取包含 Kansas 歌曲的文件名并将它们存储在一个变量中,可以像这样使用命令替换:
$ kansasFiles=$(grep -l "Artist: Kansas" *.txt)
输出可能有多行,因此为了保留任何换行符,请确保在使用该值时引用它:
$ echo "$kansasFiles"
技巧 #4:进程替换
正如你刚刚看到的,命令替换将一个命令的输出直接替换成一个字符串。进程替换 也替换一个命令的输出,但它将输出视为存储在文件中。这个强大的区别一开始可能看起来令人困惑,所以我会一步步解释。
假设你在一个包含 JPEG 图像文件的目录中,文件名从 1.jpg 到 1000.jpg,但某些文件神秘地丢失了,你想要识别它们。使用以下命令生成这样一个目录:
$ mkdir /tmp/jpegs && cd /tmp/jpegs
$ touch {1..1000}.jpg
$ rm 4.jpg 981.jpg
一种找到丢失文件的较差方法是列出目录并按数字排序,然后用肉眼查找间隙:
$ ls -1 | sort -n | less
1.jpg
2.jpg
3.jpg
5.jpg *4.jpg is missing*
⋮
更健壮、自动化的解决方案是将现有文件名与从1.jpg到1000.jpg的完整列表进行比较,使用diff命令。实现此解决方案的一种方法是使用临时文件。将现有的按排序后的文件名存储在一个临时文件original-list中:
$ ls *.jpg | sort -n > /tmp/original-list
然后使用seq生成整数 1 到 1000,并将“.jpg”附加到每一行,将完整的文件名列表从1.jpg到1000.jpg打印到另一个临时文件full-list:
$ seq 1 1000 | sed 's/$/.jpg/' > /tmp/full-list
使用diff命令比较两个临时文件,发现4.jpg和981.jpg丢失,然后删除这些临时文件:
$ diff /tmp/original-list /tmp/full-list
3a4
> 4.jpg
979a981
> 981.jpg
$ rm /tmp/original-list /tmp/full-list *Clean up afterwards*
这是很多步骤。直接比较两个文件名列表并且不再使用临时文件,这岂不是一件了不起的事情吗?挑战在于diff无法比较来自标准输入的两个列表;它需要文件作为参数。^5 进程替换解决了这个问题。它使得diff将这两个列表都看作是文件。(侧边栏 “进程替换的工作原理” 提供了技术细节。)语法:
<(*any command here*)
在子 shell 中运行命令并将其输出呈现为文件的形式。例如,以下表达式表示ls -1 | sort -n的输出,就像它被包含在一个文件中一样:
<(ls -1 | sort -n)
您可以使用cat命令来查看文件:
$ cat <(ls -1 | sort -n)
1.jpg
2.jpg
⋮
您可以使用cp命令复制文件:
$ cp <(ls -1 | sort -n) /tmp/listing
$ cat /tmp/listing
1.jpg
2.jpg
⋮
如您现在所看到的,您可以将文件与另一个文件进行diff比较。从生成您的两个临时文件的两个命令开始:
ls *.jpg | sort -n
seq 1 1000 | sed 's/$/.jpg/'
应用进程替换,使得diff能够将它们视为文件,并获得与之前相同的输出,但不使用临时文件:
$ diff <(ls *.jpg | sort -n) <(seq 1 1000 | sed 's/$/.jpg/')
3a4
> 4.jpg
979a981
> 981.jpg
通过使用grep查找以>开头的行并使用cut去掉前两个字符来清理输出,您就得到了丢失文件的报告:
$ diff <(ls *.jpg | sort -n) <(seq 1 1000 | sed 's/$/.jpg/') \
| grep '>' \
| cut -c3-
4.jpg
981.jpg
进程替换改变了我使用命令行的方式。只从磁盘文件读取的命令突然可以从标准输入读取。通过实践,以前看似不可能的命令变得很容易。
命令作为字符串的技术
每个命令都是一个字符串,但有些命令比其他命令更“字符串化”。我将向您展示几种逐步构建字符串并作为命令运行的技术:
-
将命令作为参数传递给
bash -
将命令通过 stdin 管道传递给
bash -
使用
ssh将命令发送到另一台主机 -
使用
xargs运行一系列命令
警告
以下技术可能存在风险,因为它们将看不见的文本发送到 shell 以执行。绝对不要盲目执行这些命令。在执行之前,一定要理解文本(并信任其来源)。您不希望因错误执行字符串"rm -rf $HOME"而删除所有文件。
技术 #5:将命令作为bash的参数传递
bash 是一个像其他任何普通命令一样的命令,正如在“Shell 可执行文件” 中解释的那样,因此您可以在命令行上按名称运行它。默认情况下,运行 bash 会启动一个交互式 shell,用于输入和执行命令,就像您看到的那样。或者,您可以通过 -c 选项将命令作为字符串传递给 bash,bash 将运行该字符串作为命令并退出:
$ bash -c "ls -l"
-rw-r--r-- 1 smith smith 325 Jul 3 17:44 animals.txt
这为什么有用?因为新的 bash 进程是具有自己环境的子进程,包括当前目录、具有值的变量等等。对子 shell 的任何更改都不会影响您当前运行的 shell。以下是一个 bash -c 命令,它将目录更改为 /tmp,只足够删除一个文件,然后退出:
$ pwd
/home/smith
$ touch /tmp/badfile *Create a temporary file*
$ bash -c "cd /tmp && rm badfile"
$ pwd
/home/smith *Current directory is unchanged*
bash -c 最具教育性和美丽的用法之一是在您以超级用户身份运行某些命令时产生的。具体来说,sudo 和输入/输出重定向的组合会产生一个有趣(有时是疯狂的)的情况,其中 bash -c 是成功的关键。
假设您想在系统目录 /var/log 中创建一个日志文件,这是普通用户无法写入的。您运行以下 sudo 命令以获得超级用户权限并创建日志文件,但它神秘地失败了:
$ sudo echo "New log file" > /var/log/custom.log
bash: /var/log/custom.log: Permission denied
等一下 — sudo 应该允许您在任何地方创建任何文件。这个命令为什么会失败呢?为什么 sudo 甚至没有提示您输入密码?答案是:因为 sudo 没有运行。您将 sudo 应用于 echo 命令,但没有应用于首先运行并失败的输出重定向。详细来看:
-
您按下了 Enter 键。
-
shell 开始评估整个命令,包括重定向 (
>). -
shell 尝试在受保护目录 /var/log 中创建文件 custom.log。
-
您没有权限写入 /var/log,因此 shell 放弃并打印“权限被拒绝”消息。
这就是为什么 sudo 从未运行。要解决这个问题,您需要告诉 shell:“以超级用户身份运行整个命令,包括输出重定向。”这正是 bash -c 解决得很好的情况。构建您想要作为字符串运行的命令:
'echo "New log file" > /var/log/custom.log'
并将其作为参数传递给sudo bash -c:
$ sudo bash -c 'echo "New log file" > /var/log/custom.log'
[sudo] password for smith: xxxxxxxx
$ cat /var/log/custom.log
New log file
这一次,您已经以超级用户身份运行了 bash,而不仅仅是 echo,bash 执行整个字符串作为命令。重定向成功了。每当将 sudo 与重定向配对使用时,请记住这个技巧。
技巧 #6:将命令管道传输到 bash
shell 会读取您在 stdin 上键入的每个命令。这意味着 bash 程序可以参与管道。例如,打印字符串 "ls -l" 并将其管道到 bash,bash 将该字符串视为命令并运行它:
$ echo "ls -l"
ls -l
$ echo "ls -l" | bash
-rw-r--r-- 1 smith smith 325 Jul 3 17:44 animals.txt
警告
请记住,永远不要盲目地将文本管道到 bash。要意识到您正在执行什么。
当你需要连续运行许多相似命令时,这种技术非常棒。如果可以将命令打印为字符串,那么可以将字符串通过管道传输到bash以执行。假设你在一个包含许多文件的目录中,并且想要按它们的第一个字符将它们组织到子目录中。一个名为apple的文件将被移动到子目录a,一个名为cantaloupe的文件将移动到子目录c,依此类推。^(6)(为简单起见,我们假设所有文件名以小写字母开头且不包含空格或特殊字符。)
首先,列出按排序的文件。我们假设所有名称至少为两个字符长(与模式??*匹配),因此我们的命令不会与子目录a到z发生冲突:
$ ls -1 ??*
apple
banana
cantaloupe
carrot
⋮
通过大括号扩展创建你需要的 26 个子目录:
$ mkdir {a..z}
现在生成你需要的mv命令,作为字符串。从一个为sed设计的正则表达式开始,它捕获文件名的第一个字符作为表达式#1(\1):
^\(.\)
捕获剩余的文件名作为表达式#2(\2):
\(.*\)$
连接这两个正则表达式:
^\(.\)\(.*\)$
现在用单词mv后跟一个空格,完整的文件名(\1\2),再加一个空格和第一个字符(\1)来形成一个mv命令:
mv \1\2 \1
完整的命令生成器是:
$ ls -1 ??* | sed 's/^\(.\)\(.*\)$/mv \1\2 \1/'
mv apple a
mv banana b
mv cantaloupe c
mv carrot c
⋮
其输出包含你所需的确切mv命令。通过将其管道传输到less以进行逐页查看来确认它的正确性:
$ ls -1 ??* | sed 's/^\(.\)\(.*\)$/mv \1\2\t\1/' | less
当您确信生成的命令正确时,请将输出通过管道传输到bash以执行:
$ ls -1 ??* | sed 's/^\(.\)\(.*\)$/mv \1\2\t\1/' | bash
你刚刚完成的步骤是一个可重复的模式:
-
通过操作字符串来打印一系列命令。
-
使用
less查看结果以检查正确性。 -
将结果管道传输到
bash。
技术#7:使用 ssh 远程执行字符串
免责声明:仅当您熟悉用于登录远程主机的安全外壳 SSH 时,此技术才会有意义。建立主机之间的 SSH 关系超出了本书的范围;要了解更多信息,请寻找 SSH 教程。
除了通常的远程主机登录方式:
$ ssh myhost.example.com
您还可以通过在命令行上将字符串附加到ssh的其余部分来在远程主机上执行单个命令。:
$ ssh myhost.example.com ls
remotefile1
remotefile2
remotefile3
这种技术通常比登录,运行命令和退出更快。如果命令包含特殊字符(例如需要在远程主机上评估的重定向符号),请引用或转义它们。否则,它们将由您的本地 shell 评估。以下两个命令都在远程运行ls,但输出重定向发生在不同的主机上:
$ ssh myhost.example.com ls > outfile *Creates outfile on local host*
$ ssh myhost.example.com "ls > outfile" *Creates outfile on remote host*
您还可以通过管道将命令传输到ssh以在远程主机上运行它们,就像在本地运行它们时将它们传输到bash一样:
$ echo "ls > outfile" | ssh myhost.example.com
在将命令传输到ssh时,远程主机可能会打印诊断或其他消息。这些通常不会影响远程命令,并且您可以将它们抑制。
-
如果您看到关于伪终端或伪 tty 的消息,例如“因为 stdin 不是终端,所以不会分配伪终端”,请使用
ssh的-T选项运行,以防止远程 SSH 服务器分配终端:$ echo "ls > outfile" | ssh -T myhost.example.com -
如果您看到通常在登录时出现的欢迎消息(“欢迎使用 Linux!”)或其他不需要的消息,请尝试显式告诉
ssh在远程主机上运行bash,这些消息应该会消失:$ echo "ls > outfile" | ssh myhost.example.com bash
技术#8:使用 xargs 运行命令列表
许多 Linux 用户从未听说过xargs命令,但它是一个强大的工具,用于构建和运行多个相似的命令。学习xargs是我 Linux 教育中的又一个转折点,我也希望对您有帮助。
xargs接受两个输入:
-
在标准输入上:由空格分隔的字符串列表。例如由
ls或find产生的文件路径,但任何字符串都可以。我将它们称为输入字符串。 -
在命令行上:一个不完整的命令,缺少一些参数,我将其称为命令模板。
xargs将输入字符串和命令模板合并,生成并运行新的完整命令,我将其称为生成的命令。我将通过一个玩具示例来演示这个过程。假设您在一个有三个文件的目录中:
$ ls -1
apple
banana
cantaloupe
将目录列表通过管道传递给xargs作为其输入字符串,并提供wc -l作为命令模板,如下所示:
$ ls -1 | xargs wc -l
3 apple
4 banana
1 cantaloupe
8 total
正如承诺的那样,xargs将wc -l命令模板应用于输入字符串,并计算每个文件的行数。要使用cat打印相同的三个文件,只需将命令模板更改为cat:
$ ls -1 | xargs cat
我对xargs的玩具示例有两个缺点,一个是致命的,一个是实际的。致命的缺点是,如果输入字符串包含特殊字符(例如空格),xargs可能会执行错误的操作。在侧边栏“使用 find 和 xargs 时的安全性”中有一个健壮的解决方案。
实际的缺点是,在这里您不需要xargs—您可以通过文件模式匹配更简单地完成相同的任务:
$ wc -l *
3 apple
4 banana
1 cantaloupe
8 total
那么为什么要使用xargs呢?当输入字符串比简单的目录列表更有趣时,它的强大之处就显现出来了。假设您想递归地计算一个目录及其所有子目录中所有 Python 源文件(以*.py 结尾)的行数。使用find轻松生成这样的文件路径列表:
$ find . -type f -name \*.py -print
fruits/raspberry.py
vegetables/leafy/lettuce.py
⋮
当前,xargs可以将命令模板wc -l应用于每个文件路径,生成一个递归的结果,否则将很难获得。为了安全起见,我将选项-print替换为-print0,并将xargs替换为xargs -0,原因在侧边栏“使用 find 和 xargs 时的安全性”中有解释:
$ find . -type f -name \*.py -print0 | xargs -0 wc -l
6 ./fruits/raspberry.py
3 ./vegetables/leafy/lettuce.py
⋮
通过结合find和xargs,你可以使任何命令递归运行到文件系统中,仅影响符合你指定条件的文件(和/或目录)。在某些情况下,你可以仅使用find的-exec选项达到相同效果,但xargs通常是一个更干净的解决方案。
xargs有许多选项(参见man xargs),用于控制它如何创建和运行生成的命令。在我看来,除了-0之外,最重要的选项是-n和-I。-n选项控制xargs在每个生成的命令中添加多少个参数。默认行为是尽可能添加适合 shell 限制的参数数目:^(7)
$ ls | xargs echo *Fit as many input strings as possible:*
apple banana cantaloupe carrot *echo apple banana cantaloupe carrot*
$ ls | xargs -n1 echo *One argument per echo command:*
apple *echo apple*
banana *echo banana*
cantaloupe *echo cantaloupe*
carrot *echo carrot*
$ ls | xargs -n2 echo *Two arguments per echo command:*
apple banana *echo apple banana*
cantaloupe carrot *echo cantaloupe carrot*
$ ls | xargs -n3 echo *Three arguments per echo command:*
apple banana cantaloupe *echo apple banana cantaloupe*
carrot *echo carrot*
-I选项控制输入字符串在生成命令中的位置。默认情况下,它们附加到命令模板,但你可以将它们放置在其他位置。在-I后跟任意字符串(你选择的字符串),那个字符串将成为命令模板中的占位符,指示输入字符串应该插入的确切位置:
$ ls | xargs -I XYZ echo XYZ is my favorite food *Use XYZ as a placeholder*
apple is my favorite food
banana is my favorite food
cantaloupe is my favorite food
carrot is my favorite food
我随意选择“XYZ”作为输入字符串的占位符,并将其放置在echo后面,将输入字符串移动到每个输出行的开头。请注意,-I选项限制xargs每个生成的命令只接受一个输入字符串。我建议仔细阅读xargs的手册,以了解你还能控制哪些内容。
长参数列表
当命令行变得非常长时,xargs是一个解决方案。假设当前目录包含 100 万个名为file1.txt至file1000000.txt的文件,如果你尝试通过模式匹配来删除它们:
$ rm *.txt
bash: /bin/rm: Argument list too long
模式*.txt的评估结果是一个超过 1400 万字符的字符串,这比 Linux 支持的长度还长。要解决此限制,请将文件列表传输到xargs以便删除。xargs将文件列表分割成多个rm命令。通过将完整目录列表管道传输到grep,仅匹配以.txt结尾的文件名,然后管道传输给xargs:
$ ls | grep '\.txt$' | xargs rm
这种解决方案比文件模式匹配(ls *.txt)更好,后者会产生相同的“Argument list too long”错误。更好的方法是像“使用 find 和 xargs 安全删除文件”中描述的那样运行find -print0:
$ find . -maxdepth 1 -name \*.txt -type f -print0 \
| xargs -0 rm
进程控制技术
到目前为止,我讨论的所有命令都占据父 shell 直到完成。让我们考虑几种与父 shell 建立不同关系的技术:
后台命令
立即返回提示符并在视线外执行
明确的子 shell
可以在组合命令的中间启动
进程替换
取代父 shell
技术#9:后台运行命令
到目前为止,我们所有的技术都是等待命令完成,然后显示下一个 shell 提示符。但你不必等待,特别是对于执行时间长的命令。你可以以特殊的方式启动命令,让它们从视线中消失(或者说几乎消失),但继续运行,立即释放当前 shell 运行更多的命令。这个技术称为 后台运行 命令或 在后台运行命令。相比之下,占据 shell 的命令称为 前台 命令。一个 shell 实例最多同时运行一个前台命令,加上任意数量的后台命令。
在后台启动一个命令
要在后台运行一个命令,只需附加一个&符号。shell 会用一个看起来神秘的消息回应,指示命令已被后台运行,并显示下一个提示符:
$ wc -c my_extremely_huge_file.txt & *Count characters in a huge file*
[1] 74931 *Cryptic-looking response*
$
你可以继续在这个 shell 中运行前台命令(或更多的后台命令)。后台命令的输出可能随时出现,甚至在你输入时也可能出现。如果后台命令成功完成,shell 将会用 Done 消息通知你:
59837483748 my_extremely_huge_file.txt
[1]+ Done wc -c my_extremely_huge_file.txt
或者如果失败,你会看到一个带有退出码的 Exit 消息:
[1]+ Exit 1 wc -c my_extremely_huge_file.txt
提示
这个&符号也是列表操作符,类似于 && 和 ||:
$ *command1* & *command2* & *command3* & *All 3 commands*
[1] 57351 *in background*
[2] 57352
[3] 57353
$ *command4* & *command5* & echo hi *All in background*
[1] 57431 *but "echo"*
[2] 57432
hi
暂停命令并发送到后台
一个相关的技巧是运行前台命令,在执行过程中改变主意,并将其发送到后台。按下 Ctrl-Z 暂停命令(称为 挂起 命令)并返回到 shell 提示符;然后输入 bg 来恢复在后台运行该命令。
工作和作业控制
后台命令是 shell 的一个特性,称为 作业控制,可以以各种方式操作正在运行的命令,如后台运行、挂起和恢复。一个 作业 是 shell 的工作单位:在 shell 中运行的命令的单个实例。简单命令、管道和条件列表都是作业的例子——基本上任何你可以在命令行运行的东西。
一个作业不仅仅是一个 Linux 进程。一个作业可以由一个进程、两个进程或更多进程组成。例如,一个包含六个程序的管道是一个单一的作业,其中至少包括六个进程。作业是 shell 的构造。Linux 操作系统并不跟踪作业,只跟踪底层的进程。
在任何时刻,一个 shell 可能会有多个作业在运行。给定 shell 中的每个作业都有一个正整数 ID,称为作业 ID 或作业号。当你在后台运行命令时,shell 会打印作业号和作业内第一个进程的 ID。在以下命令中,作业号为 1,进程 ID 为 74931:
$ wc -c my_extremely_huge_file.txt &
[1] 74931
常见的作业控制操作
Shell 对控制作业有内置命令,列在表 7-1 中。我将通过运行一系列作业并对其进行操作来演示最常见的作业控制操作。为了保持作业简单和可预测性,我将运行命令sleep,它什么也不做(“睡眠”)一段时间,然后退出。例如,sleep 10表示休眠 10 秒。
表 7-1. 作业控制命令
| Command | 含义 |
|---|---|
| bg | 将当前挂起的作业移到后台 |
| bg %n | 将挂起的作业号n移至后台(例如:bg %1) |
| fg | 将当前后台作业移到前台 |
| fg %n | 将后台作业号为n的作业移到前台(例如:fg %2) |
| kill %n | 终止后台作业号为n的作业(例如:kill %3) |
| jobs | 查看 shell 的作业 |
将一个作业在后台运行至完成:
$ sleep 20 & *Run in the background*
[1] 126288
$ jobs *List this shell's jobs*
[1]+ Running sleep 20 &
$
*...eventually...*
[1]+ Done sleep 20
注意
当作业完成时,“完成”消息可能不会立即显示,直到您再次按 Enter 键。
运行一个后台作业并将其切换至前台:
$ sleep 20 & *Run in the background*
[1] 126362
$ fg *Bring into the foreground*
sleep 20
*...eventually...*
$
运行一个前台作业,将其暂停,然后将其切换回前台:
$ sleep 20 *Run in the foreground*
^Z *Suspend the job*
[1]+ Stopped sleep 20
$ jobs *List this shell's jobs*
[1]+ Stopped sleep 20
$ fg *Bring into the foreground*
sleep 20
*...eventually...*
[1]+ Done sleep 20
运行前台作业并将其发送到后台:
$ sleep 20 *Run in the foreground*
^Z *Suspend the job*
[1]+ Stopped sleep 20
$ bg *Move to the background*
[1]+ sleep 20 &
$ jobs *List this shell's jobs*
[1]+ Running sleep 20 &
$
*...eventually...*
[1]+ Done sleep 20
处理多个后台作业。通过作业号加上百分号(%1,%2等)进行引用:
$ sleep 100 & *Run 3 commands in the background*
[1] 126452
$ sleep 200 &
[2] 126456
$ sleep 300 &
[3] 126460
$ jobs *List this shell's jobs*
[1] Running sleep 100 &
[2]- Running sleep 200 &
[3]+ Running sleep 300 &
$ fg %2 *Bring job 2 into the foreground*
sleep 200
^Z *Suspend job 2*
[2]+ Stopped sleep 200
$ jobs *See job 2 is suspended ("stopped")*
[1] Running sleep 100 &
[2]+ Stopped sleep 200
[3]- Running sleep 300 &
$ kill %3 *Terminate job 3*
[3]+ Terminated sleep 300
$ jobs *See job 3 is gone*
[1]- Running sleep 100 &
[2]+ Stopped sleep 200
$ bg %2 *Resume suspended job 2 in the background*
[2]+ sleep 200 &
$ jobs *See job 2 is running again*
[1]- Running sleep 100 &
[2]+ Running sleep 200 &
$
在后台进行输出和输入
后台命令可能会在不方便或混乱的时间写入标准输出。如果按预期对 Linux 字典文件(有 100,000 行)进行排序并在后台打印前两行时,Shell 会立即打印作业号(1)、进程 ID(81089)和下一个提示符:
$ sort /usr/share/dict/words | head -n2 &
[1] 81089
$
如果等到作业完成再打印两行到标准输出,输出可能会显得杂乱无章。在此情况下,光标位于第二个提示符处,所以您会得到这种看起来不整齐的输出:
$ sort /usr/share/dict/words | head -n2 &
[1] 81089
$ A
A's
按 Enter 键,Shell 将打印“作业完成”消息:
[1]+ Done sort /usr/share/dict/words | head -n2
$
后台作业的屏幕输出可能会在作业运行时的任何时候出现。为避免这种混乱,将标准输出重定向到文件,然后在方便时检查文件内容:
$ sort /usr/share/dict/words | head -n2 > /tmp/results &
[1] 81089
$
[1]+ Done sort /usr/share/dict/words | head -n2 > /tmp/results
$ cat /tmp/results
A
A's
$
当后台作业尝试从标准输入读取时,会发生一些奇怪的事情。Shell 暂停该作业,打印已停止消息,并在后台等待输入。通过不带参数后台化cat来演示这一点:
$ cat &
[1] 82455
[1]+ Stopped cat
后台作业无法读取输入,因此使用fg将作业切换至前台,然后提供输入:
$ fg
cat
Here is some input
Here is some input
⋮
提供所有输入后,执行以下任何一项操作:
-
在前台继续运行命令,直到完成。
-
通过按 Ctrl-Z 然后
bg,将命令暂停并移到后台。 -
用 Ctrl-D 结束输入,或用 Ctrl-C 终止命令。
后台化提示
后台运行非常适合那些需要长时间运行的命令,比如在长时间编辑会话期间的文本编辑器,或者任何打开自己窗口的程序。例如,程序员可以通过挂起他们的文本编辑器而不是退出来节省大量时间。我见过有经验的工程师修改他们文本编辑器中的一些代码,保存并退出编辑器,测试代码,然后重新启动编辑器并搜索他们离开的代码位置。他们每次退出编辑器都会损失 10 到 15 秒的工作切换时间。如果他们代替这样做挂起编辑器(Ctrl-Z),测试他们的代码,然后恢复编辑器(fg),他们避免了不必要的时间浪费。
后台运行也非常适合使用条件列表在后台运行一系列命令。如果列表中的任何命令失败,其余命令将不会运行,作业完成。(只需注意读取输入的命令,因为它们会导致作业挂起并等待输入。)
$ *command1* && *command2* && *command3* &
技巧 #10:显式子 shell
每次启动简单命令时,它都在子进程中运行,就像你在“父进程和子进程”中看到的那样。命令替换和进程替换创建子 shell。然而,有时明确启动额外的子 shell 很有帮助。要做到这一点,只需将命令括在括号中,它就会在子 shell 中运行:
$ (cd /usr/local && ls)
bin etc games lib man sbin share
$ pwd
/home/smith *"cd /usr/local" occurred in a subshell*
当应用于整个命令时,这种技巧并不是特别有用,除非也许可以帮助你避免运行第二个cd命令返回到先前的目录。但是,如果你在组合命令的一部分周围放置括号,你可以执行一些有用的技巧。一个典型的例子是在执行过程中更改目录的管道。假设你下载了一个压缩的tar文件,package.tar.gz,并且你想要提取文件。一个用于提取文件的tar命令是:
$ tar xvf package.tar.gz
Makefile
src/
src/defs.h
src/main.c
⋮
提取发生在当前目录的相对位置。^(8) 如果你想将它们提取到另一个目录怎么办?你可以首先cd到其他目录,然后运行tar(然后再cd回来),但你也可以用一个命令完成这个任务。窍门在于将被打包的数据传输到一个执行目录操作并在从 stdin 读取时运行tar的子 shell 中:^(9)
$ cat package.tar.gz | (mkdir -p /tmp/other && cd /tmp/other && tar xzvf -)
这个技巧还适用于使用两个tar进程将文件从一个目录dir1复制到另一个现有目录dir2,一个写入 stdout,另一个从 stdin 读取:
$ tar czf - dir1 | (cd /tmp/dir2 && tar xvf -)
相同的技巧也可以通过 SSH 将文件复制到另一个主机上的现有目录:
$ tar czf - dir1 | ssh myhost '(cd /tmp/dir2 && tar xvf -)'
警告
看起来像是bash中的括号仅仅是将命令组合在一起,就像数学中的括号一样诱人。但实际上并不是这样。每一对括号都会启动一个子 shell。
技巧 #11:进程替换
通常情况下,当你运行一个命令时,shell 会将其运行在一个单独的进程中,当命令退出时该进程被销毁,如“父进程和子进程”中所述。你可以通过 shell 内置命令exec改变这种行为。它会将正在运行的 shell(一个进程)替换为你选择的另一个命令(另一个进程)。当新命令退出时,不会出现 shell 提示符,因为原始 shell 已经不存在。
为了演示这一点,手动运行一个新的 shell 并更改其提示符:
$ bash *Run a child shell*
$ PS1="Doomed> " *Change the new shell's prompt*
Doomed> echo hello *Run any command you like*
hello
现在exec一个命令并观察新 shell 的关闭:
Doomed> exec ls *ls replaces the child shell, runs, and exits*
animals.txt
$ *A prompt from the original (parent) shell*
运行 exec 可能是致命的
如果你在 shell 中运行exec,那么 shell 在此后会退出。如果 shell 是在终端窗口中运行的,窗口会关闭。如果 shell 是登录 shell,你会被注销。
为什么会运行exec?一个原因是通过不启动第二个进程来节省资源。Shell 脚本有时会利用这种优化,在脚本的最后一个命令上运行exec。如果脚本运行多次(比如,数百万或数十亿次执行),这种节省可能是值得的。
exec 还有第二个能力——它可以重新分配当前 shell 的 stdin、stdout 和/或 stderr。这在 shell 脚本中最实用,比如这个玩具示例,将信息打印到文件 /tmp/outfile:
#!/bin/bash
echo "My name is $USER" > /tmp/outfile
echo "My current directory is $PWD" >> /tmp/outfile
echo "Guess how many lines are in the file /etc/hosts?" >> /tmp/outfile
wc -l /etc/hosts >> /tmp/outfile
echo "Goodbye for now" >> /tmp/outfile
不要单独将每个命令的输出重定向到 /tmp/outfile,而是使用exec将整个脚本的 stdout 重定向到 /tmp/outfile。随后的命令可以简单地输出到 stdout:
#!/bin/bash
# Redirect stdout for this script
exec > /tmp/outfile2
# All subsequent commands print to /tmp/outfile2
echo "My name is $USER"
echo "My current directory is $PWD"
echo "Guess how many lines are in the file /etc/hosts?"
wc -l /etc/hosts
echo "Goodbye for now"
运行这个脚本并检查文件 /tmp/outfile2 以查看结果:
$ cat /tmp/outfile2
My name is smith
My current directory is /home/smith
Guess how many lines are in the file /etc/hosts?
122 /etc/hosts
Goodbye for now
你可能不经常使用exec,但当你需要时它就在那里。
总结
现在你掌握了运行命令的 13 种技术——本章的 11 种技术加上简单命令和管道。表 7-2 概述了不同技术的常见用例。
表 7-2. 运行命令的常见习惯用法
| 问题 | 解决方案 |
|---|---|
| 将一个程序的 stdout 发送到另一个程序的 stdin | 管道传输 |
| 将输出(stdout)插入到一个命令中 | 命令替换 |
| 提供输出(stdout)给一个不读取 stdin,但读取磁盘文件的命令 | 进程替换 |
| 将一个字符串作为命令执行 | bash -c,或将其传输到bash |
| 在标准输出上打印多个命令并执行它们 | 管道传输至bash |
| 连续执行多个类似的命令 | 使用xargs,或构造命令字符串并将其传输到bash |
| 管理依赖于彼此成功的命令 | 条件列表 |
| 同时运行多个命令 | 后台运行 |
| 同时运行多个依赖于彼此成功的命令 | 后台运行的条件列表 |
| 在远程主机上运行一个命令 | 运行 ssh host command |
| 在管道中间更改目录 | 显式子 shell |
| 以后执行一个命令 | 使用sleep延时后紧跟命令的无条件列表 |
| 重定向到/从受保护文件 | 运行 sudo bash -c "command > file" |
下两章将教你如何结合技术以高效实现业务目标。
^(1) 命令 mkdir -p dir 可以创建一个目录路径,仅在该路径不存在时才创建,这在这里是一个更优雅的解决方案。
^(2) 这种行为与许多编程语言相反,其中零表示失败。
^(3) 或者,你可以使用cron进行备份作业和使用at设置提醒,但 Linux 注重灵活性——寻找实现同一目标的多种方法。
^(4) 目前,美国银行的可下载对账单文件是这样命名的。
^(5) 从技术上讲,如果你提供一个短划线作为文件名,diff可以从标准输入读取一个列表,但不能读取两个列表。
^(6) 这个目录结构类似于带有链表的哈希表。
^(7) 精确数字取决于你的 Linux 系统对长度限制;参见man xargs。
^(8) 假设tar归档是使用相对路径构建的——这在下载软件时很典型,而不是绝对路径。
^(9) 可以更简单地使用tar选项-C或--directory解决这个具体问题,该选项指定目标目录。我只是演示了使用子 shell 的一般技巧。
第八章:构建大胆一行命令
还记得前言中那个复杂的命令吗?
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g') \
| sed 's/^/mv /' \
| bash
这样的魔法咒语被称为大胆一行命令。^(1) 我们来拆解一下这个命令,理解它的作用和原理。最内层的echo命令使用大括号展开来生成 JPEG 文件名列表:
$ echo {1..10}.jpg
1.jpg 2.jpg 3.jpg ... 10.jpg
$ echo {0..9}.jpg
0.jpg 1.jpg 2.jpg ... 9.jpg
将文件名传输到sed中,将空格字符替换为换行符:
$ echo {1..10}.jpg | sed 's/ /\n/g'
1.jpg
2.jpg
⋮
10.jpg
$ echo {0..9}.jpg | sed 's/ /\n/g'
0.jpg
1.jpg
⋮
9.jpg
paste命令会将两个列表并排打印出来。过程替换允许paste像它们是文件一样读取这两个列表:
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g')
1.jpg 0.jpg
2.jpg 1.jpg
⋮
10.jpg 9.jpg
在每一行前加上mv,可以打印一系列字符串,这些字符串是mv命令:
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g') \
| sed 's/^/mv /'
mv 1.jpg 0.jpg
mv 2.jpg 1.jpg
⋮
mv 10.jpg 9.jpg
现在命令的目的已经显露出来:它生成 10 个命令来重命名图像文件1.jpg至10.jpg。新名称分别是0.jpg至9.jpg。将输出传输到bash执行这些mv命令:
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g') \
| sed 's/^/mv /' \
| bash
大胆的一行命令就像是解谜题。面对一个业务问题,比如重命名一组文件,你可以运用你的工具箱构建一个 Linux 命令来解决它。大胆的一行命令挑战你的创造力并增强你的技能。
在这一章中,你将逐步创建像前述那样的大胆一行命令,使用以下的神奇公式:
-
发明一个能解决难题的命令。
-
运行命令并检查输出。
-
回想一下历史命令并调整它。
-
重复步骤 2 和 3,直到命令产生期望的结果。
本章将让你的大脑得到锻炼。有时候,你可能会被示例搞得一头雾水。只需一步步来,边读边在计算机上运行这些命令。
注意
本章中的一些大胆一行命令太长,无法容纳在一行内,所以我用反斜杠将它们分成多行。然而,我们不称它们为大胆两行(或大胆七行)。
准备好变得大胆
在你开始创建大胆一行命令之前,花点时间调整好心态:
-
要灵活。
-
想想从哪里开始。
-
熟悉你的测试工具。
我会依次讨论每一个想法。
要灵活。
撰写大胆的一行命令的关键是灵活性。到现在为止,你学会了一些强大的工具——一套核心的 Linux 程序(以及运行它们的无数方法),还有命令历史记录、命令行编辑等等。你可以以多种方式结合这些工具,而每个问题通常都有多个解决方案。
即使是最简单的 Linux 任务也有多种完成方式。想想你会如何列出当前目录中的.jpg文件。我打赌 99.9%的 Linux 用户会运行像这样的命令:
$ ls *.jpg
但这只是众多解决方案中的一个例子。例如,你可以列出目录中的所有文件,然后使用grep仅匹配以.jpg结尾的文件名:
$ ls | grep '\.jpg$'
为什么选择这种解决方案?嗯,您在“长参数列表”中看到了一个例子,当目录包含太多文件时,无法通过模式匹配列出它们。按文件扩展名进行 grep 搜索 的技术是解决各种问题的强大、通用方法。重要的是要灵活并理解您的工具,以便在需要时应用最佳工具。这是创建大胆一行命令时的巫术技能。
以下所有命令列出当前目录中的.jpg文件。试着弄清楚每个命令的工作原理:
$ echo $(ls *.jpg)
$ bash -c 'ls *.jpg'
$ cat <(ls *.jpg)
$ find . -maxdepth 1 -type f -name \*.jpg -print
$ ls > tmp && grep '\.jpg$' tmp && rm -f tmp
$ paste <(echo ls) <(echo \*.jpg) | bash
$ bash -c 'exec $(paste <(echo ls) <(echo \*.jpg))'
$ echo 'monkey *.jpg' | sed 's/monkey/ls/' | bash
$ python -c 'import os; os.system("ls *.jpg")'
结果是否相同,还是某些命令的行为有所不同?您能否想出其他合适的命令?
思考从哪里开始
每个大胆的一行命令都以一个简单命令的输出开始。该输出可能是文件的内容、文件的一部分、目录列表、一系列数字或字母、用户列表、日期和时间或其他数据。因此,您的第一个挑战是生成命令的初始数据。
例如,如果您想知道英语字母表的第 17 个字母,那么您的初始数据可以是通过大括号扩展产生的 26 个字母:
$ echo {A..Z}
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
一旦您能够生成此输出,下一步就是决定如何进行处理以使其符合您的目标。您是否需要按行或列切片输出?将输出与其他信息连接?以更复杂的方式转换输出?可以查看第一章和第五章中的程序,如grep、sed和cut,并使用第七章中的技术应用它们。
对于这个例子,您可以使用awk打印第 17 个字段,或者使用sed删除空格并使用cut定位第 17 个字符:
$ echo {A..Z} | awk '{print $(17)}'
Q
$ echo {A..Z} | sed 's/ //g' | cut -c17
Q
作为另一个例子,如果您想打印一年中的月份,您的初始数据可以再次通过大括号扩展生成数字 1 到 12:
$ echo {1..12}
1 2 3 4 5 6 7 8 9 10 11 12
从这里开始,扩展大括号以形成每个月第一天的日期(从2021-01-01到2021-12-01);然后对每行运行date -d以生成月份名称:
$ echo 2021-{01..12}-01 | xargs -n1 date +%B -d
January
February
March
⋮
December
或者,假设您想知道当前目录中最长文件名的长度。您的初始数据可以是目录列表:
$ ls
animals.txt cartoon-mascots.txt ... zebra-stripes.txt
从这里开始,使用awk生成命令以计算每个文件名中字符的数量,并使用wc -c:
$ ls | awk '{print "echo -n", $0, "| wc -c"}'
echo -n "animals.txt" | wc -c
echo -n "cartoon-mascots.txt | wc -c"
⋮
echo -n "zebra-stripes.txt | wc -c"
(-n选项可以防止echo打印换行字符,这会使每个计数多一个。)最后,将命令管道传输给bash运行,将数字结果从高到低排序,并使用head -n1获取最大值(第一行):
$ ls | awk '{print "echo -n", $0, "| wc -c"}' | bash | sort -nr | head -n1
23
最后一个例子有些棘手,将管道生成为字符串并将其传递给进一步的管道。尽管如此,一般原则是相同的:找出您的起始数据并将其操纵以满足您的需求。
了解您的测试工具
构建一个大胆的一行代码可能需要反复试验。以下工具和技术将帮助您快速尝试不同的解决方案:
使用命令历史记录和命令行编辑。
在您尝试实验时,请不要重新输入命令。使用第三章中的技术来回忆先前的命令,调整它们并运行它们。
添加echo来测试您的表达式。
如果您不确定表达式将如何评估,请事先用echo打印它以查看 stdout 上的评估结果。
使用ls或添加echo来测试具有破坏性的命令。
如果您的命令调用rm、mv、cp或其他可能覆盖或移除文件的命令,请在它们前面加上echo以确认哪些文件将受到影响。(因此,不要执行rm,而是执行echo rm。)另一个安全策略是用ls替换rm以列出将被移除的文件。
插入一个tee来查看中间结果。
如果您想在长管道中间查看输出(stdout),插入tee命令将输出保存到文件以供检查。以下命令将command3的输出保存在文件outfile中,同时将相同的输出传递给command4:
$ *command1* | *command2* | *command3* | tee outfile | *command4* | *command5*
$ less outfile
好的,让我们来构建一些大胆的一行代码吧!
将文件名插入序列
这个大胆的一行代码与打开本章节的那个(重命名.jpg文件)相似,但更加详细。这也是我在写这本书时真实遇到的情况。像前一个一行代码一样,它结合了第七章中的两种技术:进程替换和管道到bash。结果是一个可重复使用的模式,用于解决类似问题。
我在一台使用名为AsciiDoc的排版语言的 Linux 计算机上写了这本书。这里并不重要语言的细节;重要的是每一章都是一个单独的文件,最初有 10 章:
$ ls
ch01.asciidoc ch03.asciidoc ch05.asciidoc ch07.asciidoc ch09.asciidoc
ch02.asciidoc ch04.asciidoc ch06.asciidoc ch08.asciidoc ch10.asciidoc
在某个时候,我决定在第二章和第三章之间插入第十一章。这意味着需要重命名一些文件。第 3 至第十章必须变为第 4 至第十一章,留下一个空隙,以便我可以创建一个新的第三章(ch03.asciidoc)。我本可以手动重命名文件,从ch11.asciidoc开始向后工作:^(2)
$ mv ch10.asciidoc ch11.asciidoc
$ mv ch09.asciidoc ch10.asciidoc
$ mv ch08.asciidoc ch09.asciidoc
⋮
$ mv ch03.asciidoc ch04.asciidoc
但这种方法很繁琐(想象一下如果有 1000 个文件而不是 11 个!),所以我生成了必要的mv命令并将它们传递给bash。好好看看前面的mv命令,并思考一下您可能如何创建它们。
首先专注于原始文件名ch03.asciidoc到ch10.asciidoc。你可以使用花括号扩展打印它们,比如ch{10..03}.asciidoc,就像本章节的第一个例子一样,但为了练习一些灵活性,使用seq -w命令来打印数字:
$ seq -w 10 -1 3
10
09
08
⋮
03
然后通过管道将这个数字序列转换为文件名到sed:
$ seq -w 10 -1 3 | sed 's/\(.*\)/ch\1.asciidoc/'
ch10.asciidoc
ch09.asciidoc
⋮
ch03.asciidoc
现在您有一个原始文件名列表。同样地,为第 4 至 11 章创建目标文件名:
$ seq -w 11 -1 4 | sed 's/\(.*\)/ch\1.asciidoc/'
ch11.asciidoc
ch10.asciidoc
⋮
ch04.asciidoc
要形成mv命令,您需要将原始文件名和新文件名并排打印出来。本章第一个示例使用paste解决了“并排打印”的问题,并使用进程替换将两个打印列表视为文件。在这里也要做同样的操作:
$ paste <(seq -w 10 -1 3 | sed 's/\(.*\)/ch\1.asciidoc/') \
<(seq -w 11 -1 4 | sed 's/\(.*\)/ch\1.asciidoc/')
ch10.asciidoc ch11.asciidoc
ch09.asciidoc ch10.asciidoc
⋮
ch03.asciidoc ch04.asciidoc
提示
上述命令可能看起来很长,但是通过命令历史和 Emacs 风格的命令行编辑,其实并不复杂。要从单一的“seq和sed”行转到paste命令:
-
使用向上箭头从历史记录中调用前一个命令。
-
按下 Ctrl-A 然后 Ctrl-K 来剪切整行。
-
输入单词
paste,然后加上一个空格。 -
按两次 Ctrl-Y 来创建
seq和sed命令的两个副本。 -
使用移动和编辑键来修改第二份副本。
-
依此类推。
通过将输出导向sed来在每行前面添加mv,打印出您所需的mv命令:
$ paste <(seq -w 10 -1 3 | sed 's/\(.*\)/ch\1.asciidoc/') \
<(seq -w 11 -1 4 | sed 's/\(.*\)/ch\1.asciidoc/') \
| sed 's/^/mv /'
mv ch10.asciidoc ch11.asciidoc
mv ch09.asciidoc ch10.asciidoc
⋮
mv ch03.asciidoc ch04.asciidoc
作为最后一步,将命令导向bash以执行:
$ paste <(seq -w 10 -1 3 | sed 's/\(.*\)/ch\1.asciidoc/') \
<(seq -w 11 -1 4 | sed 's/\(.*\)/ch\1.asciidoc/') \
| sed 's/^/mv /' \
| bash
我在我的书中确实使用了这个解决方案。mv命令运行后,生成的文件是第 1、2 和 4-11 章,留下了一个新的第三章的空白:
$ ls ch*.asciidoc
ch01.asciidoc ch04.asciidoc ch06.asciidoc ch08.asciidoc ch10.asciidoc
ch02.asciidoc ch05.asciidoc ch07.asciidoc ch09.asciidoc ch11.asciidoc
我刚刚呈现的模式可以在各种情况下重复使用以运行一系列相关命令:
-
在 stdout 上生成命令参数作为列表。
-
使用
paste和进程替换并排打印列表。 -
使用
sed将命令名称前置,通过替换行首字符(^)来添加程序名称和空格。 -
将结果导向
bash。
检查匹配的文件对
这个大胆的一行代码受到了 Mediawiki 的实际用例启发,这是驱动维基百科和成千上万其他维基站点的软件。Mediawiki 允许用户上传图片进行显示。大多数用户通过网页表单进行手动过程:点击“选择文件”以弹出文件对话框,浏览到图像文件并选择它,在表单中添加描述性注释,然后点击“上传”。维基管理员使用更自动化的方法:一个脚本读取整个目录并上传其图片。每个图像文件(例如bald_eagle.jpg)都与一个文本文件(bald_eagle.txt)配对,其中包含关于图像的描述性注释。
想象一下,你面对着一个充满数百个图像文件和文本文件的目录。你希望确认每个图像文件都有一个匹配的文本文件,反之亦然。这里是该目录的较小版本:
$ ls
bald_eagle.jpg blue_jay.jpg cardinal.txt robin.jpg wren.jpg
bald_eagle.txt cardinal.jpg oriole.txt robin.txt wren.txt
让我们开发两种不同的解决方案来识别任何不匹配的文件。对于第一个解决方案,创建两个列表,一个用于 JPEG 文件,一个用于文本文件,并使用cut去掉它们的文件扩展名.txt和.jpg:
$ ls *.jpg | cut -d. -f1
bald_eagle
blue_jay
cardinal
robin
wren
$ ls *.txt | cut -d. -f1
bald_eagle
cardinal
oriole
robin
wren
然后使用进程替换使用diff来比较列表:
$ diff <(ls *.jpg | cut -d. -f1) <(ls *.txt | cut -d. -f1)
2d1
< blue_jay
3a3
> oriole
您可以在这里停下来,因为输出表明第一个列表有一个额外的blue_jay(意味着blue_jay.jpg),第二个列表有一个额外的oriole(意味着oriole.txt)。尽管如此,让我们使结果更加精确。通过在每行开头 grep 字符<和>来消除不需要的行:
$ diff <(ls *.jpg | cut -d. -f1) <(ls *.txt | cut -d. -f1) \
| grep '^[<>]'
< blue_jay
> oriole
然后使用awk根据文件名($2)前面是<还是>来附加正确的文件扩展名:
$ diff <(ls *.jpg | cut -d. -f1) <(ls *.txt | cut -d. -f1) \
| grep '^[<>]' \
| awk '/^</{print $2 ".jpg"} /^>/{print $2 ".txt"}'
blue_jay.jpg
oriole.txt
现在你已经有了未匹配文件的列表。然而,这个解决方案存在一个微妙的 bug。假设当前目录包含文件名yellow.canary.jpg,其中有两个点。上述命令将产生错误的输出:
blue_jay.jpg
oriole.txt
yellow.jpg *This is wrong*
此问题发生是因为两个cut命令从第一个点开始而不是从最后一个点开始移除字符,所以yellow.canary.jpg被截断为yellow而不是yellow.canary。为了解决这个问题,用sed替换cut,从最后一个点到字符串末尾删除字符:
$ diff <(ls *.jpg | sed 's/\.[^.]*$//') \
<(ls *.txt | sed 's/\.[^.]*$//') \
| grep '^[<>]' \
| awk '/</{print $2 ".jpg"} />/{print $2 ".txt"}'
blue_jay.txt
oriole.jpg
yellow.canary.txt
第一个解决方案现在完成了。第二个解决方案采用了不同的方法。不是将diff应用于两个列表,而是生成一个单独的列表并删除匹配的文件名对。首先使用sed(使用与之前相同的 sed 脚本)去掉文件扩展名,并用uniq -c计算每个字符串的出现次数:
$ ls *.{jpg,txt} \
| sed 's/\.[^.]*$//' \
| uniq -c
2 bald_eagle
1 blue_jay
2 cardinal
1 oriole
2 robin
2 wren
1 yellow.canary
输出的每一行包含数字2,表示匹配的文件名对,或者1,表示未匹配的文件名。使用awk来隔离以空格开头和1开头的行,并只打印第二个字段:
$ ls *.{jpg,txt} \
| sed 's/\.[^.]*$//' \
| uniq -c \
| awk '/^ *1 /{print $2}'
blue_jay
oriole
yellow.canary
对于最后一步,如何添加丢失的文件扩展名?不要费心进行任何复杂的字符串操作。只需使用ls列出当前目录中的实际文件。用awk在每行输出的末尾添加一个星号(通配符):
$ ls *.{jpg,txt} \
| sed 's/\.[^.]*$//' \
| uniq -c \
| awk '/^ *1 /{print $2 "*"}'
blue_jay*
oriole*
yellow.canary*
并通过命令替换将这些行传递给ls。Shell 执行模式匹配,而ls列出未匹配的文件名。完成!
$ ls -1 $(ls *.{jpg,txt} \
| sed 's/\.[^.]*$//' \
| uniq -c \
| awk '/^ *1 /{print $2 "*"}')
blue_jay.jpg
oriole.txt
yellow.canary.jpg
从你的主目录生成一个 CDPATH
在章节“Organize Your Home Directory for Fast Navigation”中,你手动编写了一个复杂的CDPATH行。它以$HOME开头,然后是所有$HOME的子目录,并以相对路径..(父目录)结束:
CDPATH=$HOME:$HOME/Work:$HOME/Family:$HOME/Finances:$HOME/Linux:$HOME/Music:..
让我们创建一个大胆的一行命令来自动生成CDPATH行,适合插入到bash配置文件中。从$HOME中的子目录列表开始,使用子 Shell 防止cd命令改变你的 Shell 当前目录:
$ (cd && ls -d */)
Family/ Finances/ Linux/ Music/ Work/
使用sed在每个目录前面添加$HOME/:
$ (cd && ls -d */) | sed 's/^/$HOME\//g'
$HOME/Family/
$HOME/Finances/
$HOME/Linux/
$HOME/Music/
$HOME/Work/
前面的sed脚本稍微复杂,因为替换字符串$HOME/包含一个斜杠,并且sed替换也使用斜杠作为分隔符。这就是为什么我的斜杠被转义:$HOME\/。为了简化,回想一下在“Substitution and Slashes”中提到,sed接受任何方便的字符作为分隔符。让我们使用@符号代替斜杠,这样就不需要转义了:
$ (cd && ls -d */) | sed 's@^@$HOME/@g'
$HOME/Family/
$HOME/Finances/
$HOME/Linux/
$HOME/Music/
$HOME/Work/
接下来,使用另一个sed表达式去掉最后的斜杠:
$ (cd && ls -d */) | sed -e 's@^@$HOME/@' -e 's@/$@@'
$HOME/Family
$HOME/Finances
$HOME/Linux
$HOME/Music
$HOME/Work
使用echo和命令替换将输出打印在单行上。请注意,你不再需要显式地在cd和ls周围使用普通括号创建子 shell,因为命令替换会创建自己的子 shell:
$ echo $(cd && ls -d */ | sed -e 's@^@$HOME/@' -e 's@/$@@')
$HOME/Family $HOME/Finances $HOME/Linux $HOME/Music $HOME/Work
添加第一个目录$HOME和最终相对目录..:
$ echo '$HOME' \
$(cd && ls -d */ | sed -e 's@^@$HOME/@' -e 's@/$@@') \
..
$HOME $HOME/Family $HOME/Finances $HOME/Linux $HOME/Music $HOME/Work ..
通过将到目前为止的所有输出管道传输到tr来将空格更改为冒号:
$ echo '$HOME' \
$(cd && ls -d */ | sed -e 's@^@$HOME/@' -e 's@/$@@') \
.. \
| tr ' ' ':'
$HOME:$HOME/Family:$HOME/Finances:$HOME/Linux:$HOME/Music:$HOME/Work:..
最后,添加CDPATH环境变量,你就生成了一个变量定义,可以粘贴到bash配置文件中。将此命令存储在一个脚本中,随时生成该行,比如当你向$HOME添加新子目录时:
$ echo 'CDPATH=$HOME' \
$(cd && ls -d */ | sed -e 's@^@$HOME/@' -e 's@/$@@') \
.. \
| tr ' ' ':'
CDPATH=$HOME:$HOME/Family:$HOME/Finances:$HOME/Linux:$HOME/Music:$HOME/Work:..
生成测试文件
在软件行业中的常见任务是测试——向程序提供各种数据以验证程序的预期行为。下一个勇敢的一行生成包含随机文本的一千个文件,这些文件可用于软件测试。数字一千是任意的;你可以生成任意数量的文件。
该解决方案将随机从大型文本文件中选择单词,并创建一千个包含随机内容和长度的较小文件。一个完美的源文件是系统字典/usr/share/dict/words,其中包含 102,305 个单词,每个单词占一行。
$ wc -l /usr/share/dict/words
102305 /usr/share/dict/words
要生成这个勇敢的一行,你需要解决四个谜题:
-
随机洗牌字典文件
-
从字典文件中随机选择几行
-
创建一个输出文件来保存结果
-
运行你的解决方案一千次
要将字典随机打乱顺序,使用命令shuf,命名得当。每次运行命令shuf /usr/share/dict/words都会产生超过十万行的输出,因此使用head查看前几行随机行:
$ shuf /usr/share/dict/words | head -n3
evermore
shirttail
tertiary
$ shuf /usr/share/dict/words | head -n3
interactively
opt
perjurer
你的第一个谜题解决了。接下来,你如何从洗牌后的字典中选择随机数量的行?shuf有一个选项-n,可以打印给定数量的行,但你希望每次创建输出文件时该值都会变化。幸运的是,bash有一个变量RANDOM,它保存一个介于 0 和 32,767 之间的随机正整数。每次访问该变量时,它的值都会改变:
$ echo $RANDOM $RANDOM $RANDOM
7855 11134 262
因此,运行带有选项-n $RANDOM的shuf以打印随机数量的随机行。同样,完整输出可能会非常长,因此将结果管道传输到wc -l以确认每次执行时行数会改变:
$ shuf -n $RANDOM /usr/share/dict/words | wc -l
9922
$ shuf -n $RANDOM /usr/share/dict/words | wc -l
32465
你已经解决了第二个谜题。接下来,你需要一千个输出文件,或更具体地说,一千个不同的文件名。要生成文件名,请运行程序pwgen,它生成字母和数字的随机字符串:
$ pwgen
eng9nooG ier6YeVu AhZ7naeG Ap3quail poo2Ooj9 OYiuri9m iQuash0E voo3Eph1
IeQu7mi6 eipaC2ti exah8iNg oeGhahm8 airooJ8N eiZ7neez Dah8Vooj dixiV1fu
Xiejoti6 ieshei2K iX4isohk Ohm5gaol Ri9ah4eX Aiv1ahg3 Shaew3ko zohB4geu
⋮
添加选项-N1以生成一个字符串,并将字符串长度(10)作为参数指定:
$ pwgen -N1 10
ieb2ESheiw
可选择使用命令替换使字符串看起来更像文本文件的名称:
$ echo $(pwgen -N1 10).txt
ohTie8aifo.txt
第三个谜题完成了!现在你拥有生成单个随机文本文件的所有工具。使用shuf的-o选项将其输出保存在一个文件中:
$ mkdir -p /tmp/randomfiles && cd /tmp/randomfiles
$ shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words
并检查结果:
$ ls *List the new file*
Ahxiedie2f.txt
$ wc -l Ahxiedie2f.txt *How many lines does it contain?*
13544 Ahxiedie2f.txt
$ head -n3 Ahxiedie2f.txt *Peek at the first few lines*
saviors
guerillas
forecaster
看起来不错!最后一个谜题是如何一千次运行前面的shuf命令。您当然可以使用循环:
for i in {1..1000}; do
shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words
done
但这并不像创建大胆一行一句那么有趣。相反,让我们预先生成命令作为字符串,并通过bash管道传递它们。作为测试,使用echo打印您想要的命令一次。添加单引号以确保$RANDOM不被评估,pwgen不运行:
$ echo 'shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words'
shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words
这个命令可以轻松地通过bash管道执行:
$ echo 'shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words' | bash
$ ls
eiFohpies1.txt
现在,使用yes命令通过管道传递给head打印一千次您的命令,然后将结果传递给bash,您已经解决了第四个谜题:
$ yes 'shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words' \
| head -n 1000 \
| bash
$ ls
Aen1lee0ir.txt IeKaveixa6.txt ahDee9lah2.txt paeR1Poh3d.txt
Ahxiedie2f.txt Kas8ooJahK.txt aoc0Yoohoh.txt sohl7Nohho.txt
CudieNgee4.txt Oe5ophae8e.txt haiV9mahNg.txt uchiek3Eew.txt
⋮
如果您更喜欢一千个随机图像文件而不是文本文件,可以使用相同的技术(yes、head和bash),并用生成随机图像的命令替换shuf。以下是我从Stack Overflow 上 Mark Setchell 的解决方案中改编的大胆一行一句。它运行来自图形包 ImageMagick 的convert命令,以产生大小为 100 x 100 像素的由多彩方块组成的随机图像:
$ yes 'convert -size 8x8 xc: +noise Random -scale 100x100 $(pwgen -N1 10).png' \
| head -n 1000 \
| bash
$ ls
Bahdo4Yaop.png Um8ju8gie5.png aing1QuaiX.png ohi4ziNuwo.png
Eem5leijae.png Va7ohchiep.png eiMoog1kou.png ohnohwu4Ei.png
Eozaing1ie.png Zaev4Quien.png hiecima2Ye.png quaepaiY9t.png
⋮
$ display Bahdo4Yaop.png *View the first image*
生成空文件
有时候,进行测试所需的仅仅是具有不同名称的大量文件,即使它们是空的。生成命名为file0001.txt至file1000.txt的一千个空文件就像这样简单:
$ mkdir /tmp/empties *Create a directory for the files*
$ cd /tmp/empties
$ touch file{01..1000}.txt *Generate the files*
如果你喜欢更有趣的文件名,可以随机从系统字典中选择。使用grep限制名称为小写字母以简化(避免空格、撇号和其他对 shell 特殊的字符):
$ grep '^[a-z]*$' /usr/share/dict/words
a
aardvark
aardvarks
⋮
使用shuf混洗名称并用head打印前一千个:
$ grep '^[a-z]*$' /usr/share/dict/words | shuf | head -n1000
triplicating
quadruplicates
podiatrists
⋮
最后,将结果通过xargs管道传递给touch以创建文件:
$ grep '^[a-z]*$' /usr/share/dict/words | shuf | head -n1000 | xargs touch
$ ls
abases distinctly magnolia sadden
abets distrusts maintaining sales
aboard divided malformation salmon
⋮
概要
希望本章的例子有助于培养你编写大胆一行一句的技能。其中几个提供了可重复使用的模式,你可能会在其他情况下发现它们有用。
注意:在城里,大胆的一行一句并非唯一的解决方案。它们只是在命令行高效工作的一种方法。有时候,编写一个 shell 脚本可能会更有价值。其他时候,使用像 Perl 或 Python 这样的编程语言可能会找到更好的解决方案。然而,编写大胆的一行一句是执行关键任务的一项重要技能,快速而富有风格。
^(1) 我所知道的此术语最早使用(来自 BSD Unix 4.x 中的lorder(1)的 manpage)。感谢 Bob Byrnes 找到它。
^(2) 从ch03.asciidoc开始并向前工作可能是危险的——你能看出为什么吗?如果不能,使用命令touch ch{01..10}.asciidoc创建这些文件并自行尝试。
第九章:利用文本文件
在许多 Linux 系统上,纯文本是最常见的数据格式。大多数管道中从命令到命令发送的内容都是文本。程序员的源代码文件、系统配置文件(位于 /etc)、HTML 和 Markdown 文件都是文本文件。电子邮件消息是文本;即使是附件也以文本形式内部存储以进行传输。你甚至可以将像购物清单和个人笔记这样的日常文件存储为文本。
与今天的互联网形成对比,后者充斥着流媒体音频和视频、社交媒体帖子、Google Docs 和 Office 365 中的浏览器文档、PDF 和其他富媒体。 (更不用说移动应用程序处理的数据,这些应用程序已经将“文件”的概念隐藏起来,成为整个一代人的隐喻。)在这样的背景下,纯文本文件似乎显得有些古老。
然而,任何文本文件都可以成为你可以用精心设计的 Linux 命令挖掘的丰富数据源,特别是如果文本是结构化的。例如,文件 /etc/passwd 中的每一行代表一个 Linux 用户,具有七个字段,包括用户名、数字用户 ID、主目录等。这些字段由冒号分隔,使得文件可以轻松被 cut -d: 或 awk -F: 解析。这里有一个按字母顺序打印所有用户名(第一个字段)的命令:
$ cut -d: -f1 /etc/passwd | sort
avahi
backup
daemon
⋮
这里有一个例子,通过数字用户 ID 将人类用户与系统帐户分开,并向用户发送欢迎电子邮件。让我们逐步构建这个大胆的单行代码。首先使用 awk 打印用户名(第一个字段),当数字用户 ID(第三个字段)为 1000 或更大时:
$ awk -F: '$3>=1000 {print $1}' /etc/passwd
jones
smith
然后通过管道传递给 xargs 生成问候语:
$ awk -F: '$3>=1000 {print $1}' /etc/passwd \
| xargs -I@ echo "Hi there, @!"
Hi there, jones!
Hi there, smith!
然后生成命令(字符串),通过 mail 命令将每个问候发送给指定用户,带有给定的主题行(-s):
$ awk -F: '$3>=1000 {print $1}' /etc/passwd \
| xargs -I@ echo 'echo "Hi there, @!" | mail -s greetings @'
echo "Hi there, jones!" | mail -s greetings jones
echo "Hi there, smith!" | mail -s greetings smith
最后,将生成的命令通过管道传递给 bash 发送电子邮件:
$ awk -F: '$3>=1000 {print $1}' /etc/passwd \
| xargs -I@ echo 'echo "Hi there, @!" | mail -s greetings @' \
| bash
echo "Hi there, jones!" | mail -s greetings jones
echo "Hi there, smith!" | mail -s greetings smith
像本书中的许多其他解决方案一样,这些解决方案从现有的文本文件开始,并使用命令操作其内容。现在是时候扭转这种方法,有意设计新的文本文件,以便与 Linux 命令良好合作。这是在 Linux 系统上高效完成工作的一种成功策略,只需四个步骤:
-
注意你想解决的涉及数据的业务问题。
-
将数据以便捷的格式存储在文本文件中。
-
发明 Linux 命令来处理文件以解决问题。
-
(可选。)将这些命令捕获在脚本、别名或函数中,以简化运行。
在本章中,你将构建各种结构化文本文件,并创建命令来处理它们,以解决几个业务问题。
第一个例子:查找文件
假设你的主目录包含数万个文件和子目录,而且经常会忘记其中一个文件的位置。find 命令可以按文件名(例如 animals.txt)定位文件:
$ find $HOME -name animals.txt -print
/home/smith/Work/Writing/Books/Lists/animals.txt
但find很慢,因为它搜索整个主目录,而且你需要定期定位文件。这是第 1 步,注意到涉及数据的业务问题:通过名称快速在你的主目录中找到文件。
第 2 步是以方便的格式将数据存储在文本文件中。运行find一次,以构建所有文件和目录的列表,每行一个文件路径,并将其存储在一个隐藏文件中:
$ find $HOME -print > $HOME/.ALLFILES
$ head -n3 $HOME/.ALLFILES
/home/smith
/home/smith/Work
/home/smith/Work/resume.pdf
⋮
现在你有数据:文件的逐行索引。第 3 步是发明 Linux 命令以加快文件搜索,为此使用grep。在大型文件中进行grep要比在大型目录树中运行find快得多:
$ grep animals.txt $HOME/.ALLFILES
/home/smith/Work/Writing/Books/Lists/animals.txt
第 4 步是使命令更易于运行。编写一个名为ff的单行脚本,表示“查找文件”,该脚本运行grep与任何用户提供的选项和搜索字符串,如示例 9-1 中所示。
示例 9-1. ff脚本
#!/bin/bash
# $@ means all arguments provided to the script
grep "$@" $HOME/.ALLFILES
使脚本可执行,并将其放入搜索路径中的任何目录,例如您的个人bin子目录:
$ chmod +x ff
$ echo $PATH *Check your search path*
/home/smith/bin:/usr/local/bin:/usr/bin:/bin
$ mv ff ~/bin
随时运行ff来快速定位文件,当你忘记放置它们的位置时。
$ ff animal
/home/smith/Work/Writing/Books/Lists/animals.txt
$ ff -i animal | less *Case-insensitive grep*
/home/smith/Work/Writing/Books/Lists/animals.txt
/home/smith/Vacations/Zoos/Animals/pandas.txt
/home/smith/Vacations/Zoos/Animals/tigers.txt
⋮
$ ff -i animal | wc -l *How many matches?*
16
定期重新运行find命令以更新索引。(或者更好的办法是使用cron创建定期作业;参见“学习 cron、crontab 和 at”。)Voilà——你已经利用两个小命令构建了一个快速灵活的文件搜索实用程序。Linux 系统提供了其他快速索引和搜索文件的应用程序,如locate命令以及 GNOME、KDE Plasma 和其他桌面环境中的搜索实用程序,但这不是重点。看看你自己创建它多么容易。成功的关键是创建一个简单格式的文本文件。
检查域名过期
对于下一个示例,假设你拥有一些互联网域名,并希望跟踪它们的到期时间以便续订。这是第 1 步,确定业务问题。第 2 步是创建这些域名的文件,例如domains.txt,每行一个域名:
example.com
oreilly.com
efficientlinux.com
⋮
第 3 步是发明利用这个文本文件来确定到期日期的命令。从查询域名注册商提供有关域名信息的whois命令开始:
$ whois example.com | less
Domain Name: EXAMPLE.COM
Registry Domain ID: 2336799_DOMAIN_COM-VRSN
Registrar WHOIS Server: whois.iana.org
Updated Date: 2021-08-14T07:01:44Z
Creation Date: 1995-08-14T04:00:00Z
Registry Expiry Date: 2022-08-13T04:00:00Z
⋮
该过期日期之前的字符串“Registry Expiry Date”可通过grep和awk分离出来:
$ whois example.com | grep 'Registry Expiry Date:'
Registry Expiry Date: 2022-08-13T04:00:00Z
$ whois example.com | grep 'Registry Expiry Date:' | awk '{print $4}'
2022-08-13T04:00:00Z
可通过date --date命令使日期更易读,该命令可以将一个日期字符串从一种格式转换为另一种格式:
$ date --date 2022-08-13T04:00:00Z
Sat Aug 13 00:00:00 EDT 2022
$ date --date 2022-08-13T04:00:00Z +'%Y-%m-%d' *Year-month-day format*
2022-08-13
使用命令替换将whois的日期字符串提供给date命令:
$ echo $(whois example.com | grep 'Registry Expiry Date:' | awk '{print $4}')
2022-08-13T04:00:00Z
$ date \
--date $(whois example.com \
| grep 'Registry Expiry Date:' \
| awk '{print $4}') \
+'%Y-%m-%d'
2022-08-13
现在你有一个命令,查询注册商并打印到期日期。创建一个名为check-expiry的脚本,如示例 9-2 所示,运行前述命令并打印到期日期、一个制表符和域名:
$ ./check-expiry example.com
2022-08-13 example.com
示例 9-2. check-expiry脚本
#!/bin/bash
expdate=$(date \
--date $(whois "$1" \
| grep 'Registry Expiry Date:' \
| awk '{print $4}') \
+'%Y-%m-%d')
echo "$expdate $1" # Two values separated by a tab
现在,使用循环检查文件domains.txt中的所有域。创建一个新脚本check-expiry-all,如示例 9-3 所示。
示例 9-3. check-expiry-all脚本
#!/bin/bash
cat domains.txt | while read domain; do
./check-expiry "$domain"
sleep 5 # Be kind to the registrar's server
done
将脚本在后台运行,因为如果你有很多域名的话,可能需要一段时间,并将所有输出(stdout 和 stderr)重定向到文件:
$ ./check-expiry-all &> expiry.txt &
脚本完成后,文件expiry.txt包含所需的信息:
$ cat expiry.txt
2022-08-13 example.com
2022-05-26 oreilly.com
2022-09-17 efficientlinux.com
⋮
万岁!但不要停在这里。文件expiry.txt本身结构良好,便于进一步处理,有两列标签。例如,对日期进行排序,找到下一个需要续订的域名:
$ sort -n expiry.txt | head -n1
2022-05-26 oreilly.com
或者,使用awk查找今天到期或将要到期的域名,即其到期日期(字段 1)小于或等于今天的日期(使用date +%Y-%m-%d打印):
$ awk "\$1<=\"$(date +%Y-%m-%d)\"" expiry.txt
关于前述awk命令的几点说明:
-
在
awk之前,我转义了美元符号(在$1前)和日期字符串周围的双引号,这样 shell 在awk执行之前就不会对它们进行评估。 -
我稍微作弊了,使用字符串运算符
<=来比较日期。这不是数学比较,只是字符串比较,但它能够工作,因为日期格式YYYY-MM-DD按字母顺序和时间顺序排序。
更多努力的话,你可以在awk中进行日期数学运算,报告到期日期,比如提前两周,然后创建一个定时作业,每晚运行脚本并通过电子邮件发送报告。随时进行实验。然而,这里的重点是,再次用几个命令,你已经构建了一个由文本文件驱动的有用工具。
构建区号数据库
下一个示例使用一个包含三个字段的文件,你可以用多种方式处理它。文件名为areacodes.txt,包含美国的电话区号。从本书的补充材料中的目录chapter09/build_area_code_database获取文件,或者自己创建一个文件,比如从Wikipedia获取:^(2)
201 NJ Hackensack, Jersey City
202 DC Washington
203 CT New Haven, Stamford
⋮
989 MI Saginaw
提示
先排列长度可预测的字段,这样列就会整齐地对齐。如果把城市名放在第一列,文件看起来会多么凌乱:
Hackensack, Jersey City 201 NJ
Washington 202 DC
⋮
一旦这个文件就位,你可以做很多事情。使用grep按州查找区号,添加-w选项以匹配完整的单词(以防其他文本恰好包含“NJ”):
$ grep -w NJ areacodes.txt
201 NJ Hackensack, Jersey City
551 NJ Hackensack, Jersey City
609 NJ Atlantic City, Trenton, southeast and central west
⋮
或者按区号查找城市:
$ grep -w 202 areacodes.txt
202 DC Washington
或者通过文件中的任何字符串查找:
$ grep Washing areacodes.txt
202 DC Washington
227 MD Silver Spring, Washington suburbs, Frederick
240 MD Silver Spring, Washington suburbs, Frederick
⋮
使用wc计算区号:
$ wc -l areacodes.txt
375 areacodes.txt
找到区号最多的州(冠军是加利福尼亚州,有 38 个):
$ cut -f2 areacodes.txt | sort | uniq -c | sort -nr | head -n1
38 CA
将文件转换为 CSV 格式,以导入电子表格应用程序。打印第三个字段时用双引号括起来,以防止其逗号被解释为 CSV 分隔符:
$ awk -F'\t' '{printf "%s,%s,\"%s\"\n", $1, $2, $3}' areacodes.txt \
> areacodes.csv
$ head -n3 areacodes.csv
201,NJ,"Hackensack, Jersey City"
202,DC,"Washington"
203,CT,"New Haven, Stamford"
将给定州的所有区号汇总到一行上:
$ awk '$2~/^NJ$/{ac=ac FS $1} END {print "NJ:" ac}' areacodes.txt
NJ: 201 551 609 732 848 856 862 908 973
或者为每个州使用数组和for循环汇总,如“提高重复文件检测器”中所示:
$ awk '{arr[$2]=arr[$2] " " $1} \
END {for (i in arr) print i ":" arr[i]}' areacodes.txt \
| sort
AB: 403 780
AK: 907
AL: 205 251 256 334 659
⋮
WY: 307
将任何前述命令转换为别名、函数或脚本,方便使用。一个简单的例子是示例 9-4 中的areacode脚本。
示例 9-4. areacode 脚本
#!/bin/bash
if [ -n "$1" ]; then
grep -iw "$1" areacodes.txt
fi
areacode脚本搜索areacodes.txt文件中的任何整词,如区号、州名缩写或城市名:
$ areacode 617
617 MA Boston
构建密码管理器
作为最后深入的示例,让我们将用户名、密码和备注存储在加密的文本文件中,以结构化格式便于在命令行上进行检索。生成的命令是一个基本的密码管理器,一个简化记忆大量复杂密码负担的应用程序。
警告
密码管理在计算机安全中是一个复杂的主题。这个例子创建了一个非常基本的密码管理器作为教育练习。不要将其用于关键任务应用程序。
密码文件,命名为vault,有三个字段,由单个制表符分隔:
-
用户名
-
密码
-
注意(任何文本)
创建vault文件并添加数据。文件尚未加密,所以现在只插入虚假密码。
$ touch vault *Create an empty file*
$ chmod 600 vault *Set file permissions*
$ emacs vault *Edit the file*
$ cat vault
sally fake1 google.com account
ssmith fake2 dropbox.com account for work
s999 fake3 Bank of America account, bankofamerica.com
smith2 fake4 My blog at wordpress.org
birdy fake5 dropbox.com account for home
将保险库存储在已知位置:
$ mkdir ~/etc
$ mv vault ~/etc
这个想法是使用像grep或awk这样的模式匹配程序打印匹配给定字符串的行。这种简单而强大的技术可以匹配任何行的任何部分,而不仅仅是用户名或网站。例如:
$ cd ~/etc
$ grep sally vault *Match a username*
sally fake1 google.com account
$ grep work vault *Match the notes*
ssmith fake2 dropbox.com account for work
$ grep drop vault *Match multiple lines*
ssmith fake2 dropbox.com account for work
birdy fake5 dropbox.com account for home
在脚本中捕获这个简单的功能;然后,我们一步步改进它,包括最终对vault文件进行加密。将脚本命名为pman,表示“密码管理器”,并在示例 9-5中创建最简单的版本。
示例 9-5. pman 版本 1:尽可能简单
#!/bin/bash
# Just print matching lines
grep "$1" $HOME/etc/vault
将脚本存储在您的搜索路径中:
$ chmod 700 pman
$ mv pman ~/bin
尝试这个脚本:
$ pman goog
sally fake1 google.com account
$ pman account
sally fake1 google.com account
ssmith fake2 dropbox.com account for work
s999 fake3 Bank of America account, bankofamerica.com
birdy fake5 dropbox.com account for home
$ pman facebook *(produces no output)*
示例 9-6中的下一个版本添加了一些错误检查和一些值得记住的变量名。
示例 9-6. pman 版本 2:添加一些错误检查
#!/bin/bash
# Capture the script name.
# $0 is the path to the script, and basename prints the final filename.
PROGRAM=$(basename $0)
# Location of the password vault
DATABASE=$HOME/etc/vault
# Ensure that at least one argument was provided to the script.
# The expression >&2 directs echo to print on stderr instead of stdout.
if [ $# -ne 1 ]; then
>&2 echo "$PROGRAM: look up passwords by string"
>&2 echo "Usage: $PROGRAM string"
exit 1
fi
# Store the first argument in a friendly, named variable
searchstring="$1"
# Search the vault and print an error message if nothing matches
grep "$searchstring" "$DATABASE"
if [ $? -ne 0 ]; then
>&2 echo "$PROGRAM: no matches for '$searchstring'"
exit 1
fi
运行脚本:
$ pman
pman: look up passwords by string
Usage: pman string
$ pman smith
ssmith fake2 dropbox.com account for work
smith2 fake4 My blog at wordpress.org
$ pman xyzzy
pman: no matches for 'xyzzy'
这种技术的一个缺点是它无法扩展。如果vault包含数百行,而grep匹配并打印了其中的 63 行,你将不得不靠眼睛寻找你需要的密码。通过在第三列中的每行添加一个唯一的键(一个字符串),并更新pman来首先搜索该唯一键来改进脚本。现在,vault文件,用粗体标记的第三列,看起来像这样:
sally fake1 google google.com account
ssmith fake2 dropbox dropbox.com account for work
s999 fake3 bank Bank of America account, bankofamerica.com
smith2 fake4 blog My blog at wordpress.org
birdy fake5 dropbox2 dropbox.com account for home
示例 9-7展示了使用awk而不是grep的更新脚本。它还使用命令替换来捕获输出并检查是否为空(测试-z表示“零长度字符串”)。请注意,如果搜索一个在vault中不存在的键,pman会回到其原始行为,并打印所有匹配搜索字符串的行。
示例 9-7. pman 版本 3:优先搜索第三列的关键字
#!/bin/bash
PROGRAM=$(basename $0)
DATABASE=$HOME/etc/vault
if [ $# -ne 1 ]; then
>&2 echo "$PROGRAM: look up passwords"
>&2 echo "Usage: $PROGRAM string"
exit 1
fi
searchstring="$1"
# Look for exact matches in the third column
match=$(awk '$3~/^'$searchstring'$/' "$DATABASE")
# If the search string doesn't match a key, find all matches
if [ -z "$match" ]; then
match=$(awk "/$searchstring/" "$DATABASE")
fi
# If still no match, print an error message and exit
if [ -z "$match" ]; then
>&2 echo "$PROGRAM: no matches for '$searchstring'"
exit 1
fi
# Print the match
echo "$match"
运行脚本:
$ pman dropbox
ssmith fake2 dropbox dropbox.com account for work
$ pman drop
ssmith fake2 dropbox dropbox.com account for work
birdy fake5 dropbox2 dropbox.com account for home
明文文件vault是一个安全风险,所以用标准的 Linux 加密程序 GnuPG 对其进行加密,调用gpg。如果您已经设置好了 GnuPG 供使用,那很好。否则,用以下命令设置它,提供您的电子邮件地址:^(3)
$ gpg --quick-generate-key *your_email_address* default default never
在为密钥输入密码时会提示您输入密码(两次)。提供一个强密码。当 gpg 完成后,您就可以使用公钥加密来加密密码文件,生成 vault.gpg 文件:
$ cd ~/etc
$ gpg -e -r *your_email_address* vault
$ ls vault*
vault vault.gpg
作为测试,将 vault.gpg 文件解密到标准输出:^(4)
$ gpg -d -q vault.gpg
Passphrase: xxxxxxxx
sally fake1 google google.com account
ssmith fake2 dropbox dropbox.com account for work
⋮
接下来,更新您的脚本以使用加密的 vault.gpg 文件而不是明文的 vault 文件。这意味着将 vault.gpg 解密到标准输出,并将其内容管道传输到 awk 进行匹配,如 示例 9-8 中所示。
示例 9-8. pman 版本 4:使用加密保险库
#!/bin/bash
PROGRAM=$(basename $0)
# Use the encrypted file
DATABASE=$HOME/etc/vault.gpg
if [ $# -ne 1 ]; then
>&2 echo "$PROGRAM: look up passwords"
>&2 echo "Usage: $PROGRAM string"
exit 1
fi
searchstring="$1"
# Store the decrypted text in a variable
decrypted=$(gpg -d -q "$DATABASE")
# Look for exact matches in the third column
match=$(echo "$decrypted" | awk '$3~/^'$searchstring'$/')
# If the search string doesn't match a key, find all matches
if [ -z "$match" ]; then
match=$(echo "$decrypted" | awk "/$searchstring/")
fi
# If still no match, print an error message and exit
if [ -z "$match" ]; then
>&2 echo "$PROGRAM: no matches for '$searchstring'"
exit 1
fi
# Print the match
echo "$match"
脚本现在显示来自加密文件的密码:
$ pman dropbox
Passphrase: xxxxxxxx
ssmith fake2 dropbox dropbox.com account for work
$ pman drop
Passphrase: xxxxxxxx
ssmith fake2 dropbox dropbox.com account for work
birdy fake5 dropbox2 dropbox.com account for home
所有的部件现在都准备就绪,用于您的密码管理器。一些最后的步骤是:
-
当您确信可以可靠地解密 vault.gpg 文件时,请删除原始的 vault 文件。
-
如果需要的话,用真实的密码替换虚假密码。参见 “直接编辑加密文件” 了解如何编辑加密文本文件的建议。
-
支持密码保险库中的注释——以井号 (
#) 开头的行——以便您可以对条目做出备注。为此,请更新脚本将解密内容传输到grep -v来过滤任何以井号开头的行:decrypted=$(gpg -d -q "$DATABASE" | grep -v '^#')
将密码打印到标准输出不利于安全性。“改进密码管理器” 将更新脚本,用复制粘贴代替打印密码。
概要
文件路径、域名、区号和登录凭据等数据在结构化文本文件中表现良好。比如:
-
您的音乐文件?(使用类似
id3tool的 Linux 命令从 MP3 文件中提取 ID3 信息并将其放入文件中。) -
您手机上的联系人?(使用应用程序将联系人导出为 CSV 格式,将其上传到云存储,然后从 Linux 机器下载以进行处理。)
-
您在学校的成绩?(使用
awk跟踪您的平均绩点。) -
您看过的电影或读过的书籍清单,包括额外的数据(评分、作者、演员等)?
通过这种方式,您可以构建一个由个人有意义或有助于工作的节省时间命令组成的生态系统,仅受您的想象力限制。
^(1) 这种方法类似于设计一个数据库模式,以适应已知的查询。
^(2) CSV 格式中的官方区号列表,由北美编号计划管理员维护,缺少城市名称。
^(3) 此命令使用所有默认选项和“永不”过期日期生成公钥/私钥对。要了解更多信息,请查看 man gpg 以了解 gpg 选项,或在线查找 GnuPG 教程。
^(4) 如果 gpg 在不提示输入密码的情况下继续进行,它已暂时缓存(保存)了您的密码。
第三部分:额外好玩的东西
最后几章深入探讨专业话题:有些详细介绍,有些只是简短地激发你的学习兴趣。
第十章:键盘效率
在典型的 Linux 工作站上,一天之中,您可能会打开许多应用程序窗口:网页浏览器、文本编辑器、软件开发环境、音乐播放器、视频编辑器、虚拟机等等。一些应用程序专注于 GUI,例如绘图程序,并且专为鼠标或轨迹球等指针设备设计。其他应用程序则更注重键盘操作,例如终端程序中的 Shell。典型的 Linux 用户可能每小时要在键盘和鼠标之间切换数十次(甚至数百次)。每次切换都需要时间。它会减慢您的工作效率。如果能减少切换次数,您将能够更高效地工作。
本章讨论如何在键盘上花费更多时间,少用指针设备。十根手指敲击一百个键通常比两根手指在鼠标上更灵活。我不只是在谈论使用键盘快捷键 —— 我相信您可以在不需要本书的情况下查找它们(尽管我会提供一些)。我谈论的是加快一些看似天生适合“鼠标操作”的日常任务的不同方法:操作窗口、从网络检索信息以及使用剪贴板复制和粘贴。
使用 Windows
在本节中,我分享了如何高效启动窗口的技巧,特别是 Shell 窗口(终端)和浏览器窗口。
即时 Shell 和浏览器
大多数 Linux 桌面环境,如 GNOME、KDE Plasma、Unity 和 Cinnamon,都提供某种方式来定义热键或自定义键盘快捷键 —— 特殊的按键组合,可以启动命令或执行其他操作。我强烈建议您为这些常见操作定义键盘快捷键:
-
打开一个新的 Shell 窗口(终端程序)
-
打开一个新的网页浏览器窗口
有了这些定义好的快捷键,您可以随时即刻打开终端或浏览器,而不管您正在使用的是哪个其他应用程序。^(1) 要设置这些快捷键,您需要了解以下内容:
启动您偏爱的终端程序的命令
一些流行的是 gnome-terminal、konsole 和 xterm。
启动您偏爱的浏览器的命令
一些流行的是 firefox、google-chrome 和 opera。
如何定义自定义键盘快捷键
每个桌面环境的说明略有不同,并且可能会随版本变化而变化,因此最好您在网上查找相关信息。搜索您的桌面环境名称,后面跟上“定义键盘快捷键”。
在我的桌面上,我将键盘快捷键 Ctrl-Windows-T 分配给运行 konsole,将 Ctrl-Windows-C 分配给运行 google-chrome。
工作目录
当您通过桌面环境的键盘快捷键启动一个 Shell 时,它是您登录 Shell 的子进程。其当前目录是您的主目录(除非您已经以某种方式配置为不同目录)。
将此与从终端程序内部打开新 shell 进行对比——通过在命令行中显式运行(比如)gnome-terminal 或 xterm,或使用终端程序的菜单打开一个新窗口。在这种情况下,新 shell 是该终端的 shell的子进程。其当前目录与其父进程相同,这可能不是你的主目录。
一次性窗口
假设你正在同时使用几个应用程序,突然需要一个 shell 来运行一个命令。许多用户会抓起鼠标,并在他们打开的窗口中搜索运行的终端。不要这样做——这是在浪费时间。只需用你的快捷键快速打开一个新的终端,运行你的命令,然后立即退出终端。
一旦你为启动终端程序和浏览器窗口分配了快捷键,放心地随意打开和关闭这些窗口。我推荐这样做!定期创建和销毁终端和浏览器窗口,而不是长时间保持它们开放。我称这些短暂存在的窗口为一次性窗口。你快速地打开它们,使用几分钟,然后关闭它们。
如果你正在开发软件或执行其他长时间的工作,可能会保留几个终端窗口很长时间,但一次性终端窗口非常适合在一天中的其他时间执行一些随机命令。弹出一个新终端通常比在屏幕上搜索现有终端更快。 不要问自己:“我需要的那个终端窗口在哪里?” 然后在桌面上四处查找。创建一个新的,并在其完成任务后关闭它。
同样适用于 Web 浏览器窗口。在经过一整天的 Linux 黑客工作后,你是否会抬头发现你的浏览器只有一个窗口,但却有 83 个打开的标签页?这是使用一次性窗口太少的一个症状。弹出一个窗口,查看你需要查看的网页,然后关闭它。需要稍后再访问页面吗?在浏览器历史记录中找到它。
浏览器键盘快捷键
谈到浏览器窗口,确保你知道 表 10-1 中最重要的键盘快捷键。如果你的手已经在键盘上,并且你想浏览新网站,通常按下 Ctrl-L 跳到地址栏或 Ctrl-T 打开一个标签页比使用鼠标更快。
表 10-1. Firefox、Google Chrome 和 Opera 的最重要的键盘快捷键
| 动作 | 键盘快捷键 |
|---|---|
| 打开新窗口 | Ctrl-N |
| 打开新的隐私/无痕窗口 | Ctrl-Shift-P (Firefox), Ctrl-Shift-N (Chrome 和 Opera) |
| 打开新标签页 | Ctrl-T |
| 关闭标签页 | Ctrl-W |
| 在浏览器标签页之间切换 | Ctrl-Tab(向前切换)和 Ctrl-Shift-Tab(向后切换) |
| 跳转到地址栏 | Ctrl-L(或 Alt-D 或 F6) |
| 查找(搜索)当前页面的文本 | Ctrl-F |
| 显示你的浏览历史记录 | Ctrl-H |
切换窗口和桌面
当你繁忙的桌面上充满窗口时,如何快速找到想要的窗口?你可以通过指点和点击的方式在混乱中寻找,但通常使用键盘快捷键 Alt-Tab 更快。持续按下 Alt-Tab 键,你将逐个循环浏览桌面上的所有窗口。当到达你想要的窗口时,释放键,该窗口即处于焦点并准备好使用。要反向循环,请按 Alt-Shift-Tab 键。
要在桌面上循环浏览属于同一应用程序的所有窗口,例如所有 Firefox 窗口,请按下 Alt-`(Alt-backquote,或 Alt 加上 Tab 键上方的键)。要向后循环,请加上 Shift 键(Alt-Shift-backquote)。
一旦能够切换窗口,现在是时候谈论切换桌面了。如果你在 Linux 上进行严肃工作,而且只使用一个桌面,那么你会错过一个很好的组织工作的方式。多个桌面,也称为工作区或虚拟桌面,正如它们听起来的那样。你可能不只有一个桌面,而是有四个、六个或更多,每个都有自己的窗口,你可以在它们之间切换。
在运行 KDE Plasma 的 Ubuntu Linux 工作站上,我运行六个虚拟桌面,并为它们分配不同的目的。第一个桌面是我的主工作区,用于电子邮件和浏览,第二个是家庭相关任务,第三个是运行 VMware 虚拟机,第四个用于撰写书籍等工作,第五和第六个则用于任何临时任务。这些一致的分配使得从不同应用程序中快速轻松地找到我的打开窗口。
每个 Linux 桌面环境如 GNOME、KDE Plasma、Cinnamon 和 Unity 都有自己实现虚拟桌面的方式,并且它们都提供一个图形化的“切换器”或“分页器”来在它们之间切换。我建议在你的桌面环境中定义键盘快捷键,以便快速跳转到每个桌面。在我的电脑上,我定义了 Windows + F1 到 Windows + F6 分别跳转到第 1 到第 6 个桌面。
还有许多其他与桌面和窗口工作方式相关的风格。有些人每个应用程序使用一个桌面:一个用于 shell,一个用于网页浏览,一个用于文字处理等等。有些人在小型笔记本屏幕上每个桌面只打开一个全屏窗口,而不是每个桌面上打开多个窗口。找到适合你的风格,只要它既快速又高效即可。
从命令行访问网页
点击浏览器几乎已成为网页的代名词,但是你也可以通过 Linux 命令行高效访问网站。
从命令行启动浏览器窗口
你可能习惯于通过点击或触摸图标来启动网页浏览器,但你也可以通过 Linux 命令行来完成。如果浏览器尚未运行,请添加一个“&”以使其在后台运行,这样你就可以恢复到 shell 提示符:
$ firefox &
$ google-chrome &
$ opera &
如果已经有一个浏览器在运行,请省略 ampersand 符号。该命令会告诉现有的浏览器实例打开一个新窗口或选项卡。命令会立即退出并返回 shell 提示符。
提示
后台运行的浏览器命令可能会输出诊断消息并混乱你的 shell 窗口。为了防止这种情况发生,在首次启动浏览器时将所有输出重定向到 /dev/null。例如:
$ firefox &> /dev/null &
要从命令行打开浏览器并访问 URL,请将 URL 作为参数提供:
$ firefox https://oreilly.com
$ google-chrome https://oreilly.com
$ opera https://oreilly.com
默认情况下,上述命令会打开一个新选项卡并将其置于焦点。要强制它们打开一个新窗口,请添加一个选项:
$ firefox --new-window https://oreilly.com
$ google-chrome --new-window https://oreilly.com
$ opera --new-window https://oreilly.com
要打开私密或隐身浏览器窗口,请添加相应的命令行选项:
$ firefox --private-window https://oreilly.com
$ google-chrome --incognito https://oreilly.com
$ opera --private https://oreilly.com
上述命令可能会让你觉得输入和努力都很多,但你可以通过为经常访问的网站定义别名来提高效率:
# Place in a shell configuration file and source it:
alias oreilly="firefox --new-window https://oreilly.com"
同样地,如果有一个包含感兴趣 URL 的文件,请使用 grep、cut 或其他 Linux 命令提取 URL,并将其传递给命令行上的浏览器。这里有一个带有两列的制表符分隔文件的示例:
$ cat urls.txt
duckduckgo.com My search engine
nytimes.com My newspaper
spotify.com My music
$ grep music urls.txt | cut -f1
spotify.com
$ google-chrome https://$(grep music urls.txt | cut -f1) *Visit spotify*
或者,假设你通过一个包含跟踪号的文件来跟踪你正在等待的包裹:
$ cat packages.txt
1Z0EW7360669374701 UPS Shoes
568733462924 FedEx Kitchen blender
9305510823011761842873 USPS Care package from Mom
示例 10-1 中的 shell 脚本打开适当承运商(UPS、FedEx 或美国邮政服务)的追踪页面,并将追踪号追加到相应的 URL 中。
示例 10-1. track-it 脚本,访问承运商的追踪页面
#!/bin/bash
PROGRAM=$(basename $0)
DATAFILE=packages.txt
# Choose a browser command: firefox, opera, google-chrome
BROWSER="opera"
errors=0
cat "$DATAFILE" | while read line; do
track=$(echo "$line" | awk '{print $1}')
service=$(echo "$line" | awk '{print $2}')
case "$service" in
UPS)
$BROWSER "https://www.ups.com/track?tracknum=$track" &
;;
FedEx)
$BROWSER "https://www.fedex.com/fedextrack/?trknbr=$track" &
;;
USPS)
$BROWSER "https://tools.usps.com/go/TrackConfirmAction?tLabels=$track" &
;;
*)
>&2 echo "$PROGRAM: Unknown service '$service'"
errors=1
;;
esac
done
exit $errors
使用 curl 和 wget 检索 HTML 页面
Web 浏览器不是唯一可以访问网站的 Linux 程序。程序 curl 和 wget 可以通过单个命令下载网页和其他网络内容,而不需使用浏览器。默认情况下,curl 将其输出打印到标准输出,而 wget 则将其输出保存到文件中(之前会输出大量诊断消息):
$ curl https://efficientlinux.com/welcome.html
Welcome to Efficient Linux.com!
$ wget https://efficientlinux.com/welcome.html
--2021-10-27 20:05:47-- https://efficientlinux.com/
Resolving efficientlinux.com (efficientlinux.com)...
Connecting to efficientlinux.com (efficientlinux.com)...
⋮
2021-10-27 20:05:47 (12.8 MB/s) - ‘welcome.html’ saved [32/32]
$ cat welcome.html
Welcome to Efficient Linux.com!
提示
有些网站不支持 wget 和 curl 下载。在这种情况下,这两个命令可以伪装成另一个浏览器。只需告诉每个程序更改其用户代理(标识向 web 服务器表明其身份的字符串)。一个方便的用户代理是“Mozilla”:
$ wget -U Mozilla *url*
$ curl -A Mozilla *url*
wget 和 curl 都有大量的选项和功能,你可以在它们的 man 手册中了解到。现在,让我们看看如何将这些命令整合到简洁的单行命令中。假设网站 efficientlinux.com 有一个包含文件 1.jpg 到 20.jpg 的目录 images,你想下载它们。它们的 URL 是:
https://efficientlinux.com/images/1.jpg
https://efficientlinux.com/images/2.jpg
https://efficientlinux.com/images/3.jpg
⋮
一个低效的方法是逐个在 web 浏览器中访问每个 URL 并下载每个图像。(如果你曾这样做,请举手!)更好的方法是使用 wget。使用 seq 和 awk 生成 URL:
$ seq 1 20 | awk '{print "https://efficientlinux.com/images/" $1 ".jpg"}'
https://efficientlinux.com/images/1.jpg
https://efficientlinux.com/images/2.jpg
https://efficientlinux.com/images/3.jpg
⋮
然后将字符串“wget”添加到 awk 程序中,并将生成的命令通过管道传递给 bash 执行:
$ seq 1 20 \
| awk '{print "wget https://efficientlinux.com/images/" $1 ".jpg"}' \
| bash
或者,使用 xargs 创建和执行 wget 命令:
$ seq 1 20 | xargs -I@ wget https://efficientlinux.com/images/@.jpg
如果您的wget命令包含任何特殊字符,则xargs解决方案更好。而“管道到 bash”解决方案会导致 shell 评估这些字符(这是您不希望发生的),而xargs则不会。
我的示例有点牵强,因为图像文件名如此统一。在一个更现实的示例中,您可以通过使用curl检索页面,通过一系列巧妙的命令来隔离图像 URL(每行一个),然后应用我刚刚向您展示的技术之一来下载网页上的所有图像:
curl *URL* | *...clever pipeline here...* | xargs -n1 wget
使用 HTML-XML-utils 处理 HTML
如果您了解一些 HTML 和 CSS,您可以从命令行解析网页的 HTML 源代码。有时这比手动从浏览器窗口复制和粘贴网页的片段更有效。用于此目的的一套方便的工具是 HTML-XML-utils,在许多 Linux 发行版和万维网联盟上都可以找到。一个通用的步骤是:
-
使用
curl(或wget)捕获 HTML 源代码。 -
使用
hxnormalize来帮助确保 HTML 格式正确。 -
识别要捕获的值的 CSS 选择器。
-
使用
hxselect来隔离这些值,并将输出导向进一步处理的命令。
让我们扩展“构建区号数据库”的示例,从网上获取区号数据并生成在该示例中使用的areacodes.txt文件。为了方便起见,我已经为您创建了一个区号的 HTML 表格供您下载和处理,如图 10-1 所示。

图 10-1. 一个区号表格,位于https://efficientlinux.com/areacodes.html
首先,使用curl获取 HTML 源代码,使用-s选项来抑制屏幕消息。将输出导向hxnormalize -x以稍微清理一下。将其导向less以逐屏查看输出:
$ curl -s https://efficientlinux.com/areacodes.html \
| hxnormalize -x \
| less
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
⋮
<body>
<h1>Area code test</h1>
⋮
该页面上的 HTML 表格,如示例 10-2 所示,具有 CSS ID #ac,其三列(区号、州和位置)分别使用 CSS 类ac、state和cities。
示例 10-2. 图 10-1 中表格的部分 HTML 源代码
<table id="ac">
<thead>
<tr>
<th>Area code</th>
<th>State</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ac">201</td>
<td class="state">NJ</td>
<td class="cities">Hackensack, Jersey City</td>
</tr>
⋮
</tbody>
</table>
运行hxselect来从每个表格单元格中提取区号数据,提供-c选项以省略输出中的td标签。将结果打印为一行,用您选择的字符分隔字段(使用-s选项)。^(2) 我选择字符@是因为在页面上易于看到:
$ curl -s https://efficientlinux.com/areacodes.html \
| hxnormalize -x \
| hxselect -c -s@ '#ac .ac, #ac .state, #ac .cities'
201@NJ@Hackensack, Jersey City@202@DC@Washington@203@CT@New Haven, Stamford@...
最后,将输出导向sed,将这一长行转换为三个以制表符分隔的列。编写一个正则表达式来匹配以下字符串:
-
区号,由数字组成,
[0-9]* -
一个
@符号 -
州缩写,即两个大写字母,
[A-Z][A-Z] -
一个
@符号 -
城市,即不包含
@符号的任何文本,[^@]* -
一个
@符号
组合部分以生成以下正则表达式:
[0-9]*@[A-Z][A-Z]@[^@]*@
通过将区号、州和城市括在\(和\) 中,捕获三个子表达式。您现在拥有了sed的完整正则表达式:
\([0-9]*\)@\([A-Z][A-Z]\)@\([^@]*\)@
对于sed的替换字符串,提供由制表符分隔并以换行符终止的三个子表达式,这会生成areacodes.txt文件的格式:
\1\t\2\t\3\n
将前述正则表达式和替换字符串组合,以制作此 sed 脚本:
s/\([0-9]*\)@\([A-Z][A-Z]\)@\([^@]*\)@/\1\t\2\t\3\n/g
完成的命令会生成areacodes.txt文件所需的数据:
$ curl -s https://efficientlinux.com/areacodes.html \
| hxnormalize -x \
| hxselect -c -s'@' '#ac .ac, #ac .state, #ac .cities' \
| sed 's/\([0-9]*\)@\([A-Z][A-Z]\)@\([^@]*\)@/\1\t\2\t\3\n/g'
201 NJ Hackensack, Jersey City
202 DC Washington
203 CT New Haven, Stamford
⋮
使用基于文本的浏览器检索渲染的 Web 内容
有时,当您在命令行检索网页数据时,您可能不希望获取网页的 HTML 源码,而是希望获取页面的文本渲染版本。渲染后的文本可能更容易解析。要完成此任务,请使用文本浏览器如lynx或links。文本浏览器以简化格式显示网页,没有图像或其他花哨功能。图 10-2 显示了由lynx渲染的前一节中的区号页面。

图 10-2. lynx渲染页面 https://efficientlinux.com/areacodes.html
无论您更喜欢哪个程序,lynx和links都使用-dump选项下载渲染后的页面。
$ lynx -dump https://efficientlinux.com/areacodes.html > tempfile
$ cat tempfile
Area code test
Area code State Location
201 NJ Hackensack, Jersey City
202 DC Washington
203 CT New Haven, Stamford
⋮
提示
lynx和links也非常适合检查看起来可疑的链接,以确定它们是否合法或恶意。这些文本浏览器不支持 JavaScript 或渲染图像,因此它们不太容易受到攻击。(当然,它们无法完全保证安全性,所以请慎重使用。)
命令行剪贴板控制
每个带有编辑菜单的现代软件应用程序都包括剪切、复制和粘贴操作,以在系统剪贴板中传输内容。您可能还知道这些操作的键盘快捷键。但您是否知道您可以直接从命令行处理剪贴板?
首先稍微背景:Linux 上的复制和粘贴操作是称为X 选择的更一般机制的一部分。选择是复制内容的目的地,如系统剪贴板。 “X”只是 Linux 窗口软件的名称。
大多数基于 X 构建的 Linux 桌面环境,如 GNOME、Unity、Cinnamon 和 KDE Plasma,支持两种选择。^(3) 第一种是剪贴板,它的工作方式与其他操作系统上的剪贴板相似。当您在应用程序中执行剪切或复制操作时,内容将存储在剪贴板中,并且您可以通过粘贴操作检索内容。一个较少见的 X 选择称为主要选择。在某些应用程序中选择文本时,即使您没有运行复制操作,它也会被写入主要选择。例如,在终端窗口中用鼠标突出显示文本。该文本会自动写入主要选择。
注意
如果通过 SSH 或类似程序远程连接到 Linux 主机,则通常由本地计算机处理复制/粘贴,而不是远程 Linux 主机上的 X 选择。
表格 10-2 列出了在 GNOME 终端 (gnome-terminal) 和 KDE 的 Konsole (konsole) 中访问 X 选择的鼠标和键盘操作。如果您使用不同的终端程序,请检查其编辑菜单以获取复制和粘贴的键盘等效操作。
表 10-2. 在常见终端程序中访问 X 选择
| 操作 | 剪贴板 | 主要选择 |
|---|---|---|
| 复制(鼠标) | 打开右键菜单并选择复制 | 点击并拖动;或双击选择当前单词;或三次单击选择当前行 |
| 粘贴(鼠标) | 打开右键菜单并选择粘贴 | 按下中间鼠标按钮(通常是滚轮) |
| 复制(键盘) | Ctrl-Shift-C | n/a |
粘贴(键盘),gnome-terminal |
Ctrl-Shift-V 或 Ctrl-Shift-Insert | Shift-Insert |
粘贴(键盘),konsole |
Ctrl-Shift-V 或 Shift-Insert | Ctrl-Shift-Insert |
将选择连接到标准输入和输出
Linux 提供了一个命令,xclip,它将 X 选择连接到标准输入和输出。因此,您可以将复制和粘贴操作插入到管道和其他组合命令中。例如,您可以像这样将文本复制到应用程序中:
-
运行 Linux 命令并将其输出重定向到文件。
-
查看文件。
-
使用鼠标将文件内容复制到剪贴板。
-
将内容粘贴到另一个应用程序中。
使用 xclip,您可以大大简化这个过程:
-
将 Linux 命令的输出管道到
xclip。 -
将内容粘贴到另一个应用程序中。
相反,您可能已经将文本粘贴到文件中,以便使用 Linux 命令处理它,例如:
-
使用鼠标在应用程序中复制一大段文本。
-
将其粘贴到文本文件中。
-
使用 Linux 命令处理文本文件。
使用 xclip -o,您可以跳过中间文本文件:
-
使用鼠标在应用程序中复制一大段文本。
-
将
xclip -o的输出管道到其他 Linux 命令以进行处理。
警告
如果你在 Linux 设备上以数字方式阅读本书,并且想尝试本节中的一些xclip命令,请不要将命令复制并粘贴到 shell 窗口中。手动输入命令。为什么?因为你的复制操作可能会覆盖xclip访问的同一 X 选择,导致命令产生意外结果。
默认情况下,xclip从标准输入读取并写入主选择。它可以从文件中读取:
$ xclip < myfile.txt
或者来自管道:
$ echo "Efficient Linux at the Command Line" | xclip
现在将文本打印到标准输出,或将选择内容管道化到其他命令,例如wc:
$ xclip -o *Paste to stdout*
Efficient Linux at the Command Line
$ xclip -o > anotherfile.txt *Paste to a file*
$ xclip -o | wc -w *Count words*
6
任何将结果写入标准输出的组合命令都可以将其结果通过xclip管道化,就像来自“命令 #6: uniq”的这个例子:
$ cut -f1 grades | sort | uniq -c | sort -nr | head -n1 | cut -c9 | xclip
使用echo -n将主选择清空设置为空字符串:
$ echo -n | xclip
-n选项很重要;否则,echo会在标准输出中打印一个换行符,最终会出现在主选择中。
要将文本复制到剪贴板而不是主选择中,请使用xclip选项-selection clipboard运行:
$ echo https://oreilly.com | xclip -selection clipboard *Copy*
$ xclip -selection clipboard -o *Paste*
https://oreilly.com
xclip选项可以缩写,只要它们不引起歧义:
$ xclip -sel c -o *Same as xclip -selection clipboard -o*
https://oreilly.com
启动 Firefox 浏览器窗口访问前述 URL,使用命令替换:
$ firefox $(xclip -selection clipboard -o)
Linux 提供另一种命令xsel,它也可以读取和写入 X 选择。它还有一些额外的功能,如清除选择(xsel -c)和追加到选择(xsel -a)。请随意阅读 man 页并尝试使用xsel。
改进密码管理器
让我们利用你对xclip的新知识,将 X 选择集成到密码管理器pman中,来自“构建密码管理器”。修改后的pman脚本匹配vault.gpg文件中的单行,将用户名写入剪贴板,将密码写入主选择。之后,你可以通过 Ctrl-V 粘贴用户名,通过中键粘贴密码来填写网页上的登录页。
警告
确保你没有运行剪贴板管理器或其他跟踪 X 选择及其内容的应用程序。否则,用户名和/或密码会在剪贴板管理器中可见,这是一个安全风险。
新版本的pman在示例 10-3 中。pman的行为已以下列方式发生了变化:
-
新功能
load_password加载相关的用户名和密码到 X 选择中。 -
如果
pman找到一个与搜索字符串匹配的单个结果,无论是按键(字段 3)还是按行中的任何其他部分,它都会运行load_password。 -
如果
pman找到多个匹配项,它会打印所有匹配行中的键和注释(字段 3 和字段 4),以便用户可以再次按键搜索。
示例 10-3。一个改进的pman脚本,将用户名和密码加载为选择项
#!/bin/bash
PROGRAM=$(basename $0)
DATABASE=$HOME/etc/vault.gpg
load_password () {
# Place username (field 1) into clipboard
echo "$1" | cut -f1 | tr -d '\n' | xclip -selection clipboard
# Place password (field 2) into X primary selection
echo "$1" | cut -f2 | tr -d '\n' | xclip -selection primary
# Give feedback to the user
echo "$PROGRAM: Found" $(echo "$1" | cut -f3- --output-delimiter ': ')
echo "$PROGRAM: username and password loaded into X selections"
}
if [ $# -ne 1 ]; then
>&2 echo "$PROGRAM: look up passwords"
>&2 echo "Usage: $PROGRAM string"
exit 1
fi
searchstring="$1"
# Store the decrypted text in a variable
decrypted=$(gpg -d -q "$DATABASE")
if [ $? -ne 0 ]; then
>&2 echo "$PROGRAM: could not decrypt $DATABASE"
exit 1
fi
# Look for exact matches in the third column
match=$(echo "$decrypted" | awk '$3~/^'$searchstring'$/')
if [ -n "$match" ]; then
load_password "$match"
exit $?
fi
# Look for any match
match=$(echo "$decrypted" | awk "/$searchstring/")
if [ -z "$match" ]; then
>&2 echo "$PROGRAM: no matches"
exit 1
fi
# Count the matches
count=$(echo "$match" | wc -l)
case "$count" in
0)
>&2 echo "$PROGRAM: no matches"
exit 1
;;
1)
load_password "$match"
exit $?
;;
*)
>&2 echo "$PROGRAM: multiple matches for the following keys:"
echo "$match" | cut -f3
>&2 echo "$PROGRAM: rerun this script with one of the keys"
exit
;;
esac
运行脚本:
$ pman dropbox
Passphrase: xxxxxxxx
pman: Found dropbox: dropbox.com account for work
pman: username and password loaded into X selections
$ pman account
Passphrase: xxxxxxxx
pman: multiple matches for the following keys:
google
dropbox
bank
dropbox2
pman: rerun this script with one of the keys
密码会一直保存在主要选择中,直到它被覆盖。要在(比如)30 秒后自动清除密码,请在load_password函数的末尾添加以下行。该行在后台启动一个子 shell,等待 30 秒然后清除主要选择(设置为空字符串)。根据需要调整数字 30。
(sleep 30 && echo -n | xclip -selection primary) &
如果你已经定义了一个自定义的键盘快捷方式来启动终端窗口在“即时外壳和浏览器”,现在你有一个快速访问密码的方法。通过热键弹出一个终端,运行pman,然后关闭终端。
总结
希望本章节鼓励你尝试一些新技巧,使你能保持双手在键盘上。起初可能需要付出努力,但随着练习,它们会变得快速和自动化。很快你会成为 Linux 朋友羡慕的对象,因为你能够顺利地操作桌面窗口、网络内容和 X 选择,而这是鼠标绑定的群众所不能及的。
^(1) 除非你在捕获所有按键的应用程序中工作,比如一个窗口中的虚拟机。
^(2) 这个例子使用了三个 CSS 选择器,但是一些旧版本的hxselect只能处理两个。如果你的版本受到这个缺陷的影响,从万维网联盟下载最新版本,并使用命令configure && make进行构建。
^(3) 实际上有三个 X 选择,但其中一个称为次要选择,在现代桌面环境中很少暴露。
第十一章:最终节省时间
写这本书非常有趣,希望你们读起来也很愉快。在最后一个章节中,让我们涵盖一些前几章中没有完全适合的小主题。这些主题让我成为一个更好的 Linux 用户,也许它们也会帮助到你。
快速胜利
以下时间节省方法,几分钟内就能轻松掌握。
从less跳转到您的编辑器
当你使用less查看文本文件并想要编辑文件时,不要退出less。只需按v键启动你喜欢的文本编辑器。它会加载文件并将光标放在你在less中查看的位置。退出编辑器后,你将回到原来在less中的位置。
要使此技巧发挥最佳效果,请将环境变量EDITOR和/或VISUAL设置为编辑命令。这些环境变量代表你的默认 Linux 文本编辑器,可以通过各种命令启动,包括less、lynx、git、crontab和众多电子邮件程序。例如,要将emacs设置为默认编辑器,请在 shell 配置文件中添加以下任一行(或两行),然后执行:
VISUAL=emacs
EDITOR=emacs
如果你没有设置这些变量,你的默认编辑器将是你的 Linux 系统通常设置的vim。如果你进入vim而不知道如何使用它,请不要惊慌。按下 Escape 键,输入:q!(冒号、字母q和感叹号),然后按 Enter 键退出vim。要退出emacs,按 Ctrl-X,然后按 Ctrl-C。
编辑包含特定字符串的文件
想要编辑当前目录中包含特定字符串(或正则表达式)的每个文件?使用grep -l生成文件名列表,并通过命令替换将它们传递给你的编辑器。假设你的编辑器是vim,命令如下:
$ vim $(grep -l *string* *)
通过将-r选项(递归)添加到grep并从当前目录(点)开始,编辑所有包含string的文件:
$ vim $(grep -lr *string* .)
对于大型目录树的快速搜索,使用find和xargs而不是grep -r:
$ vim $(find . -type f -print0 | xargs -0 grep -l *string*)
“技巧#3:命令替换”提到了这种技术,但我想强调一下,因为它非常有用。记得要注意文件名中包含空格和其他特殊字符,因为它们可能会影响到结果,就像“特殊字符和命令替换”中解释的那样。
接受拼写错误
如果你经常拼错命令,请为你最常见的错误定义别名,以便正确的命令仍然运行:
alias firfox=firefox
alias les=less
alias meacs=emacs
注意不要意外地通过定义具有相同名称的别名来覆盖现有的 Linux 命令。首先使用which或type命令搜索你提议的别名,然后运行man命令,确保没有其他同名的命令:
$ type firfox
bash: type: firfox: not found
$ man firfox
No manual entry for firfox
快速创建空文件
在 Linux 中有几种创建空文件的方法。touch命令用于更新文件的时间戳,如果文件不存在,则也会创建文件:
$ touch newfile1
touch非常适合用于创建大量空文件进行测试:
$ mkdir tmp *Create a directory*
$ cd tmp
$ touch file{0000..9999}.txt *Create 10,000 files*
$ cd ..
$ rm -rf tmp *Remove the directory and files*
echo命令如果将其输出重定向到文件,则会创建一个空文件,但仅当提供-n选项时:
$ echo -n > newfile2
如果忘记了-n选项,则生成的文件包含一个字符,即换行符,因此不是空文件。
逐行处理文件
当您需要逐行处理文件时,将文件cat到while read循环中:
$ cat myfile | while read line; do
*...do something here...*
done
例如,要计算文件每行的长度,例如/etc/hosts,将每行管道传递给wc -c:
$ cat /etc/hosts | while read line; do
echo "$line" | wc -c
done
65
31
1
⋮
此技术的一个更实际的示例见示例 9-3。
辨识支持递归的命令
在“find 命令”中,我介绍了find -exec,它可以递归地对整个目录树应用任何 Linux 命令:
$ find . -exec *your command here* \;
还有其他一些命令本身支持递归,如果您知道它们,可以节省时间,而不是构建一个find命令来实现递归。
ls -R
递归列出目录及其内容
cp -r或cp -a
递归复制目录及其内容
rm -r
递归删除目录及其内容
grep -r
通过正则表达式在整个目录树中搜索
chmod -R
递归更改文件保护
chown -R
递归更改文件所有权
chgrp -R
递归更改文件组所有权
阅读一个手册页
选择一个常用命令,如cut或grep,并彻底阅读其手册页。您可能会发现一两个从未使用过但很有价值的选项。定期重复此活动可完善并扩展您的 Linux 工具箱。
长期学习
下面的技术需要真正的努力学习,但您将在节省时间方面得到回报。我提供了每个主题的一点点味道,不是为了教授详细内容,而是为了激励您自己去发现更多。
阅读 bash 手册页
运行man bash以显示bash的完整官方文档,并阅读所有内容——是的,全部 46318 字:
$ man bash | wc -w
46318
花几天时间,慢慢来。您肯定会学到很多,使您的日常 Linux 使用更加轻松。
学习 cron、crontab 和 at
在“第一个示例:查找文件”中,有一段简短的关于如何安排命令在未来定期自动运行的说明。我建议学习crontab程序为自己设置定期命令。例如,您可以按计划备份文件到外部驱动器,或通过电子邮件为月度事件发送提醒。
在运行crontab之前,请按照“从 less 跳转到编辑器”中所示的方式定义您的默认编辑器。然后运行crontab -e来编辑您的个人定时命令文件。crontab会启动您的默认编辑器并打开一个空文件来指定命令。该文件称为您的crontab。
简而言之,在 crontab 文件中的计划命令,通常称为cron 作业,由六个字段组成,全部位于单个(可能很长的)行上。前五个字段确定作业的调度时间,依次为分钟、小时、每月的日期、月份和星期几。第六个字段是要运行的 Linux 命令。您可以按小时、每天、每周、每月、每年的某些特定日期或时间或其他更复杂的安排来启动命令。一些示例包括:
* * * * * *command* *Run command every minute*
30 7 * * * *command* *Run command at 07:30 every day*
30 7 5 * * *command* *Run command at 07:30 the 5th day of every month*
30 7 5 1 * *command* *Run command at 07:30 every January 5*
30 7 * * 1 *command* *Run command at 07:30 every Monday*
一旦创建了所有六个字段,保存了文件并退出了编辑器,该命令会根据您定义的时间表自动启动(由一个称为cron的程序执行)。计划的语法短而难懂,但在 man 页面(man 5 crontab)和许多在线教程(搜索cron 教程)中都有详细说明。
我还建议学习at命令,该命令可安排命令在指定的日期和时间运行一次,而不是重复运行。运行man at获取详细信息。以下是一个命令,它会在明天晚上 10 点向您发送一封电子邮件提醒刷牙:
$ at 22:00 tomorrow
warning: commands will be executed using /bin/sh
at> echo brush your teeth | mail $USER
at> ^D *Type Ctrl-D to end input*
job 699 at Sun Nov 14 22:00:00 2021
要列出您待定的at作业,请运行atq:
$ atq
699 Sun Nov 14 22:00:00 20211 a smith
要查看at作业中的命令,请使用作业号运行at -c,并打印最后几行:
$ at -c 699 | tail
⋮
echo brush your teeth | mail $USER
在执行之前移除待定作业,请使用作业号运行atrm:
$ atrm 699
学习 rsync
要从一个磁盘位置复制完整目录及其子目录到另一个位置,许多 Linux 用户会使用命令cp -r或cp -a:
$ cp -a dir1 dir2
cp第一次可以完成工作,但如果稍后在目录dir1中修改了几个文件并再次执行复制,cp会浪费资源。它会忠实地再次复制dir1中的所有文件和目录,即使在dir2中已经存在完全相同的副本。
命令rsync是一个更智能的复制程序。它只复制第一个和第二个目录之间的差异。
$ rsync -a dir1/ dir2
注意
前述命令中的斜杠表示复制dir1内的文件。如果没有斜杠,rsync会复制dir1本身,从而创建dir2/dir1。
如果稍后向目录dir1添加一个文件,rsync只会复制那一个文件。如果在dir1中的文件内修改一行,rsync只会复制那一行!在多次复制大型目录树时,这可以节省大量时间。rsync甚至可以通过 SSH 连接复制到远程服务器。
rsync有几十个选项。以下是一些特别有用的选项:
-v(表示“详细模式”)
在文件被复制时打印文件名
-n
假装复制;结合-v以查看将要被复制的文件
-x
告诉rsync不要跨越文件系统边界
我强烈推荐熟悉rsync以进行更高效的复制。阅读 man 页并查看 Korbin Brown 的文章“Rsync Examples in Linux”中的示例。
学习另一种脚本语言
Shell 脚本方便且功能强大,但也有一些严重的缺陷。例如,它们无法处理文件名中包含空白字符的情况。考虑这个试图删除文件的简短bash脚本:
#!/bin/bash
BOOKTITLE="Slow Inefficient Linux"
rm $BOOKTITLE # Wrong! Don't do this!
第二行看起来是在删除一个名为Slow Inefficient Linux的文件,但实际上并不是。它尝试删除三个名为Slow、Inefficient和Linux的文件。在调用rm之前,shell 会展开变量$BOOKTITLE,其展开结果是由空白分隔的三个单词,就像你输入了以下内容一样:
rm Slow Efficient Linux
然后 shell 使用三个参数调用rm,结果可能是错误的文件被删除了。正确的删除命令应该用双引号括起$BOOKTITLE:
rm "$BOOKTITLE"
shell 会将其展开为:
rm "Slow Efficient Linux"
这种微妙且潜在破坏性的怪癖只是表明了 shell 脚本在严肃项目中的不适用性之一。因此,我建议学习第二种脚本语言,如 Perl、PHP、Python 或 Ruby。它们都能正确处理空白字符。它们都支持真实的数据结构。它们都拥有强大的字符串处理函数。它们都能轻松进行数学计算。其优势不胜枚举。
使用 shell 启动复杂命令和创建简单脚本,但对于更重要的任务,请转向另一种语言。尝试在线的许多语言教程之一。
用于非编程任务的 make
程序make会根据规则自动更新文件。它设计用于加快软件开发,但稍加努力,make也可以简化 Linux 生活的其他方面。
假设您有三个文件分别命名为chapter1.txt、chapter2.txt和chapter3.txt,分开处理。还有第四个文件book.txt,它是这三个章节文件的组合。每当章节发生变化时,您需要重新组合它们并更新book.txt,可能会使用如下命令:
$ cat chapter1.txt chapter2.txt chapter3.txt > book.txt
这种情况非常适合使用make。
-
一堆文件
-
规则涉及文件,即book.txt在任何章节文件更改时都需要更新
-
执行更新的命令
make通过读取一个配置文件(通常命名为Makefile),该文件中充满了规则和命令来操作。例如,以下Makefile规则说明book.txt依赖于三个章节文件:
book.txt: chapter1.txt chapter2.txt chapter3.txt
如果规则的目标(在本例中为book.txt)比其任何依赖项(章节文件)都要旧,则make认为目标已过期。如果在规则后的行上提供了命令,make将运行该命令以更新目标:
book.txt: chapter1.txt chapter2.txt chapter3.txt
cat chapter1.txt chapter2.txt chapter3.txt > book.txt
要应用规则,只需运行命令make:
$ ls
Makefile chapter1.txt chapter2.txt chapter3.txt
$ make
cat chapter1.txt chapter2.txt chapter3.txt > book.txt *Executed by make*
$ ls
Makefile book.txt chapter1.txt chapter2.txt chapter3.txt
$ make
make: 'book.txt' is up to date.
$ vim chapter2.txt *Update a chapter*
$ make
cat chapter1.txt chapter2.txt chapter3.txt > book.txt
make是为程序员开发的,但是通过一点学习,您可以将其用于非编程任务。每当您需要更新依赖其他文件的文件时,编写一个Makefile通常可以简化您的工作。
make帮助我编写和调试这本书。我用一种称为 AsciiDoc 的排版语言写作,并定期将章节转换为 HTML 以在浏览器中查看。以下是一个make规则,将任何 AsciiDoc 文件转换为 HTML 文件:
%.html: %.asciidoc
asciidoctor -o $@ $<
它的意思是:要创建一个扩展名为.html的文件(%.html),请查找扩展名为.asciidoc的相应文件(%.asciidoc)。如果 HTML 文件比 AsciiDoc 文件旧,通过在依赖文件($<)上运行asciidoctor命令并将输出发送到目标 HTML 文件(-o $@)来重新生成 HTML 文件。有了这个略微神秘但简短的规则,我只需输入一个简单的make命令,就可以创建您现在正在阅读的章节的 HTML 版本。make启动asciidoctor来执行更新:
$ ls ch11*
ch11.asciidoc
$ make ch11.html
asciidoctor -o ch11.html ch11.asciidoc
$ ls ch11*
ch11.asciidoc ch11.html
$ firefox ch11.html *View the HTML file*
对于小任务,学习make通常不到一个小时就可以掌握基本技能。这是值得的。有一个有用的指南在makefiletutorial.com上。
将版本控制应用于日常文件
您是否曾经想过编辑一个文件,但又担心您的更改可能会弄乱它?也许您制作了一个备份副本进行保管,并编辑了原始副本,知道如果出错可以恢复备份:
$ cp myfile myfile.bak
这种解决方案不具有可扩展性。如果您有几十个甚至几百个文件以及数十个甚至数百个人在处理它们,会怎么样?像 Git 和 Subversion 这样的版本控制系统通常可以解决这一问题,方便地跟踪多个版本的文件。
Git 在维护软件源代码中非常广泛,但我建议无论是个人文件还是操作系统文件,都学习并使用它,因为您的更改很重要。《“随环境旅行”》建议使用版本控制来维护您的bash配置文件。
在写这本书时,我使用了 Git,这样可以尝试不同的呈现材料的方式。几乎不费力气,我创建并维护了书的三个不同版本:一个是迄今为止的完整手稿,一个只包含我提交给编辑审查的章节,另一个用于实验性工作,尝试新的想法。如果我不喜欢自己写的东西,一个简单的命令就可以恢复以前的版本。
教授 Git 超出了本书的范围,但这里有一些示例命令,展示基本的工作流程并激发您的兴趣。将当前目录(及其所有子目录)转换为 Git 存储库:
$ git init
编辑一些文件。之后,将更改的文件添加到一个不可见的“暂存区”,这一操作声明了您创建新版本的意图:
$ git add .
创建新版本时,请提供注释以描述您对文件的更改:
$ git commit -m"Changed X to Y"
查看您的版本历史:
$ git log
在此过程中还有更多内容,比如检索旧版本的文件和将版本保存(推送)到另一个服务器上。获取一个git教程,然后开始吧!
再见
非常感谢你通过这本书与我同行。我希望它能实现我在前言中对提升你的 Linux 命令行技能的承诺。请告诉我你在 dbarrett@oreilly.com 的体验。祝你计算愉快。
附录 A. Linux 复习
如果你的 Linux 技能有些生疏,这里是一些你需要了解的细节(如果你是完全初学者,这篇回顾可能太简略了。请查阅结尾处的推荐阅读)。
命令、参数和选项
要在命令行中运行 Linux 命令,请输入命令并按 Enter 键。要终止正在进行的命令,请按 Ctrl-C。
简单的 Linux 命令由一个单词组成,通常是程序的名称,后跟称为参数的其他字符串。例如,以下命令由程序名 ls 和两个参数组成:
$ ls -l /bin
以破折号开头的参数,如-l,被称为选项,因为它们改变命令的行为。其他参数可能是文件名、目录名、用户名、主机名或程序所需的任何其他字符串。选项通常(但并非总是)位于其余参数之前。
命令选项以各种形式出现,取决于你运行的程序:
-
单个字母,如
-l,有时跟随一个值,如-n 10。通常可以省略字母和值之间的空格:-n10。 -
由两个破折号前导的单词,例如
--long,有时跟随一个值,如--block-size 100。选项与其值之间的空格通常可以用等号替代:--block-size=100。 -
一个由一个破折号前导的单词,例如
-type,可选择跟随一个值,如-type f。这种选项格式很少见;一个使用它的命令是find。 -
没有破折号的单个字母。这种选项格式很少见;一个使用它的命令是
tar。
多个选项经常(根据命令而定)可以在单个破折号后组合。例如,命令 ls -al 等效于 ls -a -l。
选项不仅在外观上不同,而且在含义上也不同。在命令 ls -l 中,-l 表示“长输出”,但在命令 wc -l 中,它表示“文本行数”。两个程序也可能使用不同的选项表示相同的事物,例如 -q 表示“安静运行”,而 -s 表示“静默运行”。这些不一致性使 Linux 学习起来更加困难,但最终你会习惯的。
文件系统、目录和路径
Linux 文件存储在目录(文件夹)中,这些目录按树形结构组织,例如图 A-1 中的结构。树从一个名为根的目录开始,用单个斜线(/)表示,其中可能包含文件和其他目录,称为子目录。例如,目录Music 包含两个子目录,mp3 和 SheetMusic。我们称Music 为 mp3 和 SheetMusic 的父目录。具有相同父目录的目录称为同级目录。
树中的路径写成一系列由斜杠分隔的目录名,例如/home/smith/Music/mp3。路径也可能以文件名结尾,例如/home/smith/Music/mp3/catalog.txt。这些路径称为绝对路径,因为它们从根目录开始。如果你的当前目录是/home/smith/Music,那么一些相对路径包括mp3(一个子目录)和mp3/catalog.txt(一个文件)。甚至像catalog.txt这样的文件名本身也是相对于/home/smith/Music/mp3的相对路径。
两个特殊的相对路径是单个点(.),表示当前目录,以及连续的两个点(..),表示当前目录的父目录。^1 这两者可以成为更大路径的一部分。例如,如果你的当前目录是/home/smith/Music/mp3,那么路径..指向Music,路径../../../..指向根目录,路径../SheetMusic指向mp3的一个同级目录。

图 A-1. Linux 目录树中的路径
在 Linux 系统上,你和其他每位用户都有一个指定的目录,称为主目录,你可以自由地在其中创建、编辑和删除文件和目录。其路径通常是/home/加上你的用户名,例如/home/smith。
目录移动
在任何时刻,你的命令行(shell)都在一个特定的目录中运行,称为你的当前目录、工作目录或当前工作目录。使用pwd(打印当前工作目录)命令查看当前目录的路径:
$ pwd
/home/smith *The home directory of user smith*
使用cd(更改目录)命令移动目录,提供路径(绝对或相对)到你想要的目的地:
$ cd /usr/local *Absolute path*
$ cd bin *Relative path leading to /usr/local/bin*
$ cd ../etc *Relative path leading to /usr/local/etc*
创建和编辑文件
使用标准的 Linux 文本编辑器编辑文件,通过运行以下任何命令之一:
emacs
一旦启动 emacs,键入 Ctrl-h 然后按 t 查看教程。
nano
访问nano 编辑器获取文档。
vim或vi
运行命令vimtutor查看教程。
要创建文件,只需将文件名作为参数提供,编辑器将创建它:
$ nano newfile.txt
或者,使用touch命令创建空文件,提供所需的文件名作为参数:
$ touch funky.txt
$ ls
funky.txt
文件和目录处理
使用ls命令列出目录中的文件(默认为当前目录):
$ ls
animals.txt
使用“长”列表(ls -l)查看文件或目录的属性:
$ ls -l
-rw-r--r-- 1 smith smith 325 Jul 3 17:44 animals.txt
从左到右,属性是文件权限(-rw-r—r--),描述在“文件权限”中,所有者(smith)和组(smith),文件大小(325字节),上次修改日期和时间(今年7 月 3 日,17:44),和文件名(animals.txt)。
默认情况下,ls不会打印以点开头的文件名。要列出这些文件(通常称为点文件或隐藏文件),添加-a选项:
$ ls -a
.bashrc .bash_profile animals.txt
使用cp命令复制文件,提供原始文件名和新文件名:
$ cp animals.txt beasts.txt
$ ls
animals.txt beasts.txt
使用mv(移动)命令重命名文件,提供原始文件名和新文件名:
$ mv beasts.txt creatures.txt
$ ls
animals.txt creatures.txt
使用rm(删除)命令删除文件:
$ rm creatures.txt
警告
在 Linux 上删除不友好。rm命令不会问“你确定吗?”,也没有回收站来恢复文件。
使用mkdir创建目录,使用mv重命名它,并使用rmdir(如果为空)删除它:
$ mkdir testdir
$ ls
animals.txt testdir
$ mv testdir newname
$ ls
animals.txt newname
$ rmdir newname
$ ls
animals.txt
将一个或多个文件(或目录)复制到目录中:
$ touch file1 file2 file3
$ mkdir dir
$ ls
dir file1 file2 file3
$ cp file1 file2 file3 dir
$ ls
dir file1 file2 file3
$ ls dir
file1 file2 file3
$ rm file1 file2 file3
继续,将一个或多个文件(或目录)移动到目录中:
$ touch thing1 thing2 thing3
$ ls
dir thing1 thing2 thing3
$ mv thing1 thing2 thing3 dir
$ ls
dir
$ ls dir
file1 file2 file3 thing1 thing2 thing3
使用rm -rf删除目录及其所有内容。在运行此命令之前,请小心,因为此操作不可逆。参见“永远不要再删除错误的文件(多亏了历史扩展)”获取安全提示。
$ rm -rf dir
文件查看
使用cat命令在屏幕上打印文本文件:
$ cat animals.txt
使用less命令逐屏查看文本文件:
$ less animals.txt
在运行less时,通过按空格键显示下一页。要退出less,请按 q。要获取帮助,请按 h。
文件权限
chmod命令将文件设置为你自己、指定用户组或所有人可读、可写和/或可执行。图 A-2 是文件权限的简要复习。

图 A-2. 文件权限位
这里是一些常见的chmod操作。使文件对你可读写,对其他人只可读:
$ chmod 644 animals.txt
$ ls -l
-rw-r--r-- 1 smith smith 325 Jul 3 17:44 animals.txt
保护它免受所有其他用户的影响:
$ chmod 600 animals.txt
$ ls -l
-rw------- 1 smith smith 325 Jul 3 17:44 animals.txt
使一个目录对所有人可读可进入,但只有你可写:
$ mkdir dir
$ chmod 755 dir
$ ls -l
drwxr-xr-x 2 smith smith 4096 Oct 1 12:44 dir
保护一个目录免受所有其他用户的影响:
$ chmod 700 dir
$ ls -l
drwx------ 2 smith smith 4096 Oct 1 12:44 dir
超级用户不受正常权限限制,可以读取和写入系统上的所有文件和目录。
进程
当你运行 Linux 命令时,它会启动一个或多个 Linux 进程,每个进程都有一个称为PID的数字进程 ID。使用ps命令查看你的 shell 的当前进程:
$ ps
PID TTY TIME CMD
5152 pts/11 00:00:00 bash
117280 pts/11 00:00:00 emacs
117273 pts/11 00:00:00 ps
或结束所有用户的所有正在运行的进程:
$ ps -uax
使用kill命令结束你自己的进程,提供 PID 作为参数。超级用户(Linux 管理员)可以结束任何用户的进程。
$ kill 117280
[1]+ Exit 15 emacs animals.txt
查看文档
man 命令打印关于你的 Linux 系统上任何标准命令的文档。只需输入man,后跟命令的名称。例如,要查看cat命令的文档,运行以下命令:
$ man cat
显示的文档称为命令的man 页面。当有人说“查看 grep 的 man 页面”时,他们指的是运行man grep命令。
man使用程序less逐页显示文档,^(2)因此less的标准按键适用。表 A-1 列出了一些常见的按键。
表 A-1. 使用less查看 man 页面的一些按键
| 按键 | 动作 |
|---|---|
| h | 帮助——显示less的按键列表 |
| 空格键 | 查看下一页 |
| b | 查看上一页 |
| Enter | 向下滚动一行 |
| < | 跳转到文档的开头 |
| > | 跳转到文档的末尾 |
| / | 向前搜索文本(输入文本并按 Enter 键) |
| ? | 向后搜索文本(输入文本并按 Enter 键) |
| n | 定位搜索文本的下一个出现位置 |
| q | 退出 man 帮助页面 |
Shell 脚本
要作为一个单元运行一组 Linux 命令,请按以下步骤操作:
-
将命令放置在一个文件中。
-
插入一个神奇的第一行。
-
使用
chmod将文件设置为可执行。 -
执行该文件。
文件称为脚本或shell 脚本。神奇的第一行应该是符号 #!(发音为“shebang”),后面是读取并运行脚本的程序的路径:^(3)
#!/bin/bash
这是一个显示问候并打印今天日期的 shell 脚本。以 # 开头的行是注释:
#!/bin/bash
# This is a sample script
echo "Hello there!"
date
使用文本编辑器将这些行存储在名为howdy的文件中。然后通过运行以下任意命令使文件可执行:
$ chmod 755 howdy *Set all permissions, including execute permission*
$ chmod +x howdy *Or, just add execute permission*
然后运行它:
$ ./howdy
Hello there!
Fri Sep 10 17:00:52 EDT 2021
前导的点和斜杠(./)表示脚本位于当前目录中。如果没有它们,Linux shell 将无法找到脚本:^(4)
$ howdy
howdy: command not found
Linux shell 提供了一些在脚本中非常有用的编程语言特性。例如,bash 提供 if 语句、for 循环、while 循环和其他控制结构。本书中穿插了一些示例。有关语法,请参阅 man bash。
成为超级用户
一些文件、目录和程序对普通用户是受保护的,包括您自己:
$ touch /usr/local/avocado *Try to create a file in a system directory*
touch: cannot touch '/usr/local/avocado': Permission denied
“权限被拒绝”通常意味着您尝试访问受保护的资源。它们只能由 Linux 超级用户(用户名为 root)访问。大多数 Linux 系统配备了一个名为 sudo(发音为“soo doo”)的程序,允许您在单个命令的持续时间内成为超级用户。如果您自己安装了 Linux,则您的帐户可能已经设置为运行 sudo。如果您是别人的 Linux 系统上的一个用户,您可能没有超级用户权限;如果不确定,请与系统管理员联系。
假设您已经正确设置,只需运行 sudo,并提供所需运行的命令以作为超级用户运行。系统将提示您输入登录密码以证明您的身份。正确输入密码后,命令将以 root 权限运行:
$ sudo touch /usr/local/avocado *Create the file as root*
[sudo] password for smith: *password here*
$ ls -l /usr/local/avocado *List the file*
-rw-r--r-- 1 root root 0 Sep 10 17:16 avocado
$ sudo rm /usr/local/avocado *Clean up as root*
sudo 可能会在一段时间内记住(缓存)您的密码短语,这取决于 sudo 的配置方式,因此它可能不会每次都提示您。
进一步阅读
要了解更多 Linux 使用的基础知识,请阅读我的前一本书Linux Pocket Guide(O’Reilly),或查找在线教程。
^(1) 点和双点不是 shell 评估的表达式。它们是每个目录中存在的硬链接。
^(2) 或者是另一个程序,如果你重新定义了 shell 变量 PAGER 的值。
^(3) 如果省略了 shebang 行,你的默认 shell 将运行该脚本。包含这行是一个良好的实践。
^(4) 这是因为当前目录通常会因安全原因而被省略在 shell 的搜索路径中。否则,攻击者可以在你的当前目录中放置一个恶意的可执行脚本,命名为 ls,当你运行 ls 时,这个脚本将会运行而不是真正的 ls 命令。
附录 B. 如果你使用不同的 Shell
本书假设您的登录 Shell 是bash,但如果不是,表 B-1 可能会帮助您调整本书中其他 Shell 的示例。勾号 ✓ 表示兼容性——给定特性与bash相似到足以正确运行本书示例。但是,该特性的行为可能在其他方面与bash不同,请仔细阅读任何脚注。
注
无论您的登录 Shell 是哪个,以 #!/bin/bash 开头的脚本都将由 bash 处理。
要在系统上安装的另一个 Shell 上进行实验,只需按其名称运行它(例如,ksh),完成后按 Ctrl-D。要更改登录 Shell,请阅读man chsh。
表 B-1. 其他 Shell 支持的bash特性,按字母顺序排列
bash特性 |
dash | fish | ksh | tcsh | zsh |
|---|---|---|---|---|---|
alias内建命令 |
✓ | ✓,但alias name 不打印别名 |
✓ | 没有等号:alias g grep |
✓ |
使用&进行后台化 |
✓ | ✓ | ✓ | ✓ | ✓ |
bash -c |
dash -c |
fish -c |
ksh -c |
tcsh -c |
zsh -c |
bash命令 |
dash |
fish |
ksh |
tcsh |
zsh |
bash在 /bin/bash 的位置 |
/bin/dash | /bin/fish | /bin/ksh | /bin/tcsh | /bin/zsh |
BASH_SUBSHELL变量 |
|||||
使用 {} 进行大括号展开 |
使用 seq |
仅支持 {a,b,c},不支持 {a..c} |
✓ | 使用 seq |
✓ |
cd -(切换目录) |
✓ | ✓ | ✓ | ✓ | ✓ |
cd内建命令 |
✓ | ✓ | ✓ | ✓ | ✓ |
CDPATH变量 |
✓ | set CDPATH value |
✓ | set cdpath = (dir1 dir2 …) |
✓ |
使用 $() 进行命令替换 |
✓ | 使用 () |
✓ | 使用反引号 | ✓ |
| 使用反引号进行命令替换 | ✓ | 使用 () |
✓ | ✓ | ✓ |
| 使用箭头键进行命令行编辑 | ✓ | ✓^(a) | ✓ | ✓ | |
| 使用 Emacs 键进行命令行编辑 | ✓ | ✓^(a) | ✓ | ✓ | |
使用set -o vi启用 Vim 键的命令行编辑 |
✓ | 运行bindkey -v |
✓ | ||
complete内建命令 |
不同的语法^(b) | 不同的语法^(b) | 不同的语法^(b) | compdef^(b) |
|
使用&&和||进行条件列表 |
✓ | ✓ | ✓ | ✓ | ✓ |
$HOME中的配置文件(详细信息请参阅 manpage) |
.profile | .config/fish/config.fish | .profile, .kshrc | .cshrc | .zshenv, .zprofile, .zshrc, .zlogin, .zlogout |
控制结构:for循环,if语句等 |
✓ | 不同的语法 | ✓ | 不同的语法 | ✓ |
dirs内建命令 |
✓ | ✓ | ✓ | ||
echo内建命令 |
✓ | ✓ | ✓ | ✓ | ✓ |
使用 \ 转义别名 |
✓ | ✓ | ✓ | ✓ | |
使用\进行转义 |
✓ | ✓ | ✓ | ✓ | ✓ |
exec内建命令 |
✓ | ✓ | ✓ | ✓ | ✓ |
使用$?获取退出代码 |
✓ | $status |
✓ | ✓ | ✓ |
export内建命令 |
✓ | set -x name value |
✓ | setenv name value |
✓ |
| 函数 | ✓^(c) | 不同的语法 | ✓ | ✓ | |
HISTCONTROL变量 |
在 man 页上查看以HIST_开头的变量名 |
||||
HISTFILE变量 |
set fish_history path |
✓ | set histfile = path |
✓ | |
HISTFILESIZE变量 |
set savehist = value |
+SAVEHIST | |||
history内置命令 |
有history,但命令没有编号 |
history是hist -l的别名 |
✓ | ✓ | |
history -c |
history clear |
删除~/.sh_history并重新启动ksh |
✓ | history -p |
|
使用!和^进行历史扩展 |
✓ | ✓ | |||
| 使用 Ctrl-R 进行历史增量搜索 | 输入命令的开头,然后按向上箭头搜索,右箭头选择 | ✓^(a) ^(d) | ✓^(e) | ✓^(f) | |
history number |
history -number |
history -N number |
✓ | history -*number* |
|
| 使用箭头键的历史记录 | ✓ | ✓^(a) | ✓ | ✓ | |
| 使用 Emacs 键进行历史记录 | ✓ | ✓^(a) | ✓ | ✓ | |
使用set -o vi的 Vim 键进行历史记录 |
✓ | 运行bindkey -v |
✓ | ||
HISTSIZE变量 |
✓ | ✓ | |||
使用fg、bg、Ctrl-Z、jobs进行作业控制 |
✓ | ✓ | ✓ | ✓^(g) | ✓ |
使用*、?、[]进行模式匹配 |
✓ | ✓ | ✓ | ✓ | ✓ |
| 管道 | ✓ | ✓ | ✓ | ✓ | ✓ |
popd内置命令 |
✓ | ✓ | ✓ | ||
使用<()进行进程替换 |
✓ | ✓ | |||
PS1变量 |
✓ | set PS1 value |
✓ | set prompt = value |
✓ |
pushd内置命令 |
✓ | 有pushd,但没有负参数 |
✓ | ||
| 双引号 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 单引号 | ✓ | ✓ | ✓ | ✓ | ✓ |
将 stderr 重定向(2>) |
✓ | ✓ | ✓ | ✓ | |
将 stdin(<)、stdout(>、>>)重定向 |
✓ | ✓ | ✓ | ✓ | ✓ |
将 stdout 和 stderr 重定向(&>) |
追加 2>&1^(h) |
✓ | 追加 2>&1^(h) |
>& |
✓ |
使用source或.(点号)来源化文件 |
只有点号^(i) | ✓ | ✓^(i) | 只有source |
✓^(i) |
使用()创建子 shell |
✓ | ✓ | ✓ | ✓ | |
| 文件名的制表完成 | ✓ | ✓^(a) | ✓ | ✓ | |
type内置命令 |
✓ | ✓ | type是whence -v的别名 |
不是,但which是内置命令 |
✓ |
unalias内置命令 |
✓ | functions --erase |
✓ | ✓ | ✓ |
使用name=value定义变量 |
✓ | set name value |
✓ | set name = value |
✓ |
使用$name进行变量评估 |
✓ | ✓ | ✓ | ✓ | ✓ |
^(a) 此功能默认禁用。运行 set -o emacs 可以启用它。旧版本的 ksh 可能有不同的行为。^(b) 自定义命令补全,使用 complete 命令或类似命令,在不同的 shell 中可能有显著不同;请参阅相应 shell 的 man 手册。^(c) 函数:此 shell 不支持以 function 关键字开头的新式定义。^(d) 在 ksh 中,历史记录的增量搜索方式有所不同。按 Ctrl-R 键,输入字符串,然后按 Enter 键可以调出包含该字符串的最近命令。再次按 Ctrl-R 键和 Enter 键可以向后搜索下一个匹配的命令,依此类推。按 Enter 键执行命令。^(e) 要在 tcsh 中启用 Ctrl-R 的增量历史搜索功能,请运行命令 bindkey ^R i-search-back(并将其添加到 shell 配置文件)。搜索行为与 bash 有所不同;参阅 man tcsh。^(f) 在 vi 模式下,输入 / 后跟搜索字符串,然后按 Enter 键。按 n 跳转到下一个搜索结果。^(g) 作业控制:tcsh 不像其他 shell 那样智能地跟踪默认作业号,因此您可能经常需要将作业号(例如 %1)作为 fg 和 bg 的参数提供。^(h) stdout 和 stderr 的重定向:此 shell 的语法是:command > file 2>&1。最后的术语 2>&1 意味着“重定向 stderr,即文件描述符 2,到 stdout,即文件描述符 1”。^(i) 在此 shell 中进行源文件时,需要明确指定源文件的路径,例如对于当前目录中的文件使用 ./myfile,否则 shell 将无法找到文件。或者,将文件放入 shell 搜索路径中的一个目录中。 |


浙公网安备 33010602011771号