Bash-秘籍-全-
Bash 秘籍(全)
原文:
annas-archive.org/md5/8bd1ff8e5afaf98aec24c35c26d1a902译者:飞龙
前言
在本书中,我们使用Bash(即Bourne Again Shell)编写了各种脚本。它们从简单到复杂,包含了许多实用的工具或程序。目前,Bash 是大多数 GNU/Linux 发行版的默认 shell,并且在 Linux 终端中广泛使用。它可以用于各种任务,并且在 Linux/Unix 生态系统中具有灵活性。换句话说,一个熟悉 Bash 和 Linux 命令行界面的用户,可以在几乎任何其他 Linux 系统上自行安装并执行类似任务,所需的修改(如果有的话)极少。Bash 脚本也能够在对其他已安装软件几乎没有依赖的情况下工作,在一个非常精简的系统(最小安装)上,用户仍然可以编写一个强大的脚本来自动化任务或协助执行重复性任务。
本书完全聚焦于 Ubuntu 环境中的 Bash 使用,这是一个非常常见的 Linux 发行版,但它应该能够相对容易地移植到其他发行版上。尽管可以将部分内容移植到苹果或 Windows 操作系统上,但本书并不是专为这些操作系统编写的。
本书适合谁阅读
Bash Cookbook 是为高级用户或系统管理员编写的,他们从事编写 Bash 脚本以自动化任务,或旨在提升命令行操作的工作效率。例如,不必记住一系列命令来执行特定操作,这些命令可以全部写入一个专门为该任务设计的脚本中,并且脚本能够执行输入验证和格式化输出。为什么不通过一个强大的工具来节省时间并减少错误呢?
如果你有兴趣学习如何自动化复杂的日常系统任务,且这些任务可以通过各种系统基础设施来执行,比如在系统启动时启动脚本,或通过定时的 cron 任务来执行,那么这本书也是理想的选择。
如何从本书中获得最大收益
作为作者,我们编写本书的目的是让读者可以轻松理解,并通过几个配方教授你使用 Bash 编程的多种方法。然而,为了从中获得最大的收益,我们鼓励你:
-
配置好一个 Linux 系统(最好是 Ubuntu)以完成配方
-
按照配方进行操作
-
请记住,每个配方的组件,以及配方本身,看看它们如何能够以新的方式被重用或组合
然而,本书假设你已经具备一定的知识水平,才能开始本书的学习,并且这些技能本书中并未涵盖。以下技能将不在本书中讨论:
-
如何设置和配置 Linux 系统
-
如何安装、访问和配置特定的文本编辑器(虽然大多数 Linux 发行版已经包含了几种编辑器)
-
一些计算和编程的基础知识(尽管我们会尽力提供速成课程)
为了在这些领域获得熟练度,我们建议你如果是一个擅长解决问题并且学习迅速的人,可以尽力尝试;或者首先查阅以下资源:
-
Linux 或其他发行版社区
-
开源论坛或小组
-
YouTube 或类似媒体
下载示例代码文件
您可以从您的帐户在 www.packtpub.com 下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packtpub.com/support 并注册,以便直接通过电子邮件接收文件。
您可以按照以下步骤下载代码文件:
-
在 www.packtpub.com 登录或注册。
-
选择“支持”标签。
-
点击“代码下载与勘误”。
-
在搜索框中输入书籍名称并按照屏幕上的指示操作。
一旦文件下载完成,请确保使用最新版本解压或提取文件夹:
-
Windows 下的 WinRAR/7-Zip
-
Mac 下的 Zipeg/iZip/UnRarX
-
Linux 下的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Bash-Cookbook。如果代码有任何更新,将会更新现有的 GitHub 仓库。
我们的丰富图书和视频目录中还提供了其他代码包,您可以在github.com/PacktPublishing/上找到它们。快来看看吧!
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如: "完整路径更加具体并且硬编码;解释器将尝试使用完整路径。例如,/bin/ls 或 /usr/local/bin/myBinary。"
一段代码如下所示:
#!/bin/bash
AGE=17
if [ ${AGE} -lt 18 ]; then
echo "You must be 18 or older to see this movie"
fi
任何命令行输入或输出如下所示:
rbrash@moon:~$ history
1002 ls
1003 cd ../
1004 pwd
1005 whoami
1006 history
粗体:表示新术语、重要词汇或屏幕上显示的词语。例如,菜单或对话框中的词语会以这种方式显示在文本中。
警告或重要提示通常以这种方式显示。
小贴士和技巧以这种方式显示。
各节
在本书中,您会发现多个经常出现的标题(准备就绪、如何做...、它是如何工作的...、还有更多...,以及另请参见)。
为了提供清晰的指令,说明如何完成一个配方,您可以按照以下方式使用这些部分:
准备就绪
本节告诉您配方的预期效果,并描述如何设置任何所需的软件或预先设置。
如何做...
本节包含完成配方所需遵循的步骤。
它是如何工作的...
该部分通常包括对上一部分发生内容的详细解释。
还有更多...
该部分包含关于配方的附加信息,帮助您更好地理解配方。
另请参见
本节提供了指向配方其他有用信息的链接。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请通过电子邮件 feedback@packtpub.com 与我们联系,并在邮件主题中注明书籍标题。如果你对本书的任何部分有疑问,请通过 questions@packtpub.com 向我们咨询。
勘误表:虽然我们已尽力确保内容的准确性,但难免会有错误。如果你在本书中发现错误,我们将非常感激你能向我们报告。请访问 www.packtpub.com/submit-errata,选择你的书籍,点击“勘误提交表格”链接并填写相关信息。
盗版:如果你在互联网上发现我们作品的任何非法副本,我们将非常感激你提供相关的网址或网站名称。请通过 copyright@packtpub.com 联系我们,并附上相关资料的链接。
如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有意撰写或参与编写书籍,请访问 authors.packtpub.com。
评价
请留下评价。在你阅读并使用本书后,为什么不在你购买书籍的网站上留下评价呢?潜在读者可以通过你的中立意见做出购买决定,我们在 Packt 也能了解你对我们产品的看法,而我们的作者则能看到你对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问 packtpub.com。
第一章:Bash 快速入门
本章的主要目的是让你掌握足够的 Linux shell/Bash 知识,以便你能够顺利启动并运行,之后的内容就会变得简单易懂。
在本章中,我们将涵盖以下主题:
-
开始学习 Bash 和 CLI 基础
-
创建和使用基本变量
-
隐藏的 Bash 变量和保留字
-
使用 if、else 和 elseif 的条件逻辑
-
case/switch 语句和循环结构
-
使用函数和参数
-
包括源文件
-
解析程序输入参数
-
标准输入、标准输出和标准错误
-
使用管道链接命令
-
查找更多关于 Bash 中使用的命令的信息
本章将为你提供完成本书其余章节食谱所需的基础知识。
开始学习 Bash 和 CLI 基础
首先,我们需要打开一个 Linux 终端或 shell。根据你所使用的 Linux 发行版,这可以通过多种方式完成,但在 Ubuntu 中,最简单的方法是导航到应用程序菜单并找到标有“终端”的程序。终端或 shell 是用户输入命令并在同一 shell 中执行命令的地方。简而言之,结果(如果有的话)会显示出来,终端将保持打开状态,等待输入新命令。一旦 shell 被打开,一个提示符将会出现,类似于以下内容:
rbrash@moon:~$
提示符的格式为 username@YourComputersHostName,后面跟着一个分隔符。在本书的整本食谱中,你会看到命令使用用户 rbrash;这是作者名字(Ron Brash)的缩写,在你的情况下,它将与你的用户名相匹配。
它也可能看起来类似于:
root@hostname #
$ 表示普通用户,而 # 表示 root 用户。在 Linux 和 Unix 系统中,root 指的是root 用户,类似于 Windows 中的管理员账户。它可以执行任何任务,因此在使用具有 root 权限的用户时应该小心。例如,root 用户可以访问操作系统中的所有文件,并且还可以删除操作系统使用的任何或所有关键文件,这可能会导致系统无法使用或崩溃。
当终端或 shell 运行时,Bash shell 会以一组特定于用户 bash 配置文件的参数和命令执行。这个配置文件通常称为 .bashrc,它可以用于包含命令别名、快捷方式、环境变量和其他用户增强功能,例如提示符颜色。它位于 ~/.bashrc 或 ~/.bash_profile。
~ 或 ~/ 是用户主目录的快捷方式。它与 /home/yourUserName/ 等效,对于 root 用户来说,它是 /root。
用户的 Bash shell 还会包含一个记录用户执行的所有命令的历史记录(位于 ~/.bash_history),可以通过 history 命令查看,如下所示:
rbrash@moon:~$ history
1002 ls
1003 cd ../
1004 pwd
1005 whoami
1006 history
例如,您的第一个命令可能是使用ls来确定目录的内容。命令cd用于切换到父目录上方的某个目录。pwd命令用于返回当前工作目录的完整路径(例如,终端当前所在的目录)。
您可能还会在 shell 上执行另一个命令whoami,该命令将返回当前登录到 shell 的用户:
rbrash@moon:/$ whoami
rbrash
rbrash@moon:/$
使用输入命令的概念,我们可以将这些(或任何)命令放入shell 脚本中。以最简化的方式表示,shell 脚本看起来如下:
#!/bin/bash
# Pound or hash sign signifies a comment (a line that is not executed)
whoami #Command returning the current username
pwd #Command returning the current working directory on the filesystem
ls # Command returning the results (file listing) of the current working directory
echo “Echo one 1”; echo “Echo two 2” # Notice the semicolon used to delimit multiple commands in the same execution.
第一行包含解释器的路径,并告诉 shell 在解释此脚本时使用哪个解释器。第一行始终包含 shebang(#!)和路径前缀,二者之间没有空格:
#!/bin/bash
脚本不能自行执行;它需要由用户执行或被另一个程序、系统或其他脚本调用。脚本的执行还需要具备可执行权限,用户可以授予这些权限以使其变为可执行;这可以通过chmod命令完成。
要添加或授予基本可执行权限,请使用以下命令:
$ chmod a+x script.sh
执行脚本时,可以使用以下方法之一:
$ bash script.sh # if the user is currently in the same directory as the script
$ bash /path/to/script.sh # Full path
如果应用了正确的权限,并且 shebang 和 Bash 解释器路径正确,您可以使用以下两条命令来执行script.sh:
$ ./script.sh # if the user is currently in the same directory as the script
$ /path/to/script.sh # Full path
从前面的命令片段中,您可能会注意到一些关于路径的事情。脚本、文件或可执行文件的路径可以使用相对地址和完整路径来引用。相对地址有效地告诉解释器执行当前目录中可能存在的内容,或使用用户的全局 shell $PATH 变量。例如,系统知道二进制文件或可执行二进制文件存储在/usr/bin、/bin/和/sbin中,并会首先在这些目录中查找。完整路径更加具体且硬编码;解释器会尝试使用完整路径。例如,/bin/ls或/usr/local/bin/myBinary。
当您想在当前工作目录中运行二进制文件时,可以使用./script.sh、bash script.sh,甚至是完整路径。当然,每种方法都有其优缺点。
当您确切知道某个二进制文件在特定系统上的位置时,硬编码或完整路径可能很有用,特别是在由于潜在的安全或系统配置原因,无法依赖$PATH变量时。
当需要灵活性时,相对路径非常有用。例如,程序ABC可以位于/usr/bin或/bin位置,但可以简单地使用 ABC 来调用,而不是/pathTo/ABC。
到目前为止,我们已经介绍了基本的 Bash 脚本样式,并简要介绍了一些非常基础但关键的命令和路径。然而,要创建脚本,你需要一个编辑器!在 Ubuntu 中,通常默认情况下,你可以使用几种编辑器来创建 Bash 脚本:vi/vim、nano 和 gedit。还有许多其他的文本编辑器或 集成开发环境(IDE)可供选择,但这取决于个人偏好,读者可以根据自己的喜好选择一个。无论选择哪个文本编辑器,本书中的所有示例和教程都可以遵循。
在资源受限的环境中,例如树莓派,不使用像 Eclipse、Emacs 或 Geany 这样的完整编辑器,仍然可以使用它们作为灵活的集成开发环境(IDE)。
当你希望通过 SSH 远程或在控制台上创建或修改脚本时,了解 vi/vim 和 nano 非常有用。虽然 vi/vim 看起来有些过时,但当你最喜欢的编辑器没有安装或无法访问时,它就能派上用场。
使用 Vim 编写你的第一个 Bash 脚本
让我们从使用改进版的 vi(称为 vim)创建脚本开始。如果 Vim(增强版 VI)没有安装,可以使用 sudo 或 root 权限通过以下命令进行安装(-y 是 yes 的简写):
For Ubuntu or Debian based distributions
$ sudo apt-get -y install vim
For CentOS or RHEL
$ sudo yum install -y vim
For Fedora
$ sudo dnf install -y vim
打开终端并输入以下命令,首先查看当前终端导航到哪里,并使用 vim 创建脚本:
$ pwd
/home/yourUserName
$ vim my_first_script.sh
终端窗口将转换为 Vim 应用程序(类似于下图),你将准备好编写第一个脚本。与此同时,按下 Esc + I 键进入插入模式;左下角会有指示符,光标块开始闪烁:

要在 Vim 中导航,你可以使用许多键盘快捷键,但箭头键是最简单的,可以将光标向上、下、左、右移动。将光标移动到第一行的开头并输入以下内容:
#!/bin/bash
# Echo this is my first comment
echo "Hello world! This is my first Bash script!"
echo -n "I am executing the script with user: "
whoami
echo -n "I am currently running in the directory: "
pwd
exit 0
我们已经介绍了注释的概念和一些基本命令,但还没有介绍灵活的 echo 命令。echo 命令可以用来将文本输出到控制台或文件中,-n 标志可以打印文本而不包含换行符(换行符的效果与按下 Enter 键相同)——这使得 whoami 和 pwd 命令的输出能够显示在同一行。
程序退出时返回 0 状态,表示它以正常状态退出。随着我们进一步了解如何搜索或检查命令的退出状态,错误和其他条件也将涵盖此内容。
完成后,按 Esc 键退出插入模式;返回命令模式并输入 :,然后可以执行 vim 命令 w + q。总结起来,键入以下键序列:Esc,然后 :wq。这将通过写入磁盘(w)并退出(q)来退出 Vim,并将你带回控制台。
您可以通过查阅其文档使用 Linux 手册页或参考 Packt 提供的一个兄弟书籍获得有关 Vim 的更多信息(www.packtpub.com/application-development/hacking-vim-72)。
要执行您的第一个脚本,请输入bash my_first_script.sh命令,控制台将返回类似的输出:
$ bash my_first_script.sh
Hello world! This is my first Bash script!
I am executing the script with user: rbrash
I am currently running in the directory: /home/rbrash
$
恭喜您——您已经创建并执行了您的第一个 Bash 脚本。有了这些技能,您可以开始创建更复杂的脚本来自动化和简化几乎任何日常 CLI 例程。
创建和使用基本变量
将变量视为值的占位符是最好的方式。它们可以是永久的(静态的)或瞬时的(动态的),并且它们将有一个称为作用域的概念(稍后详述)。为了准备使用变量,我们需要考虑刚刚编写的脚本:my_first_script.sh。在脚本中,我们可以轻松地使用变量来包含静态值(每次都在那里)或通过每次运行脚本创建的动态值。例如,如果我们想使用像PI的值(3.14的值),那么我们可以像这样使用一个变量来编写这段简短的脚本:
PI=3.14
echo "The value of PI is $PI"
如果包含在完整脚本中,脚本片段将输出:
The value of Pi is 3.14
请注意,将一个值(3.14)分配给变量的想法被称为赋值。我们赋予了变量名为PI的变量值为3.14。我们还使用$PI引用了PI变量。这可以通过多种方式实现:
echo "1\. The value of PI is $PI"
echo "2\. The value of PI is ${PI}"
echo "3\. The value of PI is" $PI
这将输出以下内容:
1\. The value of PI is 3.14
2\. The value of PI is 3.14
3\. The value of PI is 3.14
虽然输出是相同的,但机制略有不同。在第 1 版中,我们在双引号中引用PI变量,这表示一个字符串(字符数组)。我们也可以使用单引号,但这将使其成为字面字符串。在第 2 版中,我们在{ }或squiggly大括号内引用变量;这对于在可能破坏脚本的情况下保护变量非常有用。以下是一个例子:
echo "1\. The value of PI is $PIabc" # Since PIabc is not declared, it will be empty string
echo "2\. The value of PI is ${PI}" # Still works because we correctly referred to PI
如果任何变量未声明然后我们尝试使用它,那个变量将被初始化为空字符串。
以下命令将把数值转换为字符串表示。在我们的示例中,$PI仍然是一个包含数字的变量,但我们也可以像这样创建PI变量:
PI="3.14" # Notice the double quotes ""
这将包含一个字符串而不是数值,比如整数或浮点数。
数据类型的概念在本手册中没有得到充分探讨。最好将其作为读者探索的主题,因为它是编程和计算机使用的基本概念。
等等!你说数字和字符串之间有区别?当然有,因为如果没有转换(或者一开始没有正确设置),这可能会限制你能做的事情。例如,3.14 与 3.14(数字)是不一样的。3.14 由四个字符组成:3 + . + 1 + 4。如果我们想对以字符串形式表示的 PI 值进行乘法运算,要么计算/脚本会出错,要么我们会得到一个毫无意义的答案。
我们将在后续的第二章中进一步讲解转换内容,像打字机和文件浏览器一样工作。
假设我们想将一个变量赋值给另一个变量。我们可以这样做:
VAR_A=10
VAR_B=$VAR_A
VAR_C=${VAR_B}
如果前面的代码片段在一个有效的 Bash 脚本中,我们会为每个变量得到值 10。
动手实践变量赋值
打开一个新的空白文件并添加以下内容:
#!/bin/bash
PI=3.14
VAR_A=10
VAR_B=$VAR_A
VAR_C=${VAR_B}
echo "Let's print 3 variables:"
echo $VAR_A
echo $VAR_B
echo $VAR_C
echo "We know this will break:"
echo "0\. The value of PI is $PIabc" # since PIabc is not declared, it will be empty string
echo "And these will work:"
echo "1\. The value of PI is $PI"
echo "2\. The value of PI is ${PI}"
echo "3\. The value of PI is" $PI
echo "And we can make a new string"
STR_A="Bob"
STR_B="Jane"
echo "${STR_A} + ${STR_B} equals Bob + Jane"
STR_C=${STR_A}" + "${STR_B}
echo "${STR_C} is the same as Bob + Jane too!"
echo "${STR_C} + ${PI}"
exit 0
注意命名法。使用标准化机制来命名变量是很好的,但如果多次使用STR_A和VAR_B,显然它们不够具描述性。未来,我们将使用更具描述性的名称,比如VAL_PI表示 PI 的值,或者STR_BOBNAME表示代表 Bob 名字的字符串。在 Bash 中,通常使用大写字母来描述变量,因为这样可以增加清晰度。
按下保存并退出到终端(如果终端尚未打开,请打开一个)。在应用适当的权限后执行脚本,你应该会看到以下输出:
Lets print 3 variables:
10
10
10
We know this will break:
0\. The value of PI is
And these will work:
1\. The value of PI is 3.14
2\. The value of PI is 3.14
3\. The value of PI is 3.14
And we can make a new string
Bob + Jane equals Bob + Jane
Bob + Jane is the same as Bob + Jane too!
Bob + Jane + 3.14
首先,我们看到如何使用三个变量,为它们赋值,并打印它们。其次,我们通过演示看到,连接字符串时解释器可能会出错(我们要记住这一点)。第三,我们打印了我们的PI变量,并用echo将其连接到一个字符串中。最后,我们进行了几种不同的连接方式,包括最终版本,它将一个数字值转换并附加到字符串中。
隐藏的 Bash 变量和保留字
等等——有隐藏变量和保留字吗?是的!有一些单词你不能直接在脚本中使用,除非它们被正确地包含在某个构造中,比如字符串。全局变量可在全局上下文中使用,这意味着它们对当前 shell 或打开的 shell 控制台中的所有脚本可见。在后面的章节中,我们将进一步探讨全局 shell 变量,但目前你需要了解的是,有一些有用的变量可以供你重复使用,例如$USER、$PWD、$OLDPWD和$PATH。
若要查看所有 shell 环境变量的列表,可以使用env命令(输出已被截短):
$ env
XDG_VTNR=7
XDG_SESSION_ID=c2
CLUTTER_IM_MODULE=xim
XDG_GREETER_DATA_DIR=/var/lib/lightdm-data/rbrash
SESSION=ubuntu
SHELL=/bin/bash
TERM=xterm-256color
XDG_MENU_PREFIX=gnome-
VTE_VERSION=4205
QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1
WINDOWID=81788934
UPSTART_SESSION=unix:abstract=/com/ubuntu/upstart-session/1000/1598
GNOME_KEYRING_CONTROL=
GTK_MODULES=gail:atk-bridge:unity-gtk-module
USER=rbrash
....
修改PATH环境变量非常有用,但也可能会让人感到沮丧,因为它包含了二进制文件的文件系统路径。例如,你的二进制文件可能位于/bin、/sbin或/usr/bin目录中,但当你运行某个命令时,系统会在你没有指定路径的情况下直接执行该命令。
好的,我们已经确认了预先存在的变量以及用户或其他程序可能创建的新全局变量的存在。在使用可能具有高概率相似命名的变量时,请务必使其特定于您的应用程序。
除了隐藏变量外,还有一些保留用于脚本或 Shell 内部的词语。例如,if 和 else 是用于为脚本提供条件逻辑的词语。想象一下,如果您创建了一个与已存在的命令、变量或函数(稍后详述)同名的命令,脚本可能会中断或执行错误的操作。
在尝试避免任何命名冲突(或命名空间冲突)时,请尝试通过附加或前缀标识符使您的变量更可能被您的应用程序使用。
以下列表包含一些您可能会遇到的常见保留字。其中一些可能看起来非常熟悉,因为它们告诉 Bash 解释器以特定方式解释任何文本,重定向输出,后台运行应用程序,甚至在其他编程/脚本语言中使用。
-
if,elif,else,fi -
while,do,for,done,continue,break -
case,select,time -
function -
&,|,>,<,!,= -
#,$,(, ),;,{, },[, ],\
欲查看完整参考,请访问:www.gnu.org/software/bash/manual/html_node/Reserved-Word-Index.html.
列表中的最后一个元素包含一组特定字符,告诉 Bash 执行特定的功能。井号标志着例如注释的开始。但是反斜杠 \ 是非常特殊的,因为它是一个转义字符。转义字符用于转义或停止解释器在看到这些特定字符时执行特定功能。例如:
$ echo # Comment
$ echo \# Comment
# Comment
在 第二章 中,即《如打字机和文件资源管理器一样操作》时,处理字符串和单/双引号时,转义字符将非常有用。
转义字符阻止在斜杠后执行下一个字符。但是,当处理换行符 (\n, \r\n) 和空字节 (\0) 时,这种行为不一定是一致的。
使用 if, else 和 elseif 进行条件逻辑
前一节介绍了几个保留字和一些会影响 Bash 运行的字符。其中最基本且可能被广泛使用的条件逻辑是使用 if 和 else 语句。让我们使用一个代码片段作为例子:
#!/bin/bash
AGE=17
if [ ${AGE} -lt 18 ]; then
echo "You must be 18 or older to see this movie"
fi
注意 if 语句中方括号前后的空格。Bash 对括号语法要求格外严格。
如果我们使用小于(<)或-lt来评估变量age(Bash 提供了许多用于评估变量的语法构造),我们需要使用if语句。在我们的if语句中,如果$AGE小于18,我们将输出消息You must be 18 or older to see this movie。否则,脚本将不会执行echo语句,而是继续执行。注意,if语句以保留字fi结束。这不是错误,符合 Bash 语法要求。
假设我们想使用else添加一个通用的捕获条件。如果if语句的then命令块不满足条件,则会执行else。
#!/bin/bash
AGE=40
if [ ${AGE} -lt 18 ]
then
echo "You must be 18 or older to see this movie"
else
echo "You may see the movie!"
exit 1
fi
当AGE设置为整数值40时,if语句中的then命令块将不满足条件,else命令块将被执行。
评估二进制数字
假设我们想引入另一个if条件并使用elif(即else if的缩写):
#!/bin/bash
AGE=21
if [ ${AGE} -lt 18 ]; then
echo "You must be 18 or older to see this movie"
elif [ ${AGE} -eq 21 ]; then
echo "You may see the movie and get popcorn"
else
echo "You may see the movie!"
exit 1
fi
echo "This line might not get executed"
echo:
You may see the movie and get popcorn
This line might not get executed
使用if、elif和else,结合其他评估方法,我们可以执行特定的逻辑分支和函数,甚至退出脚本。要评估原始的二进制变量,请使用以下运算符:
-
-gt(大于 >) -
-ge(大于或等于 >=) -
-lt(小于 <) -
-le(小于或等于 <=) -
-eq(等于) -
-nq(不等于)
评估字符串
如变量小节所述,数字值与字符串不同。字符串通常按如下方式进行评估:
#!/bin/bash
MY_NAME="John"
NAME_1="Bob"
NAME_2="Jane"
NAME_3="Sue"
Name_4="Kate"
if [ "${MY_NAME}" == "Ron" ]; then
echo "Ron is home from vacation"
elif [ "${MY_NAME}" != ${NAME_1}" && "${MY_NAME}" != ${NAME_2}" && "${MY_NAME}" == "John" ]; then
echo "John is home after some unnecessary AND logic"
elif [ "${MY_NAME}" == ${NAME_3}" || "${MY_NAME}" == ${NAME_4}" ]; then
echo "Looks like one of the ladies are home"
else
echo "Who is this stranger?"
fi
MY_NAME variable will be executed and the string John is home after some unnecessary AND logic will be echoed to the console. In the snippet, the logic flows like this:
-
如果
MY_NAME等于Ron,则执行echo "Ron is home from vacation" -
否则,如果
MY_NAME不等于NAME_1并且MY_NAME不等于NAME_2并且MY_NAME等于John,则执行echo "John is home after some unnecessary AND logic" -
否则如果
MY_NAME等于NAME_3或MY_NAME等于NAME_4,则执行echo "Looks like one of the ladies" -
否则
echo "Who is this stranger?"
注意运算符:&&、||、==和!=
-
&&(表示和) -
||(表示或) -
==(等于) -
!=(不等于) -
-n(不是空的或未设置) -
-z(为空且长度为零)
在计算机世界中,Null 表示未设置或为空。你可以在脚本中使用许多不同类型的运算符或测试。欲了解更多信息,请查看:tldp.org/LDP/abs/html/comparison-ops.html和www.gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html#Shell-Arithmetic
你还可以像字符串一样评估数字,使用((" $a" > "$b"))或[[ "$a" > "$b" ]]。注意使用了双括号和方括号。
嵌套的if语句
如果单一层级的if语句不足以满足需求,且你希望在if语句中添加额外的逻辑,你可以创建嵌套条件语句。可以通过以下方式来实现:
#!/bin/bash
USER_AGE=18
AGE_LIMIT=18
NAME="Bob" # Change to your username if you want to execute the nested logic
HAS_NIGHTMARES="true"
if [ "${USER}" == "${NAME}" ]; then
if [ ${USER_AGE} -ge ${AGE_LIMIT} ]; then
if [ "${HAS_NIGHTMARES}" == "true" ]; then
echo "${USER} gets nightmares, and should not see the movie"
fi
fi
else
echo "Who is this?"
fi
case/switch语句和循环结构
除了if和else语句,Bash 还提供了 case 或 switch 语句以及循环结构,可以用来简化逻辑,使其更易读和可持续。想象一下,如果有很多elif评估的if语句,它会变得很繁琐!
#!/bin/bash
VAR=10
# Multiple IF statements
if [ $VAR -eq 1 ]; then
echo "$VAR"
elif [ $VAR -eq 2]; then
echo "$VAR"
elif [ $VAR -eq 3]; then
echo "$VAR"
# .... to 10
else
echo "I am not looking to match this value"
fi
在大量的if和elif条件逻辑块中,每个if和elif需要在执行特定的代码分支之前进行评估。使用 case/switch 语句可能更快,因为第一个匹配的条件会被执行(而且看起来更简洁)。
基本的 case 语句
你可以用case 语句代替if/else语句来评估一个变量。请注意,esac是反向拼写的,用来退出 case 语句,类似于fi用于if语句。
Case 语句遵循以下流程:
case $THING_I_AM_TO_EVALUATE in
1) # Condition to evaluate is number 1 (could be "a" for a string too!)
echo "THING_I_AM_TO_EVALUATE equals 1"
;; # Notice that this is used to close this evaluation
*) # * Signified the catchall (when THING_I_AM_TO_EVALUATE does not equal values in the switch)
echo "FALLTHOUGH or default condition"
esac # Close case statement
以下是一个可运行的示例:
#!/bin/bash
VAR=10 # Edit to 1 or 2 and re-run, after running the script as is.
case $VAR in
1)
echo "1"
;;
2)
echo "2"
;;
*)
echo "What is this var?"
exit 1
esac
基本循环
你能想象遍历一个文件列表或动态数组,单调地评估每一个文件吗?或者等到某个条件为真?对于这些类型的场景,你可能想使用for 循环、do while 循环或 until 循环来优化你的脚本,使得操作更简单。for 循环、do while 循环和 until 循环可能看起来相似,但它们之间有细微的差别。
For 循环
for循环通常用于你需要对数组中的每个条目执行多个任务或命令时,或者希望在有限数量的项上执行给定命令。在这个例子中,我们有一个包含三个元素的数组(或列表):file1、file2和file3。for循环将echo每个FILES中的元素并退出脚本:
#!/bin/bash
FILES=( "file1" "file2" "file3" )
for ELEMENT in ${FILES[@]}
do
echo "${ELEMENT}"
done
echo "Echo\'d all the files"
Do while 循环
作为替代,我们包括了do while循环。它与for循环相似,但更适用于动态条件,比如当你不知道什么时候会返回某个值,或者在满足条件之前一直执行任务时。方括号中的条件与if语句相同:
#!/bin/bash
CTR=1
while [ ${CTR} -lt 9 ]
do
echo "CTR var: ${CTR}"
((CTR++)) # Increment the CTR variable by 1
done
echo "Finished"
Until 循环
为了完整性,我们包括了until循环。它并不常用,几乎与do while循环相同。请注意,它的条件和操作与递增计数器直到某个值达到时的一致:
#!/bin/bash
CTR=1
until [ ${CTR} -gt 9 ]
do
echo "CTR var: ${CTR}"
((CTR++)) # Increment the CTR variable by 1
done
echo "Finished"
使用函数和参数
到目前为止,我们提到函数是一个保留字,仅用于单个过程的 Bash 脚本中,那么什么是函数呢?
为了说明什么是函数,首先我们需要定义什么是函数——函数是一个自包含的代码段,执行单一任务。然而,执行某项任务的函数可能还会执行多个子任务来完成其主要任务。
例如,你可以创建一个名为file_creator的函数,执行以下任务:
-
检查文件是否存在。
-
如果文件存在,截断它。否则,创建一个新文件。
-
应用正确的权限。
函数也可以传递参数。参数就像变量,可以在函数外部设置,然后在函数内部使用。这非常有用,因为我们可以创建执行通用任务的代码段,其他脚本或甚至循环内的代码都可以重用。你也可以有局部变量,它们在函数外部不可访问,仅在函数内部使用。那么函数是什么样子的呢?
#!/bin/bash
function my_function() {
local PARAM_1="$1"
local PARAM_2="$2"
local PARAM_3="$3"
echo "${PARAM_1} ${PARAM_2} ${PARAM_3}"
}
my_function "a" "b" "c"
正如我们在简单脚本中看到的那样,有一个使用function保留字声明的名为my_function的函数。函数的内容包含在大括号{}内,并引入了三个新概念:
-
参数是这样系统地引用的:
$1代表参数 1,$2代表参数 2,$3代表参数 3,依此类推。 -
local关键字指的是带有此关键字声明的变量仅在该函数内部可访问。 -
我们可以仅通过名称调用函数,并简单地添加参数,就像在前面的示例中一样。
在接下来的部分,我们将深入探讨一个更实际的示例,这将使这个观点更清晰:函数是日常有用的,并且能够在适当的地方让任何部分的功能变得易于重用。
在 for 循环中使用带参数的函数
在这个简短的示例中,我们有一个名为create_file的函数,它在FILES数组中的每个文件上调用。该函数创建一个文件,修改其权限,然后使用ls命令被动地检查其存在性:
#!/bin/bash
FILES=( "file1" "file2" "file3" ) # This is a global variable
function create_file() {
local FNAME="${1}" # First parameter
local PERMISSIONS="${2}" # Second parameter
touch "${FNAME}"
chmod "${PERMISSIONS}" "${FNAME}"
ls -l "${FNAME}"
}
for ELEMENT in ${FILES[@]}
do
create_file "${ELEMENT}" "a+x"
done
echo "Created all the files with a function!"
exit 0
包含源文件
除了函数外,我们还可以创建多个脚本并将它们包含在内,这样我们就可以利用任何共享的变量和函数。
假设我们有一个包含多个用于创建文件的函数的库或工具脚本。这个脚本本身可能在多个脚本任务中有用或可重用,因此我们使其与程序无关。然后,我们有另一个脚本,这个脚本专门处理一个任务:执行无用的文件系统操作(IO)。在这种情况下,我们将有两个文件:
-
io_maker.sh(包括library.sh并使用library.sh中的函数) -
library.sh(包含已声明的函数,但不执行它们)
io_maker.sh脚本简单地导入或包含了library.sh脚本,并继承了任何全局变量、函数和其他包含内容。通过这种方式,io_maker.sh实际上认为这些可用的函数是它自己的,并且可以像它们是在其中定义的一样执行它们。
包含/导入库脚本并使用外部函数
为了准备这个示例,创建以下两个文件并打开它们:
-
io_maker.sh -
library.sh
在library.sh中,添加以下内容:
#!/bin/bash
function create_file() {
local FNAME=$1
touch "${FNAME}"
ls "${FNAME}" # If output doesn't return a value - file is missing
}
function delete_file() {
local FNAME=$1
rm "${FNAME}"
ls "${FNAME}" # If output doesn't return a value - file is missing
}
在io_maker.sh中,添加以下内容:
#!/bin/bash
source library.sh # You may need to include the path as it is relative
FNAME="my_test_file.txt"
create_file "${FNAME}"
delete_file "${FNAME}"
exit 0
当你运行脚本时,应该得到相同的输出:
$ bash io_maker.sh
my_test_file.txt
ls: cannot access 'my_test_file.txt': No such file or directory
尽管不明显,但我们可以看到两个函数都被执行了。第一行输出是 ls 命令,在 create_file() 中创建文件后成功找到 my_test_file.txt。在第二行中,我们可以看到当删除作为参数传入的文件时,ls 返回了一个错误。
不幸的是,直到现在,我们只能创建和调用函数,并执行命令。下一步将在下一节中讨论,获取命令和函数的返回码或字符串。
获取返回码和输出
到目前为止,我们一直在间歇性地使用 exit 命令退出脚本。对于那些好奇的人,你们可能已经在网上查找过这个命令的作用,但需要记住的关键概念是,每个脚本、命令或二进制文件都会以返回码退出。返回码是数字,并且被限制在 0 到 255 之间,因为使用的是无符号的 8 位整数。如果使用 -1,则会返回 255。
好的,那么返回码有哪些用处呢?返回码在你想知道在执行匹配时是否找到匹配项时非常有用(例如),以及命令是否完全成功或出现错误。让我们通过一个实际示例来探索使用 ls 命令的情况:
$ ls ~/this.file.no.exist
ls: cannot access '/home/rbrash/this.file.no.exist': No such file or directory
$ echo $?
2
$ ls ~/.bashrc
/home/rbrash/.bashrc
$ echo $?
0
注意返回值吗?在这个例子中,0 或 2 表示成功(0)或出现错误(1 和 2)。这些值是通过获取 $? 变量获得的,我们甚至可以像这样将其设置为一个变量:
$ ls ~/this.file.no.exist
ls: cannot access '/home/rbrash/this.file.no.exist': No such file or directory
$ TEST=$?
$ echo $TEST
2
从这个例子中,我们现在知道了返回码是什么,以及如何利用它们来获取从函数、脚本和命令返回的结果。
返回码 101
深入终端并创建以下 Bash 脚本:
#!/bin/bash
GLOBAL_RET=255
function my_function_global() {
ls /home/${USER}/.bashrc
GLOBAL_RET=$?
}
function my_function_return() {
ls /home/${USER}/.bashrc
return $?
}
function my_function_str() {
local UNAME=$1
local OUTPUT=""
if [ -e /home/${UNAME}/.bashrc ]; then
OUTPUT='FOUND IT'
else
OUTPUT='NOT FOUND'
fi
echo ${OUTPUT}
}
echo "Current ret: ${GLOBAL_RET}"
my_function_global "${USER}"
echo "Current ret after: ${GLOBAL_RET}"
GLOBAL_RET=255
echo "Current ret: ${GLOBAL_RET}"
my_function_return "${USER}"
GLOBAL_RET=$?
echo "Current ret after: ${GLOBAL_RET}"
# And for giggles, we can pass back output too!
GLOBAL_RET=""
echo "Current ret: ${GLOBAL_RET}"
GLOBAL_RET=$(my_function_str ${USER})
# You could also use GLOBAL_RET=`my_function_str ${USER}`
# Notice the back ticks "`"
echo "Current ret after: $GLOBAL_RET"
exit 0
脚本将在退出并返回码为 0(记住,ls 在成功运行时返回 0)之前输出以下内容:
rbrash@moon:~$ bash test.sh
Current ret: 255
/home/rbrash/.bashrc
Current ret after: 0
Current ret: 255
/home/rbrash/.bashrc
Current ret after: 0
Current ret:
Current ret after: FOUND IT
$
在这一节中,有三个函数利用了三个概念:
-
my_function_global使用global变量来返回命令的返回码 -
my_function_return使用保留字return** 和一个值(命令的返回码) -
my_function_str使用fork(一个特殊操作)来执行命令并获取输出(我们回显的字符串)
对于选项 3,有多种方法可以从函数中返回字符串,包括使用 eval 关键字。然而,在使用 fork 时,最好注意它在多次执行相同命令以获取输出时可能消耗的资源。
链接命令、管道和输入/输出
本节可能是本书中最重要的一部分,因为它描述了 Linux 和 Unix 上一个基本且强大的功能:使用管道并重定向输入或输出。就其本身而言,管道是一个相当简单的功能——命令和脚本可以将它们的输出重定向到文件或其他命令。那么,这有什么了不起的呢?在 Bash 脚本世界中,这几乎是对管道和重定向功能的极大低估,因为它们允许你通过其他命令或功能增强命令的功能。
让我们通过一个示例来看看如何使用名为 tail 和 grep 的命令。在这个例子中,用户 Bob 想要实时查看他的日志,但他只希望查找与无线接口相关的条目。Bob 的无线设备名称可以通过 iwconfig 命令找到:
$ iwconfig
wlp3s0 IEEE 802.11abgn ESSID:"127.0.0.1-2.4ghz"
Mode:Managed Frequency:2.412 GHz Access Point: 18:D6:C7:FA:26:B1
Bit Rate=144.4 Mb/s Tx-Power=22 dBm
Retry short limit:7 RTS thr:off Fragment thr:off
Power Management:on
Link Quality=58/70 Signal level=-52 dBm
Rx invalid nwid:0 Rx invalid crypt:0 Rx invalid frag:0
Tx excessive retries:0 Invalid misc:90 Missed beacon:0
iwconfig 命令现在已被弃用。以下命令也会为你提供无线接口的信息:
$ iw dev # This will give list of wireless interfaces
$ iw dev wlp3s0 link # This will give detailed information about particular wireless interface
现在 Bob 知道了他无线网卡的标识名称(wlp3s0),他可以在系统日志中进行搜索。日志通常可以在 /var/log/messages 中找到。使用 tail 命令和 -F 标志(允许将日志持续输出到控制台),Bob 现在可以看到系统的 所有 日志。不幸的是,他希望通过 grep 过滤日志,使得只显示包含关键词 wlp3s0 的日志。
Bob 面临一个选择:他是持续搜索该文件,还是可以将 tail 和 grep 结合使用,以获取他所需要的结果?答案是肯定的——使用 管道!
$ tail -F /var/log/messages | grep wlp3s0
Nov 10 11:57:13 moon kernel: wlp3s0: authenticate with 18:d6:c7:fa:26:b1
Nov 10 11:57:13 moon kernel: wlp3s0: send auth to 18:d6:c7:fa:26:b1 (try 1/3)
Nov 10 11:57:13 moon kernel: wlp3s0: send auth to 18:d6:c7:fa:26:b1 (try 2/3)
...
当新的日志进入时,Bob 现在可以实时监控它们,并且可以通过 Ctrl+C 停止控制台输出。
使用管道,我们可以将多个命令组合成强大的混合命令,将每个命令的最佳特性扩展到单一命令行中。记住管道!
管道的使用和灵活性应该是相对直接的,但如果我们需要将命令的输入和输出进行重定向呢?这就需要引入三个命令,以便将信息从一个地方传送到另一个地方:
-
stdin(标准输入) -
stdout(标准输出) -
stderr(标准错误)
如果我们考虑的是一个 单独 的程序,stdin 是任何可以提供给它的内容,通常是作为参数或用户输入,例如使用 read。stdout 和 stderr 是两种 流,用于发送输出。通常,两个流的输出会显示在控制台上,但如果你只想将 stderr 流中的错误输出到文件呢?
$ ls /filethatdoesntexist.txt 2> err.txt
$ ls ~/ > stdout.txt
$ ls ~/ > everything.txt 2>&1 # Gets stderr and stdout
$ ls ~/ >> everything.txt 2>&1 # Gets stderr and stdout
$ cat err.txt
ls: cannot access '/filethatdoesntexist.txt': No such file or directory
$ cat stdout.txt
.... # A whole bunch of files in your home directory
>, 2>, and 2>&1. With the arrows we can redirect the output to any file or even to other programs!
注意单个 > 和双 >> 之间的区别。单个 > 会截断所有输出到文件的内容,而 >> 会附加内容到文件末尾。
当同时将stderr和stdout重定向到同一文件时,常会出现错误。Bash 应当先将输出写入文件,然后再重复输出文件描述符。有关文件描述符的更多信息,请参见:en.wikipedia.org/wiki/File_descriptor # 这是正确的
ls ~/ > everything.txt 2>&1
# 这是错误的
ls ~/ 2>&1> everything.txt
现在我们已经了解了 Bash 中最强大的功能之一的基础知识,接下来让我们尝试一个例子——重定向和管道大作战。
重定向和管道大作战
打开一个 Shell 并在你喜欢的编辑器中创建一个新的 Bash 文件:
#!/bin/sh
# Let's run a command and send all of the output to /dev/null
echo "No output?"
ls ~/fakefile.txt > /dev/null 2>&1
# Retrieve output from a piped command
echo "part 1"
HISTORY_TEXT=`cat ~/.bashrc | grep HIST`
echo "${HISTORY_TEXT}"
# Output the results to history.config
echo "part 2"
echo "${HISTORY_TEXT}" > "history.config"
# Re-direct history.config as input to the cat command
cat < history.config
# Append a string to history.config
echo "MY_VAR=1" >> history.config
echo "part 3 - using Tee"
# Neato.txt will contain the same information as the console
ls -la ~/fakefile.txt ~/ 2>&1 | tee neato.txt
首先,ls是产生错误的一种方式,而不是将错误输出推送到控制台,而是将其重定向到 Linux 中的一个特殊设备/dev/null。/dev/null特别有用,因为它是一个丢弃不会再使用的输入的地方。然后,我们将cat命令与grep结合使用,通过管道找到任何包含文本的行,并使用fork捕获输出到一个变量(HISTORY_TEXT)。
然后,我们使用stdout重定向将HISTORY_TEXT的内容回显到一个文件(history.config)中。使用history.config文件,我们将cat命令重定向为使用原始文件——这将在控制台上显示。
使用双重>>,我们将一个任意字符串追加到history.config文件中。
最后,我们使用重定向结束脚本,同时重定向stdout和stderr,管道以及tee命令。tee命令很有用,因为即使内容已经重定向到文件,它仍然可以用来显示内容(就像我们刚才演示的那样)。
获取程序输入参数
获取程序输入参数或参数与获取函数参数的方式非常相似,在最基本的层面,它们可以像$1 (arg1),$2 (arg2),$3 (arg3)等一样访问。然而,到目前为止,我们已经看到了一种叫做标志的概念,它允许你执行一些漂亮的操作,比如-l、--long-version、-v 10、--verbosity=10。标志实际上是一种用户友好的方式,可以在程序运行时传递参数或参数。例如:
bash myProgram.sh -v 99 --name=Ron -l Brash
现在你知道了标志是什么以及它们如何帮助你改进脚本,可以使用以下部分作为模板。
传递程序标志
在进入 Shell 并打开你喜欢的编辑器中新建一个文件后,让我们开始创建一个 Bash 脚本,实现以下功能:
-
当没有指定任何标志或参数时,它会打印帮助信息。
-
当设置了
-h或--help标志时,它会打印帮助信息。 -
当设置了
-f或--firstname标志时,它会设置名字变量。 -
当设置了
-l或--lastname标志时,它会设置姓氏变量。 -
当设置了
firstname和lastname标志时,它会打印欢迎信息并且不会报错。
除了基本的逻辑之外,我们可以看到代码利用了一个叫做getopts的功能。Getopts 允许我们抓取程序的参数标志,以便在程序中使用。还有一些我们也学过的基本构造——条件逻辑、while 循环和 case/switch 语句。一旦脚本发展成不仅仅是一个简单的工具或提供多个功能时,更基础的 Bash 构造就会变得司空见惯。
#!/bin/bash
HELP_STR="usage: $0 [-h] [-f] [-l] [--firstname[=]<value>] [--lastname[=]<value] [--help]"
# Notice hidden variables and other built-in Bash functionality
optspec=":flh-:"
while getopts "$optspec" optchar; do
case "${optchar}" in
-)
case "${OPTARG}" in
firstname)
val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
FIRSTNAME="${val}"
;;
lastname)
val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
LASTNAME="${val}"
;;
help)
val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
;;
*)
if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
echo "Found an unknown option --${OPTARG}" >&2
fi
;;
esac;;
f)
val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
FIRSTNAME="${val}"
;;
l)
val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
LASTNAME="${val}"
;;
h)
echo "${HELP_STR}" >&2
exit 2
;;
*)
if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
echo "Error parsing short flag: '-${OPTARG}'" >&2
exit 1
fi
;;
esac
done
# Do we have even one argument?
if [ -z "$1" ]; then
echo "${HELP_STR}" >&2
exit 2
fi
# Sanity check for both Firstname and Lastname
if [ -z "${FIRSTNAME}" ] || [ -z "${LASTNAME}" ]; then
echo "Both firstname and lastname are required!"
exit 3
fi
echo "Welcome ${FIRSTNAME} ${LASTNAME}!"
exit 0
执行前面的程序时,我们应该期待类似以下的响应:
$ bash flags.sh
usage: flags.sh [-h] [-f] [-l] [--firstname[=]<value>] [--lastname[=]<value] [--help]
$ bash flags.sh -h
usage: flags.sh [-h] [-f] [-l] [--firstname[=]<value>] [--lastname[=]<value] [--help]
$ bash flags.sh --fname Bob
Both firstname and lastname are required!
rbrash@moon:~$ bash flags.sh --firstname To -l Mater
Welcome To Mater!
获取有关命令的更多信息
随着我们深入,您可能会看到本书广泛使用许多命令,且没有详尽的解释。为了不让本书充斥着 Linux 介绍和有用命令的内容,有几个非常实用的命令可供使用:man和info。
man命令,即手册命令,非常详细,甚至在同一条目在不同类别中存在时,有多个部分。对于调查可执行程序或 Shell 命令来说,第 1 类就足够了。我们来看一下 mount 命令的条目:
$ man mount
...
MOUNT(8) System Administration MOUNT(8)
NAME
mount - mount a filesystem
SYNOPSIS
mount [-l|-h|-V]
mount -a [-fFnrsvw] [-t fstype] [-O optlist]
mount [-fnrsvw] [-o options] device|dir
mount [-fnrsvw] [-t fstype] [-o options] device dir
DESCRIPTION
All files accessible in a Unix system are arranged in one big tree, the
file hierarchy, rooted at /. These files can be spread out over sev‐
eral devices. The mount command serves to attach the filesystem found
on some device to the big file tree. Conversely, the umount(8) command
will detach it again.
...
(Press 'q' to Quit)
$
另外,还有info命令,如果信息页面存在,它会为你提供相关信息。
习惯man和info页面的风格,可以让你快速访问信息,从而节省时间,尤其是在没有网络连接时。
总结
在本章中,我们介绍了变量、类型和赋值的概念。我们还介绍了一些基础的 Bash 编程原语,如 for 循环、while 循环和 switch 语句。稍后,我们学习了函数的概念,它们的使用方式,以及如何传递
参数。
在下一章,我们将学习一些附加技术,使 Bash 更加强大。
第二章:像打字机和文件资源管理器一样操作
在本章中,我们将介绍以下内容:
-
基本的字符串和文件搜索
-
使用通配符和正则表达式
-
脚本中的数学和计算
-
仅使用 Bash 对字符串进行剥离/修改/排序/删除/搜索
-
使用 SED 和 AWK 删除/替换子字符串
-
使用 echo 和 printf 格式化数据/输出
-
为不同语言准备脚本的国际化
-
根据文件内容计算统计信息并减少重复项
-
使用文件属性与条件逻辑
-
读取定界数据并修改输出格式
介绍
希望前面的 Bash 入门课程章节已经提供了有关 Bash 工具和功能的提示。另一方面,本章介绍了几种附加技术,使得在搜索项目和文本或自动化文件资源管理器/文件系统操作时,Bash 的功能更加广泛。
单独来看,Bash 只是一个强大的脚本语言,但 Bash 的灵活性在于能够“粘合”其他技术(工具或语言),使得输出更加有用。换句话说,Bash 是一个基础平台,就像一些汽车爱好者在进行改装之前选择一个特定的平台。经过改装的车能做所有事情吗?当然不能,但它可以在特定情况下变得更强大或更有用,至少为移动提供了四个轮子。
常见的脚本不仅包含一系列自动化命令,通常还包括逻辑来修改字符串,例如以下内容:
-
删除尾部字符
-
替换单词的部分(子字符串)
-
在文件中搜索字符串
-
查找文件
-
测试文件类型(目录、文件、空文件等)
-
执行简单计算
-
限制搜索或数据的范围(过滤)
-
修改变量的内容(字符串变量中的字符串)
这种修改、限制甚至替换输入/输出数据的逻辑,在需要执行广泛搜索特定字符串或处理大量数据时,可以非常强大。终端可能会阻塞;充满输出或巨大的数据文件可能会让人感到探索起来非常艰巨!
然而,还有一个非常重要的概念仍然需要讨论,那就是递归功能。递归功能可以应用于脚本函数、逻辑,甚至命令操作。例如,你可以使用grep来递归地遍历整个目录,直到没有更多文件,或者可以递归地执行一个函数,直到满足某个条件(例如,在一个字符串中一次打印一个字符):
# e.g. File system
# / (start here)
# /home (oh we found home)
# /home/user (neat there is a directory inside it called user)
# /home/user/.. (even better, user has files - lets look in them too)
# /etc/ # We are done with /home and its "children" so lets look in /etc
# ... # Until we are done
小心递归(特别是函数),因为它有时可能会很慢,取决于结构的复杂性(例如,文件系统或文件大小)。如果存在逻辑错误,你可能会让函数永远递归执行下去!
本章的内容将集中在限制数据、利用数据、修改数据、国际化数据、替换数据,甚至是搜索数据。
基本的字符串和文件搜索
想象一下,在一个大花园里寻找四叶草。这将是非常困难的(对于计算机来说,这依然非常困难)。幸运的是,文字不是图像,计算机上的文本可以根据格式轻松搜索。之所以使用格式这个术语,是因为如果你的工具无法理解某种类型的文本(编码),那么你可能会遇到无法识别模式,甚至无法检测到文本的情况!
通常,当你查看控制台、文本文件、源代码(C、C++、Bash、HTML)、电子表格、XML 等类型时,你看到的是ASCII或UTF格式。ASCII 是*NIX世界中在控制台上常用的格式。还有 UTF编码方案,它是对 ASCII 的改进,支持计算机最初没有的各种扩展字符。它有多种格式,如 UTF-8、UTF-16 和 UTF-32。
当你听到编码和解码这两个词时,它类似于加密和解密。其目的不是隐藏某些内容,而是将某些数据转化为适合特定用途的形式。例如,用于传输、语言使用和压缩。
ASCII 和 UTF 并不是你目标数据可能采用的唯一格式。在各种类型的文件中,你可能会遇到不同类型的数据编码。这是一个与数据相关的不同问题,需要额外的考虑。
在本示例中,我们将开始搜索字符串的过程,并介绍几种在大量数据堆中搜索你自己目标的方法。让我们深入探讨。
准备工作
除了打开一个终端(如果需要,还可以打开你最喜欢的文本编辑器),我们只需要几个核心命令,如grep、ls、mkdir、touch、traceroute、strings、wget、xargs和find。
假设你的用户已经拥有正确的使用权限(当然也需要授权),我们将需要生成数据来开始搜索:
$ ~/
$ wget --recursive --no-parent https://www.packtpub.com www.packtpub.com # Takes awhile
$ traceroute packtpub.com > traceroute.txt
$ mkdir -p www.packtpub.com/filedir www.packtpub.com/emptydir
$ touch www.packtpub.com/filedir/empty.txt
$ touch www.packtpub.com/findme.xml; echo "<xml>" www.packtpub.com/findme.xml
如何操作...
通过递归爬取Packt Publishing 网站获得的数据,我们可以看到,在www.packtpub.com上,整个网站是可以访问的。哇!我们还创建了一些测试数据目录和文件。
- 接下来,打开一个终端并创建以下脚本:
#!/bin/bash
# Let's find all the files with the string "Packt"
DIRECTORY="www.packtpub.com/"
SEARCH_TERM="Packt"
# Can we use grep?
grep "${SEARCH_TERM}" ~/* > result1.txt 2&> /dev/null
# Recursive check
grep -r "${SEARCH_TERM}" "${DIRECTORY}" > result2.txt
# What if we want to check for multiple terms?
grep -r -e "${SEARCH_TERM}" -e "Publishing" "${DIRECTORY}" > result3.txt
# What about find?
find "${DIRECTORY}" -type f -print | xargs grep "${SEARCH_TERM}" > result4.txt
# What about find and looking for the string inside of a specific type of content?
find "${DIRECTORY}" -type f -name "*.xml" ! -name "*.css" -print | xargs grep "${SEARCH_TERM}" > result5.txt
# Can this also be achieved with wildcards and subshell?
grep "${SEARCH_TERM}" $(ls -R "${DIRECTORY}"*.{html,txt}) > result6.txt
RES=$?
if [ ${RES} -eq 0 ]; then
echo "We found results!"
else
echo "It broke - it shouldn't happen (Packt is everywhere)!"
fi
# Or for bonus points - a personal favorite
history | grep "ls" # This is really handy to find commands you ran yesterday!
# Aaaannnd the lesson is:
echo "We can do a lot with grep!"
exit 0
注意脚本中使用的~/* ?。这指的是我们的主目录,并引入了*通配符,允许我们指定从此位置开始的任何内容。本章后面会详细介绍通配符和正则表达式的概念。
- 如果你保持在主目录(
~/)并运行该脚本,输出应该类似于以下内容:
$ bash search.sh; ls -lah result*.txt
We found results!
We can do a lot with grep!
-rw-rw-r-- 1 rbrash rbrash 0 Nov 14 14:33 result1.txt
-rw-rw-r-- 1 rbrash rbrash 1.2M Nov 14 14:33 result2.txt
-rw-rw-r-- 1 rbrash rbrash 1.2M Nov 14 14:33 result3.txt
-rw-rw-r-- 1 rbrash rbrash 1.2M Nov 14 14:33 result4.txt
-rw-rw-r-- 1 rbrash rbrash 33 Nov 14 14:33 result5.txt
-rw-rw-r-- 1 rbrash rbrash 14K Nov 14 14:33 result6.txt
它是如何工作的...
本节有点难懂,因为我们要引入一个更广泛的话题——如何在字符串中使用正则表达式和通配符。我们已经介绍了它们,但也向你展示了,即使不使用它们,你也可以专门搜索诸如${SEARCH_TERM}或Packt之类的术语——这只是需要更多的工作和更多的语句。你能想象为每个术语(如Packt1、Packt2、Packt3等)写一个特定的grep语句吗?那可不有趣。
使用 Packt Publishing 网站作为基准数据集,我们通过grep命令在目录中搜索,目标仅限于我们的当前位置——用户的主目录。Grep是一个强大的工具,可以用来解析命令和文件的输出,使用模式、正则表达式和用户提供的参数。在这个例子中,我们并未预期会找到匹配Packt的字符串,因为www.packtpub.com和www.Packtpub.com并不相同。因此,result1.txt是一个空文件。
grep和许多其他工具都可以区分大小写。要使用不区分大小写的grep,请使用-i标志。
在第二次使用grep时,我们使用了递归标志(-r)并找到了许多匹配项。默认情况下,grep会返回匹配项的路径(包括文件名)和匹配所在的行。如果你想找到行号,也可以使用标志(-n)。
在第三个示例中,我们演示了如何使用多个用户提供的参数来运行grep:
$ grep -e "Packt" -e "Publishing" -r ~/www.packtpub.com/
在这个示例中,我们正在使用暴力破解机制进行搜索,这意味着我们将完全依靠我们的力量找到所有内容。当对大量数据进行搜索时,甚至是在PacktPublishing网站上执行像搜索这样看似简单的操作时,更先进和更有针对性的算法能更高效、快速地帮你找到你想要的内容,而不是像我们现在这样做!
在第四个和第五个执行示例中,我们使用了find命令。我们还将其与管道和xargs命令结合使用。单独使用find是一个非常强大的 CLI 工具,可以用来执行搜索功能(因此,如果使用不当或恶意使用,也可能造成损害):
$ find "${DIRECTORY}" -type f -print | xargs grep "${SEARCH_TERM}" > result4.txt
在前面的find命令中,我们使用了-type f,这意味着我们仅在${DIRECTORY}中查找文件。然后,我们将结果通过管道传输到xargs命令,并与 grep 一起使用。等等!什么是 xargs!?Xargs是一个常用于与管道配合使用的命令,用于将换行符(回车)数据传递给另一个命令。例如,如果我们运行ls -l(带长格式标志),结果将像这样返回(我们添加了不可见的换行符或\n来说明):
$ ls -l
drwxr-xr-x 7 rbrash rbrash 4096 Nov 13 21:48 Desktop\n
drwxr-xr-x 2 rbrash rbrash 4096 Feb 11 2017 Documents\n
drwxr-xr-x 7 rbrash rbrash 32768 Nov 14 10:54 Downloads\n
-rw-r--r-- 1 rbrash rbrash 8980 Feb 11 2017 examples.desktop\n
...
如果我们将结果直接通过管道传递给另一个期望输入的命令,就会出错!
$ someProgram Desktop\n Documents\n Downloads\n ...
相反,someProgram要求输入值用空格分隔,而不是换行符:
$ someProgram Desktop Documents Downloads ...
这就是你使用xargs的原因:去除或转换换行符,避免出现问题。
回到第二个 find 命令的例子,你可以看到我们使用了 -name 和 ! -name 参数。-name 很简单;我们在寻找一个具有特定用户提供名称的文件。在第二个 ! -name 实例中,! 表示没有或 不 包含这个名称。这就是所谓的 反向逻辑。
我们还在与 grep 的第一次示例中使用了 * 通配符,但这次在不同的上下文中使用它(稍后会进一步讨论)。这次,我们用 * 来匹配文件扩展名前的任何内容(*.xml 或 *.css)。它甚至可以这样使用:
$ ls -l /etc/*/*.txt
-rw-r--r-- 1 root root 17394 Nov 10 2015 /etc/X11/rgb.txt
在以下 grep 命令中,我们使用内联子 Shell 执行 ls 命令,配合通配符。然后,我们通过将 ${RES} 设置为 $? 来获取结果。$? 是一个特殊变量,用来获取返回码。通过 ${RES} 中的值,我们现在可以在找到结果时提供一些条件逻辑,并适当地 echo:
We found results!
就在我们退出 Shell 之前,我们想给大家一个额外的提示:你可以使用 history 命令和 grep 来搜索你过去执行过的命令。
使用通配符和正则表达式
如我们在上一节中所看到的,出现了递归函数的新概念和通配符的引入。本节将基于这些基本的原语,通过使用正则表达式和通配符扩展更高级的搜索方法。
本节还将通过一系列内置的 Bash 特性以及一些单行命令(巧妙的小技巧)来增强我们的搜索。简而言之:
-
一个通配符可以是:
*,{*.ooh,*.ahh},/home/*/path/*.txt,[0-10],[!a],?,[a,p] m -
一个正则表达式可以是:
$,^,*,[],[!],|(使用时要小心转义此符号)
通配符匹配 基本上指的是一个计算机术语,可以用通俗的语言简单描述为 扩展模式匹配。通配符是用来描述模式的 符号,而 正则表达式 是 regular expression 的简称,表示用来描述要匹配数据序列的模式。
Bash 中的通配符匹配非常强大,但可能不是执行更复杂或精细模式匹配的最佳场所。在这些情况下,Python 或其他语言/工具可能更合适。
正如我们可以想象的,通配符和模式匹配非常有用,但并不是所有工具或应用程序都能使用它们。不过,通常它们可以在命令行中与诸如 grep 等工具一起使用。例如:
$ ls -l | grep '[[:lower:]][[:digit:]]' # Notice no result
$ touch z0.test
$ touch a1.test
$ touch A2.test
$ ls -l | grep '[[:lower:]][[:digit:]]'
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 11:31 z0.test
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 11:31 a1.test
使用 ls 命令并将其通过管道传递给带有正则表达式的 grep,我们可以看到在使用 touch 创建了三个文件并重新运行命令后,正则表达式使我们能够正确地过滤出以小写字母开头并紧接着一个数字的文件。
如果我们想进一步增强 grep(或其他命令),我们可以使用以下任意一种方法:
-
[:alpha:]:字母字符(不区分大小写) -
[:lower:]:小写可打印字符 -
[:upper:]:大写可打印字符 -
[:digit:]:十进制数字 0 到 9 -
[:alnum:]:字母数字字符(所有数字和字母字符) -
[:space:]:空白字符,表示空格、制表符和换行符 -
[:graph:]:可打印字符,不包括空格 -
[:print:]:可打印字符,包括空格 -
[:punct:]:标点符号(例如,句号) -
[:cntrl:]:控制字符(如使用Ctrl + C时生成的不可打印字符) -
[:xdigit:]:十六进制字符
准备开始
除了打开终端(并且如果需要的话,还可以打开你喜欢的文本编辑器),我们只需要几个核心命令:grep、tr、cut、**和touch。我们假设在前一步中我们爬取的www.packtpub.com目录仍然可用:
$ cd ~/
$ touch {a..c}.test
$ touch {A..C}[0..2].test2
$ touch Z9.test3 Z9\,test2 Z9..test2
$ touch ~/Desktop/Test.pdf
如何操作……
让我们开始吧:
-
打开终端,并选择一个你喜欢的编辑器来创建一个新的脚本。
-
在你的脚本中,添加以下内容:
#!/bin/bash
STR1='123 is a number, ABC is alphabetic & aBC123 is alphanumeric.'
echo "-------------------------------------------------"
# Want to find all of the files beginning with an uppercase character and end with .pdf?
ls * | grep [[:upper:]]*.pdf
echo "-------------------------------------------------"
# Just all of the directories in your current directory?
ls -l [[:upper:]]*
echo "-------------------------------------------------"
# How about all of the files we created with an expansion using the { } brackets?
ls [:lower:].test .
echo "-------------------------------------------------"
# Files with a specific extension OR two?
echo ${STR1} > test.txt
ls *.{test,txt}
echo "-------------------------------------------------"
# How about looking for specific punctuation and output on the same line
echo "${STR1}" | grep -o [[:punct:]] | xargs echo
echo "-------------------------------------------------"
# How about using groups and single character wildcards (only 5 results)
ls | grep -E "([[:upper:]])([[:digit:]])?.test?" | tail -n 5
exit 0
-
现在,执行脚本,你的控制台应该会被输出结果淹没。最重要的是,让我们看看最后五个结果。注意到结果中有
Z9(,)和Z9.test(3)吗?这就是正则表达式的强大作用!好了,我们已经知道可以使用变量创建并搜索一堆文件夹或文件了,但是我能否使用正则表达式来查找像变量参数这样的东西呢?当然可以!请看下一步。 -
在控制台中,尝试以下命令:
$ grep -oP 'name="\K.*?(?=")' www.packtpub.com/index.html
- 再次在控制台中,尝试以下命令:
$ grep -P 'name=' www.packtpub.com/index.html
- 在查找可能跨越多行的 IF 实例时,使用
tr等命令来删除换行符,我们能做得更好吗?
$ tr '\n' ' ' < www.packtpub.com/index.html | grep -o '<title>.*</title>'
- 现在,让我们使用
cut来清理屏幕上的一些杂乱信息,作为收尾。通常,控制台的宽度是80个字符,因此让我们添加行号并修剪grep的输出:
$ grep -nP 'name=' www.packtpub.com/index.html | cut -c -80
整本书都在讲解如何使用正则表达式解析数据,但关键点是,正则表达式并不总是最适合的选择,特别是在性能和像 HTML 这样的标记语言中。例如,在解析 HTML 时,最好使用一个能够理解该语言及其特定语法的解析器。
它是如何工作的……
正如你可能已经猜到的,没有正则表达式和通配符的情况下,通过大量数据进行查询,对于没有经验的人来说可能是一场噩梦。更可怕的情况可能发生在你的表达式没有使用正确的术语或有效(且准确)表达式的情况下。然而,通配符在命令行中非常有用,尤其是在你需要拼接字符串、快速查找数据和寻找文件时。有时候,如果我仅仅是想找到特定发生位置的文件名和大致位置/行号,搜索结果的可用性可能并不重要。例如,这个 CSS 类在哪个文件里?
好吧,你已经通过脚本并执行了多个命令,了解了如何在表面上使用正则表达式和通配符。让我们回过头来,走一遍这个过程。
在第 1 步中,我们打开了控制台,创建了一个简单的脚本并执行。然后,输出结果显示在控制台上:
$ bash test.sh
-------------------------------------------------
Linux-Journal-2017-08.pdf
Linux-Journal-2017-09.pdf
Linux-Journal-2017-10.pdf
Test.pdf
-------------------------------------------------
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 A0.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 A1.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 A2.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 B0.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 B1.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 B2.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 C0.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 C1.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 C2.test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 Z9,test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 Z9..test2
-rw-rw-r-- 1 rbrash rbrash 0 Nov 15 22:13 Z9.test3
Desktop:
total 20428
drwxrwxr-x 2 rbrash rbrash 4096 Nov 15 12:55 book
# Lots of files here too
Documents:
total 0
Downloads:
total 552776
-rw------- 1 root root 1024 Feb 11 2017 ~
... # I have a lot of files for this book
Music:
total 0
Pictures:
total 2056
drwxrwxr-x 2 rbrash rbrash 4096 Sep 6 21:56 backgrounds
Public:
total 0
Templates:
total 0
Videos:
total 4
drwxrwxr-x 13 rbrash rbrash 4096 Aug 11 10:42 movies
-------------------------------------------------
a.test b.test c.test
-------------------------------------------------
a.test b.test c.test test.txt
-------------------------------------------------
, & .
-------------------------------------------------
C0.test2
C1.test2
C2.test2
Z9,test2
Z9.test3
这可能会更可怕!对吧?在第一行中,我们开始查找以大写字母开头的 PDF 文件。命令ls * | grep [[:upper:]]*.pdf使用了带有*通配符(表示所有文件)的ls命令,然后将输出传递给grep,并使用一个简单的正则表达式。这个正则表达式是[[:upper:]],后面跟着另一个*通配符和.pdf字符串的组合。这将返回我们的搜索结果,最少会包含Test.pdf(我的结果也返回了一个流行的 Linux 期刊的 PDF 文件)。
然后,我们几乎进行相同的搜索,使用ls -l [[:upper:]]*,但使用带正则表达式的ls命令会返回大量数据(如果所有文件夹都有内容的话)。它从脚本所在的当前目录开始,然后深入到一个目录并打印内容。一个很好的特性是使用-l标志,它会产生详细的结果并打印目录的大小(以字节为单位)。
接下来,我们使用ls命令查找所有以小写字母开头并以.test扩展名结尾的文件。你可能没注意到,当你设置这个命令时,你也看到了通配符和扩展的实际应用:touch {a..c}.test。touch命令创建了三个文件:a.test、b.test和c.test。带有这个简单正则表达式的ls命令返回了这三个文件的名称。
再次,我们使用带有(*)通配符和扩展括号的ls命令来匹配文件扩展名:ls *.{test,txt}。它会查找任意名称(*)的文件,这些文件会与一个句点(.)连接,后面跟着test或txt扩展名。
接下来,在第 7 步中,我们结合了通过管道、grep、xargs和正则表达式学到的一些内容,命令是:echo "${STR1}" | grep -o [[:punct:]] | xargs echo。由于grep的输出是以\n(换行符)分隔的形式(每个找到的实例一个新行),这会打破我们希望将所有值按这种形式回显到控制台的意图,因此我们需要xargs来修复输出,以便echo可以正确使用这些参数。例如,echo "item1\n item2\n item3\n"是无法工作的,但使用xargs后,它看起来像这样:echo "item1" "item2" "item3"。
在最后的命令中,我们终于达到了一个更疯狂的正则表达式,实际上它其实相当简单:ls | grep -E "([[:upper:]])([[:digit:]])?.test?." | tail -n 5。它引入了一些概念,包括组(括号)、(?)通配符,以及如何组合多个表达式组件,并使用tail命令。
使用grep、-E(表达式标志)和两个组(括号内的表达式),我们可以将它们与?正则操作符组合使用。这个操作符是用于表示单个字符的通配符:
C0.test2
C1.test2
C2.test2
Z9,test2
Z9.test3
我们可以看到,最后五个结果被返回,且第一个字母是大写,后跟一个数字,一个字符(可能是.或,),然后是“test”这个词和一个数字。我们创建了一个名为Z9..test2的测试文件。注意它没有包含在列表项中吗?这是因为我们没有使用这样的表达式:
$ ls | grep -E "([[:upper:]])([[:digit:]])?.?.test?"
在第 4 步中,我们使用grep和-oP标志运行了一个特定的正则表达式,grep -oP 'name="\K.*?(?=")' www.packtpub.com/index.html,对我们最近抓取的www.packtpub.com的档案进行了处理。-o标志表示仅输出匹配的值,-P用于使用 Perl 正则表达式。
注意那些被双引号包围的值吗?它是在寻找与模式name="anythingGoesHere"匹配的任何内容。单独来看,这并不特别有用,但它说明了快速提取值的一个方法(例如,如果name非常具体,你可以把name=改成其他值,得到完全相同的结果!)。
在相同的上下文中,在第 5 步中,我们也可以找到所有name=的出现:grep -P 'name=' www.packtpub.com/index.html。这种类型的命令对于理解信息的上下文或仅仅了解其存在非常有用;这回到我们寻找 CSS、C/C++以及其他数据/源文件中的值的概念。
进入第 6 步,我们需要寻找标题的 HTML 标签。通常情况下,你应该使用专门的 HTML 解析器,但如果我们想快速用 grep 结合正则表达式来处理——是完全可以的!tr '\n' ' ' < www.packtpub.com/index.html | grep -o '<title>.*</title>' 命令使用了转换函数(tr),将\n或换行符特殊字符转换为空格。这在数据标记可能跨越多行时非常有用。
在最后的步骤中,我们将进行一些细化操作,以执行广泛的搜索。我们只需使用grep来提供行号和文件名。使用cut,我们可以修剪控制台输出中的剩余字符(这非常有用):
$ grep -nHP 'name=' www.packtpub.com/index.html | cut -c -80
正则表达式也可以通过许多在线正则模拟器来测试!一个常见且免费的在线工具是:regexr.com/。
别忘了,某些正则表达式功能也允许你在组内嵌套命令!我们没有演示这一功能,但它在某些使用场景中是有效的,并且结果是可以接受的!
脚本中的数学和计算
在经历了繁琐的通配符和正则表达式介绍后,我们将进入在控制台上执行一些基本数学运算的部分。如果你还没尝试过,运行类似以下命令会发生什么呢?它看起来是这样的:
$ 1*5
1*5: command not found
命令未找到?当然,我们知道计算机能够进行数学运算,但显然 Bash 无法以这种方式解释数学运算。我们必须确保 Bash 能够通过以下方法正确解释这些操作:
-
expr命令(过时) -
bc命令 -
POSIX Bash shell 扩展
-
另一个语言/程序来做这些脏活
让我们再试一次,但使用 POSIX Bash shell 扩展:
$ echo $((1*5))
5
我们得到了期望的答案 5,但它错在哪里呢?它出错的地方在于使用除法和浮点数,因为 Bash 主要处理整数:
$ echo $((1/5))
0
没错,1 除以 5 是 0,但缺少了一个余数!这就是为什么我们可能依赖其他方法来进行简单的数学运算。
使用方程和 math 在脚本中的众多用途之一是确定文件系统分区的大小。你能想象如果磁盘空间过满会发生什么吗?或者如果某个目录达到预定大小,我们可能想要自动归档它?当然,这只是理论上的,但如果我们让文件系统悄悄地变满,确实可能会发生故障!
以下这个方案是关于确定一个 tarball(及其内容)大小、目标分区剩余空间以及操作是否可以继续或取消的。
准备工作
这个方案将考虑一些有趣的因素:
-
Bash 并非无所不能
-
还有其他工具(例如,
bc) -
我们可以用其他语言,比如 C,自己创建一个
-
创建一个 tarball
有时,在一些小型嵌入式系统上,Python 可能不可用,但 Bash(或其近亲)和 C 是可用的。这时,能够在没有额外程序(可能不可用)的情况下进行数学运算就显得非常方便!
我们将使用以下命令来确保所有必要的工具已经安装,以便进行此次实验:
$ sudo apt-get install -y bc tar
现在,我们需要创建一个名为 archive.tar.gz 的 tarball:
$ dd if=/dev/zero of=empty.bin bs=1k count=10000
$ tar -zcvf archive.tar.gz empty.bin
$ rm empty.bin
我们意识到,创建/编译一个不是用 Bash 编写的简单程序的目的是超出了本书的范围,但这确实是一个有用的技能。为了做到这一点,我们需要安装 GCC,它是 GNU 编译器集合的简称。听起来可能很复杂,但我们向你保证,我们已经做了所有的艰苦工作:
$ sudo apt-get install -y gcc
上面的命令安装了编译器,现在我们需要 C 源代码(以便编译一个简单的 C 程序)。打开控制台并通过以下命令获取代码:
$ wget https://raw.githubusercontent.com/PacktPublishing/Bash-Cookbook/master/chapter%2002/mhelper.c
这段代码也可以在 Github 上找到,链接:github.com/PacktPublishing/Bash-Cookbook。
要编译这段代码,我们将使用 gcc 和 -lm(指的是 libmath),如下所示:
$ gcc -Wall -02 -o mhelper main.c -lmath
如果编译器成功完成(应该是的),你将得到一个名为 mhelper(或数学助手)的工具二进制文件。我们也可以通过使用 sudo 和 cp 将它复制到 /bin 来将其添加到本地命令列表中:
$ sudo cp mhelper /bin; sudo chmod a+x /bin/mhelper;
现在,mhelper 可以用来进行基本的运算,如除法、乘法、加法和减法:
$ mhelper "var1" "-" "var2"
mhelper 代码并不是为了特别强大且能处理特定边缘情况而设计的,而是为了演示另一个工具的使用。Python 和 numpy 会是很好的替代品!
如何操作...
使用 mhelper 二进制文件、bc 和其他表达式,我们可以开始这道菜谱:
- 首先,打开终端和编辑器,创建一个新的脚本文件
mathexp.sh,并在其中添加以下内容:
#!/bin/bash
# Retrieve file system information and remove header
TARBALL="archive.tar.gz"
CURRENT_PART_ALL=$(df --output=size,avail,used /home -B 1M | tail -n1)
# Iterate through and fill array
IFS=' ' read -r -a array <<< $CURRENT_PART_ALL
# Retrieve the size of the contents of the tarball
COMPRESSED_SZ=$(tar tzvf "${TARBALL}" | sed 's/ \+/ /g' | cut -f3 -d' ' | sed '2,$s/^/+ /' | paste -sd' ' | bc)
echo "First inspection - is there enough space?"
if [ ${array[1]} -lt ${COMPRESSED_SZ} ]; then
echo "There is not enough space to decompress the binary"
exit 1
else
echo "Seems we have enough space on first inspection - continuing"
VAR=$((${array[0]} - ${array[2]}))
echo "Space left: ${VAR}"
fi
echo "Safety check - do we have at least double the space?"
CHECK=$((${array[1]}*2))
echo "Double - good question? $CHECK"
# Notice the use of the bc command?
RES=$(echo "$(mhelper "${array[1]}" "/" "2")>0" | bc -l)
if [[ "${RES}" == "1" ]]; then
echo "Sppppppaaaaccee (imagine zombies)"
fi
# We know that this will break! (Bash is driven by integers)
# if [ $(mhelper "${array[2]}" "/" "2") -gt 0 ]; then
#~ echo "Sppppppaaaaccee (imagine zombies) - syntax error"
# fi
# What if we tried this with Bash and a concept again referring to floats
# e.g., 0.5
# It would break
# if [ $((${array[2]} * 0.5 )) -gt 0 ]; then
# echo "Sppppppaaaaccee (imagine zombies) - syntax error"
# fi
# Then untar
tar xzvf ${TARBALL}
RES=$?
if [ ${RES} -ne 0 ]; then
echo "Error decompressing the tarball!"
exit 1
fi
echo "Decompressing tarball complete!"
exit 0
- 现在,运行脚本应该会产生类似于以下的输出:
First inspection - is there enough space?
Seems we have enough space on first inspection - continuing
Space left: 264559
Safety check - do we have at least double the space?
Double - good question ? 378458
Sppppppaaaaccee (imagine zombies)
empty.bin
Decompressing tarball complete!
很棒!所以我们可以使用 Bash 大小计算,bc 命令和我们的二进制。如果你想计算一个圆的半径(这肯定会得到一个浮动值)或者一个百分比,例如,你将需要注意 Bash 中这个限制。
最后值得注意的是,expr 命令仍然存在,但它已经被弃用。建议在新的脚本中使用 $(( 你的方程式 )) 作为首选方法。
使用相同的前提条件,利用 mhelper 二进制文件或 $((..)) 脚本,你还可以计算需要变量输出的情况的百分比(例如,不是整数的情况)。例如,在计算基于百分比的屏幕尺寸时,虽然你会期望得到一个整数,你也可以在计算后进行四舍五入。
它是如何工作的……
首先,正如这个教程所暗示的那样——我们注意到 Bash shell 不喜欢带有小数点或甚至非整数的十进制数字。等等,数学!?不幸的是,我们无法隐藏所有细节,但在编程中,有几个概念是你应该了解的:
-
有符号和无符号数字
-
浮点数、双精度数和整数
第一个概念相当简单——当前的计算机是二进制的,这意味着它们使用零(0)和一(1)进行计算。这意味着它们的运算是基于 2^ 的幂次进行的。在不深入基础计算机科学课程的情况下,如果你看到一个数据类型是 int(整数),并且它是一个 32 位数字,那么如果它从 0 开始,最大值是十进制下的 4,294,967,295(2³²)。这做出了一个关键假设,那就是所有数字(包括 0)都是正数。这个正负特性叫做 符号!如果数据类型提到有符号或无符号——现在你知道它是什么意思了!
然而,是否带符号会有一个后果,那就是最大正数或负数的值会减少,因为一个比特用于表示符号。一个有符号的 32 位 int(也可以称为 int32)现在的范围是 (-)2,147,483,647 到 (+)2,147,483,647。
作为作者的说明,我意识到一些计算机科学的定义并不完全符合计算机科学的标准,意思是我稍微调整了它们的含义,以确保在 大多数 一般情况下关键点能清晰传达。
另一方面,Bash 只使用整数,可能你已经看到,当你将一个值像 1/5 进行除法时,结果是 0。没错,它无法整除,但结果是 0.20(作为一个小数)。我们也不能对带有小数点的数字进行乘法运算!因此,我们必须使用其他程序,比如 bc 或 mhelper。
如果你对计算机有兴趣,你也知道有浮点数、双精度数和其他数据类型来表示数字。Mhelper和bc可以帮助你处理这些类型的数字,当整数的概念无法使用时(例如,除法得到的数字不是整数时)。
-
回到步骤 1:
-
我们创建了一个脚本,它将检查
/home目录,使用df命令确定有多少可用空间。通过使用tail,另一个可以用来减少输出的命令,我们跳过了输出的第一行,并将所有输出通过管道传入$CURRENT_PART_ALL变量(或所有当前分区信息)。 -
然后,
$CURRENT_PART_ALL变量的内容通过read命令读入一个数组。注意错误重定向的使用:<<<。这被称为her****e-string,简单来说就是展开变量并将其传递到`stdin.`。 -
现在,
/home分区的存储信息已经在一个数组中,我们有一个tarball(或压缩并包含内部内容的文件),我们需要知道tarball内部内容的大小。为此,我们使用一个冗长的命令,通过多个管道命令来获取包含元素的大小,并将其传递给bc命令。 -
在确定我们归档文件中包含的元素大小后,我们将计算出的大小与剩余可用空间进行验证。这个值位于
array element[1]中。如果可用空间小于等于提取的文件,则退出。否则,执行后打印剩余空间。 -
为了好玩,我们结合了分叉子壳程序以获取
mhelper的除法结果,这些结果通过bc管道传递。这样,我们就能确定是否有足够的空间,并以boolean值为真(1)或假(0)表示。 -
由于我们假设有足够的空间,我们解压(解压并提取内容)
$TARBALL。如果tar命令返回的值不等于0,则退出并报错。否则,退出并表示成功。
-
-
执行脚本后,tarball(
empty.bin)的内容应该出现在当前工作目录中。
在脚本中,我们在注释中加入了两个不同的评估,它们会返回浮动值或语法错误。我们加入它们是为了提醒你并帮助强化主要的教学内容。
我们错过了什么吗?当然有!我们从未检查过 tarball 本身的大小,也没有确保它的大小包括在执行检查以确定剩余可用空间时的使用空间中。进行和强制大小限制时,一定要小心!
仅使用 Bash 进行字符串的剥离/修改/排序/删除/查找
到目前为止,我们已经看到 Linux 中可用命令的强大之处,其中一些命令是最强大的:sed 和 grep。然而,尽管我们可以轻松地将这些命令一起使用,仅使用 sed 或者使用另一个非常有用的命令 awk,我们也可以利用 Bash 本身来节省时间并减少外部依赖,从而实现可移植的解决方案!
那么,我们该怎么做呢?让我们从几个使用 Bash 语法的例子开始:
#!/bin/bash
# Index zero of VARIABLE is the char 'M' & is 14 bytes long
VARIABLE="My test string"
# ${VARIABLE:startingPosition:optionalLength}
echo ${VARIABLE:3:4}
在前面的例子中,我们可以看到一种特殊的调用子字符串功能的方式,使用 ${...},其中 VARIABLE 是脚本中的字符串变量(甚至是全局变量),接下来的变量是 :。在 : 后面是 startingPosition 参数(记住字符串就是字符数组,每个字符可以通过索引访问),并且还有一个可选的分号和长度参数(optionalLength)。
如果我们运行这个脚本,输出会是:
$ bash script.sh
test
你可能会问,这怎么可能呢?嗯,这可以通过 Bash 中等效的 substr(C 语言和许多其他编程语言中的函数)来实现,具体通过使用 ${...} 语法。这告诉 bash 查找名为 VARIABLE 的变量,并获取两个参数:从字节/字符 3(技术上是 4,因为在 Bash 中数组是从元素 0 开始的)开始,长度为 4(只打印四个字符)。echo 的结果是 test。
我们可以做更多吗?比如去除最后一个字符?删除单词?搜索?当然可以,所有这些内容都会在本教程中讲解!
准备开始
通过创建一些模拟常见日常问题的数据集来准备练习:
$ rm -rf testdata; mkdir -p testdata
$ echo "Bob, Jane, Naz, Sue, Max, Tom$" > testdata/garbage.csv
$ echo "Zero, Alpha, Beta, Gama, Delta, Foxtrot#" >> testdata/garbage.csv
$ echo "1000,Bob,Green,Dec,1,1967" > testdata/employees.csv
$ echo "2000,Ron,Brash,Jan,20,1987" >> testdata/employees.csv
$ echo "3000,James,Fairview,Jul,15,1992" >> testdata/employees.csv
使用这两个 CSV 文件,我们将:
-
去掉
garbage.csv前两行的多余空格 -
删除
garbage.csv中每行的最后一个字符 -
将
garbage.csv中前两行的每个字符转换为大写 -
将
employees.csv中的Bob替换为Robert -
在
employees.csv每行的开头插入一个# -
删除
employees.csv中每行的精确出生日期列/字段
如何做到...
让我们开始我们的活动:
- 打开一个新的终端,并用你喜欢的编辑器打开一个新文件。将以下内容添加到新脚本中,并将其保存为
builtin-str.sh:
#!/bin/bash
# Let's play with variable arrays first using Bash's equivalent of substr
STR="1234567890asdfghjkl"
echo "first character ${STR:0:1}"
echo "first three characters ${STR:0:3}"
echo "third character onwards ${STR: 3}"
echo "forth to sixth character ${STR: 3: 3}"
echo "last character ${STR: -1}"
# Next, can we compare the alphabeticalness of strings?
STR2="abc"
STR3="bcd"
STR4="Bcd"
if [[ $STR2 < $STR3 ]]; then
echo "STR2 is less than STR3"
else
echo "STR3 is greater than STR2"
fi
# Does case have an effect? Yes, b is less than B
if [[ $STR3 < $STR4 ]]; then
echo "STR3 is less than STR4"
else
echo "STR4 is greater than STR3"
fi
-
执行脚本
bash builtin-str.sh并注意我们如何从字符串中剥离最后一个字符,甚至比较字符串。 -
再次,打开一个名为
builtin-strng.sh的新文件,并将以下内容添加到其中:
#!/bin/bash
GB_CSV="testdata/garbage.csv"
EM_CSV="testdata/employees.csv"
# Let's strip the garbage out of the last lines in the CSV called garbage.csv
# Notice the forloop; there is a caveat
set IFS=,
set oldIFS = $IFS
readarray -t ARR < ${GB_CSV}
# How many rows do we have?
ARRY_ELEM=${#ARR[@]}
echo "We have ${ARRY_ELEM} rows in ${GB_CSV}"
让我们去除垃圾—去掉空格:
INC=0
for i in "${ARR[@]}"for i in "${ARR[@]}"
do
:
res="${i//[^ ]}"
TMP_CNT="${#res}"
while [ ${TMP_CNT} -gt 0 ]; do
i=${i/, /,}
TMP_CNT=$[$TMP_CNT-1]
done
ARR[$INC]=$i
INC=$[$INC+1]
done
让我们删除每行的最后一个字符:
INC=0
for i in "${ARR[@]}"
do
:
ARR[$INC]=${i::-1}
INC=$[$INC+1]
done
现在,让我们将所有字符都转换为大写!
INC=0for i in "${ARR[@]}"
do
:
ARR[$INC]=${i^^}
printf "%s" "${ARR[$INC]}"
INC=$[$INC+1]
echo
done
# In employees.csv update the first field to be prepended with a # character
set IFS=,
set oldIFS = $IFS
readarray -t ARR < ${EM_CSV}
# How many rows do we have?
ARRY_ELEM=${#ARR[@]}
echo;echo "We have ${ARRY_ELEM} rows in ${EM_CSV}"
# Let's add a # at the start of each line
INC=0
for i in "${ARR[@]}"
do
:
ARR[$INC]="#${i}"
printf "%s" "${ARR[$INC]}"
INC=$[$INC+1]
echo
done
# Bob had a name change, he wants to go by the name Robert - replace it!
echo
echo "Let's make Bob, Robert!"
INC=0
for i in "${ARR[@]}"
do
:
# We need to iterate through Bobs first
ARR[$INC]=${i/Bob/Robert}
printf "%s" "${ARR[$INC]}"
INC=$[$INC+1]
echo
done
我们将删除 birth 列中的日期。要删除的字段是 5(但实际上是 -4):
echo;
echo "Lets remove the column: birthday (1-31)"
INC=0
COLUM_TO_REM=4
for i in "${ARR[@]}"
do
:
# Prepare to also parse the ARR element into another ARR for
# string manipulation
TMP_CNT=0
STR=""
IFS=',' read -ra ELEM_ARR <<< "$i"
for field in "${ELEM_ARR[@]}"
do
# Notice the multiple argument in an if statement
# AND that we catch the start of it once
if [ $TMP_CNT -ne 0 ] && [ $TMP_CNT -ne $COLUM_TO_REM ]; then
STR="${STR},${field}"
elif [ $TMP_CNT -eq 0 ]
then
STR="${STR}${field}"
fi
TMP_CNT=$[$TMP_CNT+1]
done
ARR[$INC]=$STR
echo "${ARR[$INC]}"
INC=$[$INC+1]
done
- 执行脚本
bash builtin-strng.sh并查看输出。
你有没有注意到所有重定向输入或输出的机会?想象一下这些可能性!此外,之前的许多脚本可以改用另一个工具 AWK 来执行。
它是如何工作的...
这份食谱有些迭代性,但它应该反复强调(请原谅这个双关语)来展示 Bash 内置了相当多的功能来操作字符串或任何结构化数据。然而,有一个基本假设,那就是许多操作系统使用 C 语言编写程序。
-
字符串是字符的数组。
-
像
,这样的字符和其他任何字符一样。 -
因此,我们可以评估或测试某个字符的存在,用于分隔行中的字段,甚至用它来构建数组。
现在,回顾一下这份食谱中的步骤:
- 运行脚本后,我们在控制台上得到了以下输出:
$ builtin-str.sh
first character 1
first three characters 123
third character onwards 4567890asdfghjkl
forth to sixth character 456
last character l
STR2 is less than STR3
STR4 is greater than STR3
我们从字符串STR="1234567890asdfghjkl"开始,当脚本在第一步运行时:
-
在第一步中,我们从位置零(0)开始打印出一个字符。记住,这是一个数组,位置
0是起始元素。 -
接下来,我们提取了前三个字符,得到:
123。 -
然而,如果我们想要获取位置 3 之后的所有字符怎么办?我们可以使用
${STR: 3}而不是${STR: 0-3}。 -
然后,基于之前的内容,如果我们想要获取位置 4 的字符(数组中的第四个元素,但由于从零(0)开始计数,因此这是位置三(3)),我们使用
${STR: 3: 3}。 -
最后,要仅获取最后一个字符,我们可以使用
${STR:-1}。
为了完成食谱中的第一个脚本,我们还需要三个字符串。如果我们希望将它们彼此比较,可以使用条件逻辑来实现。记住,bcd 小于 BCD。
使用简单的 Bash 构造来比较字符串非常有用,当你想写一个脚本来快速比较文件名以指定执行顺序时。例如,先运行001-test.sh脚本,再运行002-test.sh。
- 在这部分食谱中,我们从一个冗长的脚本开始,以易于解释的方式进行复制。我们涵盖了在不使用 AWK 和 SED 的情况下,你可以在 Bash shell 中使用的一些技巧:
$ ./builtin-strng.sh
We have 2 rows in testdata/garbage.csv
BOB,JANE,NAZ,SUE,MAX,TOM
ZERO,ALPHA,BETA,GAMA,DELTA,FOXTROT
We have 3 rows in testdata/employees.csv
#1000,Bob,Green,Dec,1,1967
#2000,Ron,Brash,Jan,20,1987
#3000,James,Fairview,Jul,15,1992
Let's make Bob, Robert!
#1000,Robert,Green,Dec,1,1967
#2000,Ron,Brash,Jan,20,1987
#3000,James,Fairview,Jul,15,1992
Lets remove the column: birthday (1-31)
#1000,Robert,Green,Dec,1967
#2000,Ron,Brash,Jan,1987
#3000,James,Fairview,Jul,1992
这是脚本的详细解析,但在此之前需要简要介绍数组、readarray、IFS和oldIFS。这个练习的重点不是深入讲解数组(这将在后面讨论),而是要知道你可以自动使用它们来创建动态列表,例如文件或文件中的行。它们通过${ARR[@]}符号引用,且每个元素可以通过其索引值在方括号[...]中引用。
readarray命令使用IFS和oldIFS变量将输入解析成数组。它根据一个共同的分隔符(IFS)分隔数据,如果这些值被修改,oldIFS 可以保持原值:
-
在第一步中,我们使用 read 读取
garbage.csv(${GB_CSV}),然后使用${#ARR[@]}来获取数组中元素的数量。我们不使用这个值,但值得注意的是文件的结构以及它是否被正确读取。然后,对于数组中的每个成员,我们通过计算空格的数量来移除空格,再通过额外的 while 循环,执行${i/, /,}直到完成。修正后的值然后重新插入到数组中。 -
在下一步中,我们使用
${i::-1}和 for 循环来删除每行的最后一个字符。然后,将结果重新插入到数组中。 -
使用
for循环和ARR[$INC]=${i^^},数组中的所有字符都被转为大写,我们使用printf打印数组(稍后在其他示例中会详细介绍)。 -
接下来是
employees.csv,我们再次使用readarray将其读入数组。然后,我们在每一行的开头添加一个井号(#),并将其重新插入到ARR[$INC]="#${i}"数组中。 -
然后,我们搜索子字符串
Bob并用ARR[$INC]=${i/Bob/Robert}替换它。要使用内建的查找和替换功能,我们使用以下语法:${variable/valueToFind/valueToReplaceWith}。请注意,这也是前面步骤中移除空格所用的原理。 -
最后一步稍微复杂一点,有点冗长,意味着它可以通过使用其他工具如 AWK 来缩短和执行,但为了让示例易于阅读——它写得有点像 C 程序。这里,我们要删除实际的生日值(0-31),或者说是第 5 列(如果考虑到数组从 0 开始,索引是 4)。首先,我们使用 for 循环遍历数组,然后使用 read 将输入值也作为数组!接着,对于数组
${ELEM_ARR[@]}中的每个字段,我们检查它是否不是第一个值,也不是我们希望删除的列。我们通过连接构建正确的字符串,然后将其重新插入数组,再使用echo打印每个值。
数组是一种数据结构,重要的是要考虑数据可以以多种方式进行操作。就像我们如何按行拆分文件以创建元素数组一样,我们也可以将这些元素拆分成自己的数组!
使用 SED 和 AWK 来删除/替换子字符串
同样,当我们需要删除某个恼人的字符或删除字符串中的某些部分时,我们总是可以依赖这两个强大的命令:sed和awk。虽然我们看到 Bash 确实有类似的内建功能,但完整的工具能提供相同的功能甚至更复杂的功能。那么,我们什么时候应该使用这些工具呢?
-
当我们不太关心通过使用 Bash 内建功能可能获得的速度时
-
当需要更复杂的功能时(例如需要多维数组或编辑流等编程结构)
-
当我们专注于可移植性时(Bash 可能被嵌入或是一个有限版本,并且可能需要独立工具)
已经有完整的书籍写了关于 SED 和 AWK,你可以随时在www.gnu.org/software/sed/和www.gnu.org/software/gawk/上找到更多信息。
流编辑器(SED)是一个方便的文本处理工具,非常适合一行命令,并提供一个简单的编程语言和正则表达式匹配。或者,AWK 也是强大的,可以说比 SED 更多。它提供了一个更完整的编程语言,具有各种数据结构和其他构造。但是,当处理可能包含字段或结构化数据的文件(例如 CSV)时,它更适合。然而,当使用管道(例如grep X | sed ... > file.txt)时,SED 在处理文本替换时可能更好。
准备工作
让我们通过创建一些模仿常见日常问题的数据集来为这个练习做好准备:
$ rmdir testdata; mkdir -p testdata
$ echo "Bob, Jane, Naz, Sue, Max, Tom$" > testdata/garbage.csv
$ echo "Bob, Jane, Naz, Sue, Max, Tom#" >> testdata/garbage.csv
$ echo "1000,Bob,Green,Dec,1,1967" > testdata/employees.csv
$ echo" 2000,Ron,Brash,Jan,20,1987" >> testdata/employees.csv
$ echo "3000,James,Fairview,Jul,15,1992" >> testdata/employees.csv
使用这两个 CSV 文件,我们将要:
-
在
garbage.csv的前两行中移除额外的空格。 -
从
garbage.csv中的每行删除最后一个字符。 -
在
garbage.csv的前两行中将每个字符的大小写更改为大写。 -
在
employees.csv中用Robert替换Bob。 -
在
employees.csv的每行开头插入一个#。 -
在每行的
employees.csv中移除出生日期列/字段的确切日期。
如何做...
就像只使用 Bash 练习一样,我们将执行类似的操作如下:
创建一个名为some-strs.sh的脚本,并使用以下内容打开一个新的终端:
#!/bin/bash
STR="1234567890asdfghjkl"
echo -n "First character "; sed 's/.//2g' <<< $STR # where N = 2 (N +1)
echo -n "First three characters "; sed 's/.//4g' <<< $STR
echo -n "Third character onwards "; sed -r 's/.{3}//' <<< $STR
echo -n "Forth to sixth character "; sed -r 's/.{3}//;s/.//4g' <<< $STR
echo -n "Last character by itself "; sed 's/.*\(.$\)/\1/' <<< $STR
echo -n "Remove last character only "; sed 's/.$//' <<< $STR
执行脚本并查看结果。
创建另一个名为more-strsng.sh的脚本,然后执行它:
#!/bin/sh
GB_CSV="testdata/garbage.csv"
EM_CSV="testdata/employees.csv"
# Let's strip the garbage out of the last lines in the CSV called garbage.csv
# Notice the forloop; there is a caveat
set IFS=,
set oldIFS = $IFS
readarray -t ARR < ${GB_CSV}
# How many rows do we have?
ARRY_ELEM=${#ARR[@]}
echo "We have ${ARRY_ELEM} rows in ${GB_CSV}"
# Let's strip the garbage - remove spaces
INC=0
for i in "${ARR[@]}"
do
:
ARR[$INC]=$(echo $i | sed 's/ //g')
echo "${ARR[$INC]}"
INC=$[$INC+1]
done
# Remove the last character and make ALL upper case
INC=0
for i in "${ARR[@]}"
do
:
ARR[$INC]=$(echo $i | sed 's/.$//' | sed -e 's/.*/\U&/' )
echo "${ARR[$INC]}"
INC=$[$INC+1]
done
我们希望在每行开头添加#,并且我们还将在每个文件基础上使用sed工具。我们只需删除 Bob 并通过对文件进行操作而将其更改为 Robert:
set IFS=,
set oldIFS = $IFS
readarray -t ARR < ${EM_CSV}
INC=0
for i in "${ARR[@]}"
do
:
ARR[$INC]=$(sed -e 's/^/#/' <<< $i )
echo "${ARR[$INC]}"
INC=$[$INC+1]
done
sed -i 's/Bob/Robert/' ${EM_CSV}
sed -i 's/^/#/' ${EM_CSV} # In place, instead of on the data in the array
cat ${EM_CSV}
# Now lets remove the birthdate field from the files
# Starts to get more complex, but is done without a loop or using cut
awk 'BEGIN { FS=","; OFS="," } {$5="";gsub(",+",",",$0)}1' OFS=, ${EM_CSV}
检查结果——是否更容易获得仅使用 Bash 内置构造的配方的结果?在许多情况下可能是的,如果它们可用的话。
工作原理...
在运行本教程中的两个脚本后,我们可以看到一些项目出现(特别是如果我们比较内置的 Bash 功能用于搜索、替换和子字符串)。
- 在执行
some-strs.sh后,我们可以在控制台中看到以下输出:
$ bash ./some-strs.sh
First character 1
First three characters 123
Third character onwards 4567890asdfghjkl
Forth to sixth character 456
Last character by itself l
Remove last character only 1234567890asdfghjk
到目前为止,我们已经多次看到 echo 命令的使用,但 -n 标志意味着我们不应自动创建新的一行(或回车)。<<< 输入重定向用于将值作为字符串输入,这之前也已经使用过,因此这不会是新信息。基于这一点,在第一次使用 sed 时,我们这样写:sed 's/.//2g' <<< $STR。与通过正则表达式结合 sed 的各种方式相比,这个脚本中的 sed 使用非常简单。首先,你有命令(sed),然后是参数('s/.//2g'),接着是输入(<<< $STR)。你也可以像这样组合参数:'s/.//2g;s/','/'.'/g'。要获取第一个字符,我们在替换模式(s/)中使用 sed,并通过(/2g)来获取两个字符,其中 g 代表全局模式。
之所以是 2g 而不是 1g,是因为会自动返回一个空字节,因此,如果你需要 n 个字符,那么必须指定 n+1 个字符。要返回前三个字符,我们只需要将 sed 参数中的 2g 改为 4g。
在脚本的下一个代码块中,我们使用如下的 sed:sed -r 's/.{3}//' 和 sed -r '$s/.{3}//;s/.//4g'。你可以看到,在第一次执行 sed 时,-r 用于指定正则表达式,因此我们使用正则表达式返回位置为 4 的字符串(再次提到,那些麻烦的数组和字符串),以及后续的内容。在第二次执行时,我们从第三个字符开始,但限制输出只包含 3 个字符。
在脚本的第三个代码块中,我们想要获取字符串的最后一个字符,使用 sed 's/.*\(.$\)/\1/',然后使用 sed 's/.$//' 获取去掉最后一个字符的整个字符串。第一次,我们使用分组和通配符创建正则表达式,只返回一个字符(即字符串中的最后一个字符);而在第二次,我们使用 .$ 模式创建一个表达式,返回去掉最后一个字符的所有内容。
需要注意的是,搜索和替换也可以通过指定空值来执行删除操作。你还可以使用 -i 标志进行就地编辑,并通过其他标志/参数执行删除。
- 进入下一个脚本,执行后,控制台应该类似如下所示:
$ bash more-strsng.sh
We have 2 rows in testdata/garbage.csv
Bob,Jane,Naz,Sue,Max,Tom$
Zero,Alpha,Beta,Gama,Delta,Foxtrot#
BOB,JANE,NAZ,SUE,MAX,TOM
ZERO,ALPHA,BETA,GAMA,DELTA,FOXTROT
#1000,Robert,Green,Dec,1,1967
#2000,Ron,Brash,Jan,20,1987
#3000,James,Fairview,Jul,15,1992
#1000,Robert,Green,Dec,1,1967
#2000,Ron,Brash,Jan,20,1987
#3000,James,Fairview,Jul,15,1992
#1000,Robert,Green,Dec,1967
#2000,Ron,Brash,Jan,1987
#3000,James,Fairview,Jul,1992
同样,在第一个代码块中,我们将 CSV 读取到数组中,并且对每个元素进行替换操作,去除空格:sed 's/ //g'。
在第二个代码块中,我们再次遍历数组,但我们去掉最后一个字符,使用 sed 's/.$//',然后将输出通过管道转换为大写,使用 sed -e 's/.*/\U&/'。在管道的第一部分,我们使用 .$ 搜索最后一个字符并将其移除(//)。接着,我们使用表达式选择所有内容,并通过 \U& 将其转换为大写(注意,这是 GNU sed 允许的特殊情况)。如果需要小写,则可以使用 \L&。
在第三个代码块中,我们再次使用了for each循环和子 Shell,但我们没有将输入回显到sed。Sed 也可以通过<<<输入方向来接受这样的输入。使用sed -e 's/^/#/',我们从字符串的开头(由^指定)开始,添加一个#。
接下来,对于最后三个示例,我们将在实际文件本身上执行操作,而不是内存中加载的数组,方法是使用带有-i标志的sed。这是一个重要的区别,因为它会对作为输入使用的文件产生直接影响;这很可能是你在脚本中所期望的!要将Bob替换为Robert,这和删除空格一样,只是我们指定了替换内容。然而,我们是在对整个输入的 CSV 文件进行替换!我们还可以为文件中的每一行添加哈希符号。
在最后一个示例中,我们简要使用 AWK 来展示这个工具的强大。在这个示例中,我们指定了分隔符(FS 和 OFS),然后使用 AWK 语言中的gsub sub命令指定第五列来删除该列或字段。Begin 指定了 AWK 在解析输入时使用的规则,如果有多个规则,则按照接收到的顺序执行。
或者,我们可以使用awk 'BEGIN { FS=","} { print $1}' testdata/employees.csv打印第一列或字段,甚至通过指定NR==1来打印第一次出现的内容,像这样:awk 'BEGIN { FS=","} NR==1{ print $1}'。指定返回的记录数量在使用grep命令时非常有用,尤其是在返回大量匹配结果时。
再次,AWK 和 SED 能做的事情非常多。结合正则表达式(regex),各种用法的解释和示例足以填满一本书,专门讲解每个命令。你可以查看网上文档中的工具,以便了解一些平台差异。
使用 echo 和 printf 格式化数据/输出
有时候,找到你要找的字符串或数据是任务中最简单的部分,但格式化输出数据却很棘手。例如,以下是一些需要调整的微妙情况:
-
输出时不加换行符(\n)
-
输出原始十六进制(hex)数据
-
打印原始十六进制值和可打印的 ASCII 字符
-
字符串连接
-
转义特定字符
-
对齐文本
-
打印水平分隔线
除了技巧,我们还可以将值打印到屏幕上,这些值也可以是浮动值(除了数学公式的部分)。等等,什么是十六进制数?是的,存在另一种数据类型,或者至少是一种表示方式。为了理解什么是十六进制数,我们首先需要记住计算机使用 二进制,它由 1 和 0(即 1 和 0)组成。然而,二进制对我们人类并不友好(通常我们在查看数字时使用十进制格式),所以有时需要其他表示方式,其中一种就是 十六进制。如你所料,它是基数 16,因此看起来像 0x0 到 0xF(0x0,0x1,...,0x9,0xA,0xB,...,0xF)。以下是一个例子:
$ echo -en '\xF0\x9F\x92\x80\n'
$ printf '\xF0\x9F\x92\x80\n'
在前面的例子中,printf 和 echo 都可以用来打印原始十六进制和 Unicode 字符。通过一个 Unicode 引用,我找到了 骷髅 字符(F0 9F 92 80)的 UTF-8 编码,然后使用 \xFF 格式化它。注意 FF 的位置,它在每个 字节 中。
知道了“原始十六进制”值后,你能做什么呢?嗯,你可以发送 shell 可以解释为不同内容的字符,或者你可以打印出漂亮的东西!查看更多详情请访问 unicode-table.com。
等等,另一个术语叫做 字节?是的,还有一个叫做 比特。比特 是指 0 或 1,而 字节是 8 个比特(一个字节由八个比特组成!明白了吗?)。
顺便提一下,根据平台或度量的不同——请注意,1 千字节或 KB 可能意味着 1,024 字节(B),或者在许多市场数据表中,1 KB = 1,000 B。此外,当你看到 Kb 时——它并不意味着千字节。它意味着 千比特!
再次提醒,了解计算基础知识,如数据类型和基本数据形式之间的转换,是你技能库中非常有用的工具。这可能会出现在一两次面试中!
然而,我们有点超前了——什么是 echo 和 printf?这两个命令你可能已经在本书中看到过,它们允许你将变量的内容等输出到控制台或文件中。Echo 更“直接”,而 printf 则可以使用 C 风格的参数提供相同的功能,甚至更多。事实上,printf 相对于 echo 的一个主要优势是它可以格式化字符、填充字符,甚至对齐字符。
好的,开始工作吧。
准备工作
对于这个练习,不需要额外的工具或脚本——只需你、你的终端和 Bash。
如何操作……
让我们开始如下活动:
- 打开一个新的脚本文件,命名为
echo-mayhem.sh,使用你喜欢的编辑器,并打开一个新的终端。在终端中输入以下内容并执行该脚本:
#!/bin/bash
# What about echo?
echo -n "Currently we have seen the command \"echo\" used before"
echo " in the previous script"
echo
echo -n "Can we also have \t tabs? \r\n\r\n?"
echo " NO, not yet!"
echo
echo -en "Can we also have \t tabs? \r\n\r\n?"
echo " YES, we can now! enable interpretation of backslash escapes"
echo "We can also have:"
echo -en '\xF0\x9F\x92\x80\n' # We can also use \0NNN for octal instead of \xFF for hexidecimal
echo "Check the man pages for more info ;)"
- 在查看
echo-mayhem.sh的结果后,创建另一个名为printf-mayhem的脚本,并输入以下内容:
#!/bin/bash
export LC_NUMERIC="en_US.UTF-8"
printf "This is the same as echo -e with a new line (\\\n)\n"
DECIMAL=10.0
FLOAT=3.333333
FLOAT2=6.6666 # On purpose two missing values
printf "%s %.2f\n\n" "This is two decimal places: " ${DECIMAL}
printf "shall we align: \n\n %.3f %-.6f\n" ${FLOAT} ${FLOAT2}
printf " %10f %-6f\n" ${FLOAT} ${FLOAT2}
printf " %-10f %-6f\n" ${FLOAT} ${FLOAT2}
# Can we also print other things?
printf '%.0s-' {1..20}; printf "\n"
# How about we print the hex value and a char for each value in a string?
STR="No place like home!"
CNT=$(wc -c <<< $STR})
TMP_CNT=0
printf "Char Hex\n"
while [ ${TMP_CNT} -lt $[${CNT} -1] ]; do
printf "%-5s 0x%-2X\n" "${STR:$TMP_CNT:1}" "'${STR:$TMP_CNT:1}"
TMP_CNT=$[$TMP_CNT+1]
done
- 执行
printf-mayhem.sh的内容,并查看其微妙的差异。
它是如何工作的……
虽然这是一个关于数据类型的非常重要的话题(特别是处理数学或计算时),我们将这个问题的解决方案分成了两部分:
- 在第一步中,echo 相当简单。我们之前提到过有特殊字符和转义符。
\t表示制表符,\r\n表示 Windows 中的换行符(尽管在 Linux 中,\n\n就足够了),我们还可以打印出一个漂亮的 UTF 字符:
$ bash echo-mayhem.sh
Currently we have seen the command "echo" used before in the previous script
Can we also have \t tabs? \r\n\r\n? NO, not yet!
Can we also have tabs?
? YES, we can now! enable interpretation of backslash escapes
We can also have:
Check the man pages for more info ;)
- 然而,第二步的结果有点不同,我们可以从下面的代码中看到这一点。让我们深入探讨一下,因为这看起来不仅仅是对齐不当的问题:
$ bash printf-mayhem.sh
This is the same as echo -e with a new line (\n)
This is two decimal places: 10.00
shall we align:
3.333 6.666600
3.333333 6.666600
3.333333 6.666600
--------------------
Char Hex
N 0x4E
o 0x6F
0x20
p 0x70
l 0x6C
a 0x61
c 0x63
e 0x65
0x20
l 0x6C
i 0x69
k 0x6B
e 0x65
0x20
h 0x68
o 0x6F
m 0x6D
e 0x65
! 0x21
0x0
- 如我们所见,在前面的步骤执行后,有几个有趣的地方。首先我们注意到的是,
printf是强化版的 echo,它提供了相同的功能,甚至更多的功能,比如对齐、用%s打印字符串,以及小数点(例如%.2f)。深入了解后,我们可以看到,使用%后跟一个值,我们可以限制小数点的位数。注意,通常在%符号后面紧跟的字符——这就是你如何格式化后续参数。使用像%10f这样的值时,我们将为该值分配 10 个空格,或者说,分配 10 个字符的宽度。如果我们使用%-10,则表示我们将值左对齐。除了几乎水平的规则使用扩展之外,我们还通过“逐步”处理字符串“No place like home!”来进行演示。使用while循环,我们打印出每个 ASCII 字符(使用%-c)及其对应的十六进制值(%-2X)。
注意,即使是空格也有一个十六进制(hex)值,它是0x20。如果你运行了脚本并得到了 "printf-mayhem.sh: line 26: printf: !: invalid number",那是因为你在"'${STR:$TMP_CNT:1}"中漏掉了单引号'。这意味着如何将返回的值解释为字符串/字符或数字值。
准备好你的脚本以适应不同语言的国际化
太好了,你有了这个很棒的脚本,但它是用标准英语写的,你希望面向那些讲其他语言的人。在像加拿大这样的国家,他们(我们)有两种官方语言:英语和法语。有时候,双语组件通过立法和本地化语言法律得以执行。
为了绕过这个问题,假设你是一个编写了脚本的个人,这个脚本首先用英语打印特定的字符串。他/她希望将所有字符串放入变量中,以便可以使用系统语言变量动态地进行替换。这里是基础知识:
-
创建一个利用gettext并设置适当变量的 Shell 脚本
-
创建一个包含必要语言定义的po文件
-
为你的脚本安装输出语言本地化文件
-
使用不同于原始语言的语言运行脚本(通过设置 LANG 变量)
不过,在开始之前,有两个术语需要讨论:国际化(i18n)和本地化(L10n)。国际化是一个使翻译和本地化/适应特定脚本或程序成为可能的过程,而本地化则是指将程序/应用程序适配特定文化的过程。
从一开始就翻译脚本是节省时间并提高多语言工作的成功率的一种有效方法。然而,请注意,如果开发人员只精通一种语言,或者翻译技能不立即可用,这可能是一个耗时的过程。
例如,在英语中,有几种方言。在美国,一个过程的产物或剩余物可以称为artifact,但在加拿大英语中,它可能被称为artefact。这可能不会引起注意(或被忽略),但程序可以通过特定的本地化自动适应。
准备就绪
让我们准备好进行练习,确保已经安装了以下应用程序和支持库:
$ sudo apt-get install -y gettext
接下来,验证你的语言环境变量(LANG):
$ locale
LANG=en_CA:en
LANGUAGE=en_CA:en
LC_CTYPE="en_CA:en"
LC_NUMERIC="en_CA:en"
LC_TIME="en_CA:en"
LC_COLLATE="en_CA:en"
LC_MONETARY="en_CA:en"
LC_MESSAGES="en_CA:en"
LC_PAPER="en_CA:en"
LC_NAME="en_CA:en"
LC_ADDRESS="en_CA:en"
LC_TELEPHONE="en_CA:en"
LC_MEASUREMENT="en_CA:en"
LC_IDENTIFICATION="en_CA:en"
LC_ALL=
我们假设你的环境可能已经将某种形式的英语设为默认语言(en_CA:en是加拿大英语)——记得记录返回的值以便后续使用!
如果出现问题,你可能需要恢复你的语言和区域设置。网上有许多帖子提供帮助,几个提示是:$ export LC_ALL="en_US.UTF-8";sudo locale-gen;以及sudo dpkg-reconfigure locales。
如何操作……
我们的活动开始如下:
- 打开一个新的终端,并创建一个名为
hellobonjour.sh的脚本,内容如下:
#!/bin/bash
. gettext.sh
function i_have() {
local COUNT=$1
###i18n: Please leave $COUNT as is
echo -e "\n\t" $(eval_ngettext "I have \$COUNT electronic device" "I have \$COUNT electronic devices" $COUNT)
}
echo $(gettext "Hello")
echo
echo $(gettext "What is your name?")
echo
###i18n: Please leave $USER as is
echo -e "\t" $(eval_gettext "My name is \$USER" )
echo
echo $(gettext "Do you have electronics?")
i_have 0
i_have 1
i_have 2
- 运行
xgettext以生成适当的字符串。我们不会使用这些结果,但这就是你如何生成一个极简的 PO 文件:
$ xgettext --add-comments='##i18n' -o hellobonjour_fr.po hellobonjour.sh --omit-header
- 将已经编译好的字符串列表复制到名为
hellobonjour_fr.po的语言 PO 文件中:
# Hellobonjour.sh
# Copyright (C) 2017 Ron Brash
# This file is distributed under the same license as the PACKAGE package.
# Ron Brash <ron.brash@gmail.com>, 2017
# Please ignore my terrible google translations;
# As always, some is better than none!
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: i18n@example.com\n"
"POT-Creation-Date: 2017-12-08 12:19-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: French Translator <fr@example.org>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=iso-8859-1\n"
"Content-Transfer-Encoding: 8bit\n"
#. ##i18n: Please leave $COUNT as is
#: hellobonjour.sh:6
#, sh-format
msgid "I have $COUNT electronic device"
msgid_plural "I have $COUNT electronic devices"
msgstr[0] "J'ai $COUNT appareil electronique"
msgstr[1] "J'ai $COUNT appareils electroniques"
#: hellobonjour.sh:10
msgid "Hello"
msgstr "Bonjour"
#: hellobonjour.sh:13
msgid "What is your name?"
msgstr "Comment t'appelles tu?"
#. ##i18n: Please leave $USER as is
#: hellobonjour.sh:17
#, sh-format
msgid "My name is $USER"
msgstr "Mon nom est $USER"
#: hellobonjour.sh:20
msgid "Do you have electronics?"
msgstr "Avez-vous des appareils electroniques?"
- 接下来,使用
msgfmt将 PO 文件编译为具有.mo扩展名的二进制语言文件,并将其放入我们指定的语言文件夹中:
$ rm -rf locale/fr/LC_MESSAGES
$ mkdir -p locale/fr/LC_MESSAGES
$ sudo msgfmt -o locale/fr/LC_MESSAGES/hellobonjour.mo hellobonjour_fr.po
- 一旦你将语言文件就位,创建以下名为
translator.sh的脚本:
#!/bin/bash
./hellobonjour.sh
export TEXTDOMAIN="hellobonjour"
export TEXTDOMAINDIR=`pwd`/locale
export LANGUAGE=fr
./hellobonjour.sh
- 执行
translator.sh后,检查两次执行translator.sh的结果:
$ bash translator.sh
它是如何工作的……
不言而喻,翻译可能是一个棘手的事情,尤其是在处理编码时,以及生成在语言层面上有意义的结果时。此外,即使脚本中的值发生轻微变化,也可能会破坏 PO 文件,导致生成的脚本没有完全翻译(有时甚至根本没有翻译)。
在稍后修改脚本时,请小心不要破坏keys。
- 第一步相当简单——你只需创建一个脚本。如果你运行脚本,你会看到纯粹的英文输出,但至少复数和单数的输出是正确的。请注意
. gettext.sh;这一行准备了gettext以进行国际化/本地化。在脚本中,我们还使用了gettext、eval_gettext和eval_ngettext。这些是允许翻译发生的函数。对于简单的翻译,使用gettext,对于包含变量的翻译,使用eval_gettext,对于包含复数对象的翻译,使用eval_ngettext。正如你可能注意到的,eval_ngettext稍微复杂一些:$(eval_ngettext "I have \$COUNT electronic device" "I have \$COUNT electronic devices" $COUNT)。eval_ngettext的第一个参数是单数翻译,第二个是复数翻译,而计数是用来确定是使用单数还是复数值的变量。变量在原始脚本中以转义符\$COUNT来表示,翻译字符串中的变量将出现在翻译文件中,形式为$COUNT,而没有转义符:
./hellobonjour.sh
Hello
What is your name?
My name is rbrash
Do you have electronics?
I have 0 electronic devices
I have 1 electronic device
I have 2 electronic devices
-
在第二步中,我们使用
xgettext创建了一个名为 PO 文件的语言文件。PO 是“便携对象”(Portable Object)的缩写。请注意,我们省略了头部信息,因为它会产生额外的输出。这在你想要写笔记、版本信息,甚至指定使用的编码时特别有用。 -
我们并没有从头开始编写翻译,而是使用了我们值得信赖的朋友 Google 翻译来生成一些基础翻译,然后将其复制到
xgettext的输出中。Xgettext 几乎生成了相同的文件!请注意msgid、msgstr、msgplural和msgstr[...]。Msgid和msgid_plural用于匹配原始值,就像它们是一个键一样。例如,当脚本运行时,gettext看到"I have $COUNT electronic device",然后知道输出一个特定的翻译,匹配那个相同的msgid:
msgid "I have $COUNT electronic device"
msgid_plural "I have $COUNT electronic devices"
msgstr[0] ".."
-
hellobonjour_fr.po包含了我们的所有翻译,现在我们可以使用一个叫做msgfmt的命令,它用于生成 MO 文件或机器对象。如果你用像vi这样的编辑器打开这个文件,你会发现它包含了一堆符号,代表二进制数据和字符串。这个文件不应该被编辑,而应该编辑输入的 PO 文件本身。 -
接下来,我们创建一个名为
translator.sh的文件。它运行hellobonjour.sh并包含几行代码,这些代码设置了三个重要的变量:TEXTDOMAIN、TEXTDOMAINDIR和LANGUAGE。TEXTDOMAIN通常是用来描述二进制文件或脚本的变量(可以将其视为命名空间),而TEXTDOMAINDIR是gettext查找翻译的目录。请注意,它位于本地相对目录中,而不是/usr/share/locale(尽管它也可以是)。最后,我们将LANGUAGE设置为法语(fr)。 -
当我们执行
translator.sh时,hellobonjour.sh会被运行两次,第一次输出英文,第二次输出法语:
$ bash translator.sh
Hello
What is your name?
My name is rbrash
Do you have electronics?
I have 0 electronic devices
I have 1 electronic device
I have 2 electronic devices
Bonjour
Comment t'appelles tu?
Mon nom est rbrash
Avez-vous des appareils electroniques?
J'ai 0 appareils electroniques
J'ai 1 appareil electronique
J'ai 2 appareils electroniques
不要使用旧的格式 $"my string" 来进行翻译。它存在安全风险!
基于文件内容计算统计数据并减少重复项
初看之下,基于文件内容计算统计数据可能不是使用 Bash 脚本完成的最有趣的任务,但在某些情况下,它却能派上用场。让我们假设程序从多个命令获取用户输入。我们可以计算输入的长度,以确定它是否过短或过长。或者,我们也可以确定字符串的大小,以便为用其他编程语言(如 C/C++)编写的程序确定缓冲区大小:
$ wc -c <<< "1234567890"
11 # Note there are 10 chars + a new line or carriage return \n
$ echo -n "1234567890" | wc -c
10
我们可以使用像wc这样的命令来计算单词出现的次数、行数总数以及许多其他功能,结合脚本提供的功能来使用。
更好的是,如果我们使用一个名为strings的命令将所有可打印的 ASCII 字符串输出到一个文件呢?strings程序会输出每一个字符串的出现次数——即使它们是重复的。通过使用其他程序,如sort和uniq(或者这两者的组合),我们也可以对文件内容进行排序,并减少重复项,如果我们想计算文件中唯一行的数量:
$ strings /bin/ls > unalteredoutput.txt
$ ls -lah unalteredoutput.txt
-rw-rw-r-- 1 rbrash rbrash 22K Nov 24 11:17 unalteredoutput.txt
$ strings /bin/ls | sort -u > sortedoutput.txt
$ ls -lah sortedoutput.txt
-rw-rw-r-- 1 rbrash rbrash 19K Nov 24 11:17 usortedoutput.txt
现在我们已经知道了执行一些基本统计的几个基本前提,接下来继续进行实际步骤。
准备工作
让我们通过创建一个数据集来准备练习:
$ mkdir -p testdata
$ cat /etc/hosts > testdata/duplicates.txt; cat /etc/hosts >> testdata/duplicates.txt
如何做...
我们已经在之前的步骤中看到了这些概念,甚至在其中一个食谱中也使用了wc,所以我们直接开始:
- 打开终端并运行以下命令:
$ wc -l testdata/duplicates.txt
$ wc -c testdata/duplicates.txt
- 正如你可能已经注意到的,输出中包含了文件名。我们能用 AWK 移除它吗?当然可以,但我们也可以使用名为
cut的命令来移除它。-d选项代表分隔符,我们想选择一个字段(由-f1指定):
$ wc -c testdata/duplicates.txt | cut -d ' ' -f1
$ wc -c testdata/duplicates.txt | awk '{ print $1 }'
- 假设我们有一个充满字符串的庞大文件。我们能减少返回的结果吗?当然可以,但首先我们使用
sort命令对testdata/duplicates.txt中的元素进行排序,然后使用sort命令生成仅包含唯一元素的列表:
$ sort testdata/duplicates.txt
$ sort -u testdata/duplicates.txt
$ sort -u testdata/duplicates.txt | wc -l
工作原理...
总体而言,除了计数出现次数和排序的好处外,本脚本没有引入真正的抽象概念。排序可能是一个耗时的过程,尤其是在减少不需要的或多余数据时,或者当顺序很重要时,但当进行大规模操作时,它也可以带来回报,并且预处理通常会更快地得到结果。
接下来开始实际步骤:
- 运行这两个
wc命令会生成文件testdata/duplicates.txt的字符数和行数统计。同时,这也暴露了另一个问题:数据可能会被文件名前的空格填充:
$ wc -l testdata/duplicates.txt
18 testdata/duplicates.txt
$ wc -c testdata/duplicates.txt
438 testdata/duplicates.txt
- 在步骤 2 中,我们使用
awk和cut命令删除第二个字段。cut命令是一个有用的命令,用于修剪字符串,可以按分隔符分割,或使用硬编码的值(例如删除 X 个字符)。使用cut时,-d表示分隔符,这里是空格(' '),-f1表示字段1:
$ wc -c testdata/duplicates.txt | cut -d ' ' -f1
438
$ wc -c testdata/duplicates.txt | awk '{ print $1 }'
438
- 在最后一步,我们运行
sort命令三次。第一次我们仅仅对testdata/duplicates.txt中的元素进行排序,然后使用-u选项对其进行排序并保留唯一元素,最后使用final命令统计唯一元素的数量。当然,返回的值是9,因为我们在原始重复文件中有 18 行:
$sort testdata/duplicates.txt
127.0.0.1 localhost
127.0.0.1 localhost
127.0.1.1 moon
127.0.1.1 moon
::1 ip6-localhost ip6-loopback
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::2 ip6-allrouters
# The following lines are desirable for IPv6 capable hosts
# The following lines are desirable for IPv6 capable hosts
$ sort -u testdata/duplicates.txt
127.0.0.1 localhost
127.0.1.1 moon
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
# The following lines are desirable for IPv6 capable hosts
$ sort -u testdata/duplicates.txt | wc -l
9
使用文件属性和条件逻辑
在本书的早些时候,我们提到了各种针对字符串、数字和变量的测试。通过 Bash 内置的类似概念,我们也可以使用各种属性来对文件和目录进行测试。这是在条件逻辑介绍的基础上扩展,来执行文件测试。例如:是否存在某个文件?它是目录吗?等等。
但是,稍等一下,我们难道不可以直接使用执行结果并检查返回码吗?当然可以!这是你可以使用的另一种方法,尤其是当你使用支持所有 Bash 功能的 Bash 版本时。这只是“换个方式做事”的另一种方法。
让我们首先从一些常见的标志开始,如果满足以下条件则返回真:
-
-e:文件存在 -
-f:这是一个常规文件,而不是目录或设备文件 -
-s:该文件不为空或大小为零 -
-d:这是一个目录 -
-r:该文件具有读取权限 -
-w:该文件具有写入权限 -
-x:该文件具有执行权限 -
-O:这是文件的所有者,当前用户 -
-G:如果用户与您属于同一组,则执行该操作 -
f1(-nt,-ot,-ef)f2:指示f1是否比f2更新,或者比f2更旧,或者是否与f2是硬链接到同一文件
文件测试操作的更多信息可以在 GNU Bash 手册中找到:www.gnu.org/software/bash/manual/html_node/Bash-Conditional-Expressions.html。
准备开始
让我们通过创建一些文本文件和目录并添加内容来为练习做好准备:
$ cd ~/
$ mkdir -p fileops
$ touch fileops/empty.txt
$ echo "abcd1234!!" > fileops/string.txt
$ echo "yieldswordinthestone" > fileops/swordinthestone.txt
$ touch fileops/read.txt fileops/write.txt fileops/exec.txt fileops/all.txt
$ chmod 111 fileops/exec.txt; chmod 222 fileops/write.txt; chmod 444 fileops/read.txt; fileops/all.txt;chmod 777 fileops/all.txt
$ sudo useradd bob
$ echo "s the name" > fileops/bobs.txt
$ sudo chown bob.bob fileops/bobs.txt
这个食谱讲解的是执行一些简单的文件测试,并结合之前条件逻辑中学到的一些知识,但有个小变化——使用来自 CLI 的用户输入和文件权限。
注意命令chmod,useradd和chmod。Chmod 是你用来更改文件权限(例如执行权限等)的命令。
如何操作……
让我们按如下方式开始我们的活动:
- 打开一个新的终端,启动你选择的编辑器并创建一个新的脚本。以下是脚本中的代码片段:
#!/bin/bash
FILE_TO_TEST=""
function permissions() {
echo -e "\nWhat are our permissions on this $2?\n"
if [ -r $1 ]; then
echo -e "[R] Read"
fi
if [ -w $1 ]; then
echo -e "[W] Write"
fi
if [ -x $1 ]; then
echo -e "[X] Exec"
fi
}
function file_attributes() {
if [ ! -s $1 ]; then
echo "\"$1\" is empty"
else
FSIZE=$(stat --printf="%s" $1 2> /dev/null)
RES=$?
if [ $RES -eq 1 ]; then
return
else
echo "\"$1\" file size is: ${FSIZE}\""
fi
fi
if [ ! -O $1 ]; then
echo -e "${USER} is not the owner of \"$1\"\n"
fi
if [ ! -G $1 ]; then
echo -e "${USER} is not among the owning group(s) for \"$1\"\n"
fi
permissions $1 "file"
}
-
执行脚本并尝试访问各种文件,包括不存在的目录和文件。你注意到了什么?
-
现在使用以下命令删除该文件夹:
$ sudo rm -rf fileops
它是如何工作的……
首先,在深入研究脚本本身甚至文件的属性/属性之前,我们需要了解 Linux 及其兄弟操作系统的一些情况:
-
文件和目录可以有所有者。这意味着它们可以有一个所有者(用户)和与其所有权相关联的组。为此,我们可以使用
chown和chgrp命令。 -
文件和目录可以应用不同的权限。这意味着它们可能是可执行的、可读的、可写的和/或所有的。为此,我们可以使用
chmod命令和适当的权限设置。 -
文件和目录也可以是空的。
太棒了!此外,还有两个需要介绍的概念:
-
read命令,用于等待用户输入并将其读入变量。它在脚本中也非常有用,用于“暂停”功能。 -
递归函数。请注意,脚本中的特定函数在退出或用户按下ctl + C之前会不断调用。这就是递归,除非停止或应用限制。
到目前为止,我们还了解了函数、参数、输入/输出、返回码、子 shell 和条件逻辑。您可能没有注意到!字符,这用于否定语句。例如,如果我们使用-e测试运算符测试fileops/bobs.txt的存在,它将返回 true。相反,我们可以测试其相反情况,即fileops/bobs.txt不存在。
与反转或否定语句相同的逻辑也可以通过 if/else 功能实现,但有时候这可能会提高脚本的“可读性”和“流畅性”。最终,是否使用反转取决于脚本编写者。
-
太棒了!我们已经创建了我们的脚本,并准备执行它。
-
执行脚本后,我们看到:
$ ./files-extended.sh
Welcome to the file attributes tester
To exit, press CTRL + C
What is the complete path of the file you want to inspect?
#
如果我们回顾一下这个配方的设置,我们知道我们在fileops/目录内创建了几个文件,并且其中一些文件具有不同的权限,其中一个文件是由名为Bob的用户拥有的。
让我们尝试一些执行(按顺序):
-
fileops/bobs.txt -
fileops/write.txt -
fileops/exec.txt -
fileops/all.txt -
thisDoesNotExist.txt:
# fileops/bobs.txt
"fileops/bobs.txt" file size is: 11"
rbrash is not the owner of "fileops/bobs.txt"
rbrash is not among the owning group(s) for "fileops/bobs.txt"
What are our permissions on this file?
[R] Read
What is the complete path of the file you want to inspect?
# fileops/write.txt
"fileops/write.txt" is empty
What are our permissions on this file?
[W] Write
What is the complete path of the file you want to inspect?
# fileops/exec.txt
"fileops/exec.txt" is empty
What are our permissions on this file?
{X] Exec
What is the complete path of the file you want to inspect?
# fileops/all.txt
"fileops/all.txt" is empty
What are our permissions on this file?
[R] Read
[W] Write
{X] Exec
What is the complete path of the file you want to inspect?
# fileops
Directory "fileops" has children:
all.txt
bobs.txt
empty.txt
exec.txt
read.txt
string.txt
swordinthestone.txt
write.txt
What are our permissions on this directory?
[R] Read
[W] Write
{X] Exec
What is the complete path of the file you want to inspect?
# thisDoesNotExist.txt
Error: "thisDoesNotExist.txt" does not exist!
$
因为thisDoesNotExist.txt不存在,脚本会突然退出并将您放回到控制台提示符。我们测试了各种标志、否定、所有权,甚至我们非常有用的实用程序xargs。
读取分隔数据和更改输出格式
每天,我们会打开许多不同格式的文件。但是,当处理大量数据时,使用标准格式始终是一个好习惯。其中之一称为逗号分隔值,或 CSV,它使用逗号(,)来分隔元素或分隔每一行。当您拥有大量数据或记录,并且这些数据将以脚本方式使用时,这特别有用。例如,在每个学期,系统管理员 Bob 需要创建一系列新用户并设置他们的信息。Bob 还从负责出勤的人那里得到一个标准化的 CSV(就像下面的片段中那样):
Rbrash,Ron,Brash,01/31/88,+11234567890,rbrash@acme.com,FakePassword9000
...
如果管理员 Bob 只希望将此信息读入数组并创建用户,那么对他来说,解析 CSV 并在一个单一的脚本化操作中创建每个记录相对较为简单。这使 Bob 可以将时间和精力集中在其他重要问题上,比如解决最终用户的 WiFi 问题。
虽然这只是一个琐碎的例子,但这些文件可能以不同形式存在,使用分隔符(例如逗号,或美元符号$)、不同的数据和不同的结构。但是,每个文件的基础都是每一行都是一个需要被读入某种结构(不管是什么)的记录,在 SQL、Bash 数组等中:
Line1Itself: Header (optional and might not be present)
Line2ItselfIsOneREc:RecordDataWithDelimiters:endline (windows \r\n, in Linux \n)
....
在上述伪 CSV 的示例中,有一个可能是可选的标题(不出现),然后有几行(每行是一条记录)。Bob 有许多方法可以解析 CSV,但他可能会使用应用策略的专业函数,例如:
$ Loop through each item until done
for each line in CSV:
# Do something with the data such as create a user
# Loop through Next item if it exists
要读取数据,Bob 或您自己可能会使用:
-
对于循环和数组
-
一种迭代器的形式
-
手动逐行读取(效率低下)
一旦读入任何输入数据,下一步就是对数据本身进行处理。是要进行转换?是要立即使用?经过消毒处理?存储?还是转换为另一种格式?就像 Bob 一样,使用脚本读入的数据可以执行许多操作。
关于输出数据,我们也可以将其转换为 XML、JSON,甚至将其作为 SQL 插入到数据库中。不幸的是,此过程需要了解至少两个方面:输入数据的格式和输出数据的格式。
知道常见的数据格式及其通常应用验证时,可以在构建自动化脚本和识别未来任何变化时提供极大帮助。强制执行数据验证还有几个好处,可以在脚本突然无预警中断时挽救局面!
本文旨在引导你逐步阅读一个简单的 CSV 并将数据输出到一些任意的格式中。
准备工作
让我们通过创建一些模仿常见日常问题的数据集来为这项练习做好准备:
$ cd ~/
$ echo
$ echo -e "XML_HDR='<?xml version="1.0" encoding="UTF-8"?>'\\nSRT_CONTR='<words type="greeting">'\\nEND_CONTR='</words>'" > xml-parent.tpl
$ echo -e "ELM='\"<word lang=\"\$1\">\"\$2\"</word>\"'" > word.tpl
$ echo -e "\"EN\",\"Hello\"\n\"FR\",\"Bonjour\"" > words.csv
在 Bash 中,单引号(')用于字面字符串。在这种情况下,我们希望字符串的每一部分都存在,不需要转义斜杠和双引号。
要运行此脚本,您需要安装以下应用程序,以便在脚本中使用:
$ sudo apt-get install npm sed awk
$ sudo npm install -g xml2json-command
$ sudo ln -s /usr/bin/nodejs /usr/bin/node
如何操作...
让我们按以下方式开始活动:
- 打开终端并创建名为
data-csv-to-xml.sh的脚本,内容如下。保存后,使用$ bash data-csv-to-xml.sh执行该脚本:
#!/bin/bash
# Import template variables
source xml-parent.tpl
source word.tpl
OUTPUT_FILE="words.xml"
INPUT_FILE="words.csv"
DELIMITER=','
# Setup header
echo ${XML_HDR} > ${OUTPUT_FILE}
echo ${SRT_CONTR} >> ${OUTPUT_FILE}
# Enter content
echo ${ELM} | \
sed '{:q;N;s/\n/\\n/g;t q}'| \
awk \
'{ print "awk \x27 BEGIN{FS=\"'${DELIMITER}'\"}{print "$0"}\x27 '${INPUT_FILE}'"}' | \
sh >> ${OUTPUT_FILE}
# Append trailer
echo ${END_CONTR} >> ${OUTPUT_FILE}
cat ${OUTPUT_FILE}
-
审查输出时,请注意,“美观”的 XML 不是必需的,实际上,我们甚至不需要将 XML 分布在多行上。如果 web 应用程序只需要纯数据,那么额外的换行符和制表符就是不必要的传输数据。
-
创建另一个名为
data-xml-to-json.sh的脚本,内容如下。保存后,使用$ data-xml-to-json.sh执行该脚本:
!#/bin/bash
INPUT_FILE"words.xml"
OUTPUT_FILE="words.json"
# Easy one line!
xml2json < ${INPUT_FILE} ${OUTPUT_FILE}
- 审查输出,看看它是多么简单!在两个脚本中,是否有可以改进的地方?
它是如何工作的...
我们已经讨论了几个重要的方面,比如 SED 和 AWK 命令的强大功能,甚至 CSV 文件,但我们还没有讨论能够转换数据格式和结构的重要性。CSV 是一种基础且非常常见的数据格式,但不幸的是,它并不是某些应用程序的最佳选择,因此我们可能会使用 XML 或 JSON。这里有两个脚本(或者说一个脚本和一个工具),可以将我们的原始数据转换成各种格式:
- 执行
data-csv-to-xml.sh时,我们注意到几个要点:我们使用了两个源模板文件,可以根据需要进行修改,然后是一个大型的管道命令,利用了 sed 和 AWK。在输入时,我们将每个 CSV 值提取出来,并使用word.tpl中的格式模板构建一个<word lang="x">Y</word>的 XML 元素,其中$0是第一列,$1是第二列。该脚本将生成words.csv并输出以下内容:
$ bash data-csv-to-xml.sh
<?xml version="1.0" encoding="UTF-8"?>
<words type="greeting">
<word lang="EN">"Hello"</word>
<word lang="FR">"Bonjour"</word>
</words>
- 在第二个脚本中,我们仅仅将
words.xml作为输入传递给命令xml2json。输出将是 JSON 格式。很酷吧?
!#/bin/bash
{
"words": {
"type": "greeting",
"word": [
{
"lang": "EN",
"$t": "\"Hello\""
},
{
"lang": "FR",
"$t": "\"Bonjour\""
}
]
}
}
关于三种数据格式(CSV、XML 和 JSON)之间的差异及其原因,留给读者自行发现。另一个需要探索的练习是执行数据验证,以确保数据的完整性和约束。例如,XML 可以使用 XSD 模式来强制数据限制。
第三章:理解并掌握文件系统管理
本章中,我们将介绍以下内容:
-
从不同角度查看文件——头部、尾部、less 和 more
-
按名称和/或扩展名搜索文件
-
创建两个文件的差异并打补丁
-
创建符号链接并有效使用它们
-
爬取文件系统目录并打印目录树
-
查找并删除重复的文件或目录
-
在任意位置合并和拆分文件
-
生成各种大小的数据集和随机文件
介绍
本章中,我们将扩展第二章的部分内容,像打字机和文件浏览器一样操作,但目标是让你在创建、查看和管理文件时更强大。毕竟,如何查看一个非常大的文件?如何找到二进制文件的外部软件依赖并操作文件?这些任务无疑是每个开发者、管理员或高级用户都能想到的基础任务。
例如,读者 Bob 已经了解了 VI,或许他有自己的 GUI 编辑器或应用程序,如 Open Office,但如果这个编辑器在打开完整文件时崩溃了怎么办?他可以只查看文件的前几行吗?完全可以。他能在 X 行处拆分该文件(如果结构已知,如 CSV 格式)吗?当然可以!
这些事情并非不可能,Bob 可以做的活动清单几乎是无止境的。本章的目的是让你了解如果生活不如意或你需要快速访问/控制系统中的文件时,你可以做的一些事情。
本章的脚本可以在 github.com/PacktPublishing/Bash-Cookbook/tree/master/chapter%2003 找到。
从不同角度查看文件——头部、尾部、less 和 more
目前,你的系统可能有许多不同大小的文本文件,其中还包括一个不断写入的日志文件。你可能还拥有一些包含大量代码的大文件(如 Linux 内核或软件项目),并希望能快速查看这些文件,而不会让系统变得非常缓慢。
为此,有四个基本命令,应该能够为你提供足够的功能来实现它们的目的:
-
Head:可以用于输出文件的开头部分
-
Tail:可以用于输出文件的末尾部分(也可以连续输出)
-
More:一个作为 分页器 用于逐页或逐行查看大文件的工具
-
Less:与 more 相同,但它有更多功能,包括向后滚动
有时,你可能会在嵌入式系统上看到 more 命令,而不是 less 命令。这是因为 less 命令比 more 更大。你的头痛了吗?
准备工作
除了打开终端之外,本食谱还需要几个大文本文件。如果你已经有了,那太好了;如果没有,可以安装以下内容:
$ wget http://www.randomtext.me/download/txt/lorem/ol-20/98-98.txt
$ fmt 98-98.txt > loremipsum.txt
fmt 命令是一个简单的文本格式化工具,用于清理输出内容,使命令行结果更为整洁。
如何操作...
打开终端并运行以下命令:
$ cat loremipsum.txt
$ head loremipsum.txt
$ head -n 1 loremipsum.txt
$ tail loremipsum.txt
$ tail -n 1 loremipsum.txt
有趣的是,tail 命令具有一个不同于 head 命令的特性:它可以持续监控文件的尾部,直到命令被退出或终止,前提是使用了 -f 或 -F 标志。运行以下命令:
$ tail -F /var/log/kern.log
保持 tail 命令运行,尝试断开你的无线或以太网端口。你看到了什么?
按 Ctrl + C 退出 tail,并运行以下命令:
$ more loremipsum.txt
按下空格键或 Enter 键将使你浏览文件直到末尾。按 q 将立即退出 more,并返回到控制台提示符。
接下来,尝试运行以下命令:
$ less loremipsum.txt
尝试使用 pg up、pg dn、上下箭头键、Enter 和空格键浏览文件。注意到什么了吗?
它是如何工作的...
在继续之前,请注意,loremipsum.txt 文件的内容在每次下载时都会有所不同。Lorem Ipsum 是一种伪随机文本,广泛用于各种与文本相关的任务,通常作为占位符值,因为它 看起来 像某种语言,且在复制粘贴占位文本时能避免人类大脑的干扰。
太好了!我们开始吧:
- 在第一步中,命令应该会生成类似于以下的输出(为了简洁,我们省略了部分输出以保持教程的一致性),但注意,
head从文件的开头,即loremipsum.txt的 head 开始,而tail从文件的末尾,即loremipsum.txt的 tail 开始。当我们指定带有小数数字(如1)的-n标志时,这两个工具都会输出一行或用户输入的任意行数:
$ cat loremipsum.txt
Feugiat orci massa inceptos proin adipiscing urna vestibulum
hendrerit morbi convallis commodo porta magna, auctor cras nulla ligula
sit vehicula primis ultrices duis rutrum cras feugiat sit facilisis
fusce placerat sociosqu amet cursus quisque praesent mauris facilisis,
egestas curabitur imperdiet sit elementum ornare sed class ante pharetra
in, nisi luctus sit accumsan iaculis eu platea sit ullamcorper platea
erat convallis orci volutpat curabitur nostra tellus erat non nisl
condimentum, cubilia lacinia eget rhoncus pharetra euismod sagittis
morbi risus, nisl scelerisque fringilla arcu auctor turpis ultricies
imperdiet nibh eget felis leo enim auctor sed netus ultricies sit fames
...
$ head loremipsum.txt
Feugiat orci massa inceptos proin adipiscing urna vestibulum
hendrerit morbi convallis commodo porta magna, auctor cras nulla ligula
sit vehicula primis ultrices duis rutrum cras feugiat sit facilisis
fusce placerat sociosqu amet cursus quisque praesent mauris facilisis,
egestas curabitur imperdiet sit elementum ornare sed class ante pharetra in
$ head -n 1 loremipsum.txt
Feugiat orci massa inceptos proin adipiscing urna vestibulum
$ tail loremipsum.txt
euismod torquent primis mattis velit aptent risus accumsan cubilia eros
justo ad sodales dapibus tempor, donec mauris erat at lacinia senectus
luctus venenatis mollis ullamcorper ante mollis nisl leo sollicitudin
felis congue tempus nam curabitur viverra venenatis quis, felis pretium
enim posuere elit bibendum dictumst, bibendum mattis blandit sociosqu
adipiscing cursus quisque augue facilisis vehicula metus taciti conubia
odio proin rutrum aliquam lorem, erat lobortis etiam eget risus lectus
sodales mauris blandit, curabitur velit risus litora tincidunt inceptos
nam ipsum platea felis mi arcu consequat velit viverra, facilisis
ulputate semper vitae suspendisse aliquam, amet proin potenti semper
$ tail -n 1 loremipsum.txt
ulputate semper vitae suspendisse aliquam, amet proin potenti semper
- 在第二步中,我们发现
head和tail命令之间的一个关键区别。tail能够监控文件,并持续将文件的尾部内容输出到标准输出(stdout)。如果文件发生读取错误,或者被移动/旋转出去,-f(小写)通常会停止输出信息,而-F会重新打开文件并继续输出内容。
如果需要监视系统日志,通常在两者之间选择 -F 而非 -f。
- 在
tail命令仍然以连续模式运行的情况下,输出中应该会出现一些新的条目。这个示例来自于系统的无线适配器被强制重新连接到标准接入点(AP)时:
Dec 8 14:21:40 moon kernel: userif-2: sent link up event.
Dec 8 14:21:40 moon kernel: wlp3s0: authenticate with 18:d6:c7:fa:26:b0
Dec 8 14:21:40 moon kernel: wlp3s0: send auth to 18:d6:c7:fa:26:b0 (try 1/3)
Dec 8 14:21:40 moon kernel: wlp3s0: authenticated
Dec 8 14:21:40 moon kernel: wlp3s0: associate with 18:d6:c7:fa:26:b0 (try 1/3)
Dec 8 14:21:40 moon kernel: wlp3s0: RX AssocResp from 18:d6:c7:fa:26:b0 (capab=0x411 status=0 aid=1)
Dec 8 14:21:40 moon kernel: wlp3s0: associated
Dec 8 14:21:40 moon kernel: bridge-wlp3s0: device is wireless, enabling SMAC
Dec 8 14:21:40 moon kernel: userif-2: sent link down event.
Dec 8 14:21:40 moon kernel: userif-2: sent link up event.
-
杀死
tail命令后,控制台应该会恢复到提示符$。 -
运行
more并使用空格键或 Enter 键可以浏览整个loremipsum.txt文件。more命令只能从文件开头到末尾查看,无法前后跳转。 -
less命令无疑更强大,允许用户通过多个键盘组合来浏览loremipsum.txt文件,还提供了搜索功能等其他特性。
按名称和/或扩展名搜索文件
当我们有大量文件需要查看时,有时需要在众多文件中找到一个文件,而不使用图形界面搜索工具,或者提供更好的细粒度筛选器以减少返回的结果。要在命令行中搜索,有几个工具/命令可以使用:
-
locate(也是updatedb命令的兄弟命令):用于通过文件索引更高效地查找文件 -
find:用于在特定目录中查找具有特定属性、扩展名甚至名称的文件
find 命令更适合用于命令行,并且广泛应用(通常在嵌入式设备上),而 locate 命令则是桌面、笔记本和服务器常用的工具。Locate 要简单得多,它通过递归索引所有配置跟踪的文件,并且能够快速生成文件列表。文件索引可以通过以下命令更新:
$ sudo updatedb
在第一次更新数据库,或者在大量文件被创建、移动或复制后,更新数据库的时间可能会长于平均时间。保持数据库自动频繁更新的一种特定机制是通过使用 cron 调度器。更多关于此主题的内容将在后续介绍。
locate 命令也可以在报告文件位置之前测试文件是否存在(数据库可能已经过时),并且可以限制返回的条目数量。
如前所述,find 没有一个复杂的数据库,但它有许多用户可配置的标志,这些标志可以在执行时传递给它。一些与 find 命令一起使用的常用标志如下:
-
-type:用于指定文件类型,可以是文件或目录 -
-delete:用于删除文件,但可能不可用,这意味着需要使用exec -
-name:用于指定按名称搜索功能 -
-exec:用于指定匹配后的操作 -
-{a,c,m}time:用于查找访问、创建和修改时间等信息 -
-d, -depth:用于指定搜索的递归深度 -
-maxdepth:用于指定每次递归的最大深度 -
-mindepth:用于指定递归搜索时的最小深度 -
-L,-H,-P:按顺序,-L跟随符号链接,-H除特定情况外不跟随符号链接,-P永远不跟随符号链接 -
-print,-print0:这些命令用于打印当前文件的名称到标准输出 -
!,-not:用于指定逻辑操作,例如匹配所有文件,但不符合此标准 -
-i:用于指定在匹配时与用户的交互,如-iname test
请注意,你的平台可能不支持 GNU find 的所有功能。这可能是由于嵌入式系统的限制、资源约束或安全原因。
准备工作
除了打开终端,还需要几个大文本文件来完成这个教程。如果你已经有了一些,太好了;如果没有,可以安装以下文件:
$ sudo apt-get install locate manpages manpages-posix
$ sudo updatedb
$ git clone https://github.com/PacktPublishing/Linux-Device-Drivers-Development.git Linux-Device-Drivers-Development # Another Packt title
$ mkdir -p ~/emptydir/makesure
如果使用 locate 命令找不到文件,数据库可能只是过时了,需要重新运行。也可能是 updatedb 没有索引如可移动媒体(如 USB 闪存)的分区,文件可能存在于那里,而不是常规系统分区。
在为这个教程做准备时,注意到两个概念不经意间被介绍了:git 和 manpages。manpages 是 Linux 中最古老的帮助文档之一,而 git 是一个版本控制系统,它简化了文件(如代码)的管理、版本控制和分发。了解如何使用它们肯定是有益的,但超出了本书的范围。如需了解更多关于 git 的信息,可以参考 Packt 另一部书籍:GIT 版本控制实用手册。
如何操作...
- 打开一个终端,运行以下命令以理解
locate命令:
$ locate stdio.h
$ sudo touch /usr/filethatlocatedoesntknow.txt /usr/filethatlocatedoesntknow2.txt
$ sudo sh -c 'echo "My dear Watson ol\'boy" > /usr/filethatlocatedoesntknow.txt'
$ locate filethatlocatedoes
$ sudo updatedb
$ locate filethatlocatedoesntknow
- 接下来,运行以下命令来演示
find的一些强大功能:
$ sudo find ${HOME} -name ".*" -ls
$ sudo find / -type d -name ".git"
$ find ${HOME} -type f \( -name "*.sh" -o -name "*.txt" \)
- 接下来,我们可以使用
&&将find命令链在一起,最终执行exec,而不是将输出传递给另一个进程、命令或脚本。尝试以下命令:
$ find . -type d -name ".git" && find . -name ".gitignore" && find . -name ".gitmodules"
$ sudo find / -type f -exec grep -Hi 'My dear Watson ol boy' {} +
- 最后,
find最常见的用途之一是删除文件,可以使用内建的-delete标志,或者通过exec与rm -rf结合使用:
$ find ~/emptydir -type d -empty -delete
$ find Linux-Device-Drivers-Development -name ".git*" -exec rm -rf {} \;
它是如何工作的...
跟我重复—"locate 很简单,但需要更新,find 在困境中有效,但功能强大且晦涩,可能会破坏系统。" 让我们继续,并开始讲解:
- 如前所述,
locate命令是一个相对简单的搜索工具,它使用数据库作为后端,该数据库包含所有文件的索引列表,便于快速高效地进行搜索。与find命令不同,locate不是实时的,find命令会搜索在执行时存在的所有内容(具体取决于提供给find的参数)。在定位stdio.h时,结果会根据你的系统不同而有所不同。然而,当我们再次运行locate时,它并不了解或包含关于/usr/filethatlocatedoesntknow.txt和/usr/filethatlocatedoesntknow2.txt文件的信息。运行updatedb会重新索引文件,之后使用locate命令将返回相应的结果。注意,locate支持部分名称或完整路径匹配:
$ locate stdio.h
/usr/include/stdio.h
/usr/include/c++/5/tr1/stdio.h
/usr/include/x86_64-linux-gnu/bits/stdio.h
/usr/include/x86_64-linux-gnu/unicode/ustdio.h
/usr/lib/x86_64-linux-gnu/perl/5.22.1/CORE/nostdio.h
/usr/share/man/man7/stdio.h.7posix.gz
$ sudo touch /usr/filethatlocatedoesntknow.txt /usr/filethatlocatedoesntknow2.txt
$ sudo sh -c 'echo "My dear Watson ol\'boy" > /usr/filethatlocatedoesntknow.txt'
$ locate filethatlocatedoes
$ sudo updatedb
$ locate filethatlocatedoes
/usr/filethatlocatedoesntknow.txt
/usr/filethatlocatedoesntknow2.txt
- 在第二步中,我们介绍了
find命令提供的一些惊人功能。
再次提醒,使用 find 执行如删除操作等可能会破坏你的系统,如果没有适当处理或没有仔细监控和过滤输入。
-
至少,
find命令应按以下方式执行:$ find ${START_SEARCH_HERE} ${OPTIONAL_PARAMETERS ...}。在第一次使用find命令时,我们从用户的主目录(${HOME}环境变量)开始搜索,然后使用通配符查找以.开头的隐藏文件。最后,我们使用-ls来创建文件列表。这并非偶然,正如你可能已经观察到的那样;你可以创建在 GUI 文件浏览器中首次查看时缺失的文件(特别是在用户的主目录中)或在控制台中(例如,除非你使用带有-a标志的ls命令)。在接下来的命令中,我们使用find -type d来查找名为.git的目录。接着,我们使用特殊符号-type f \( -name "*.sh" -o -name "*.txt" \)来查找与*.sh或*.txt匹配的文件。注意正斜杠\和括号(。我们可以使用-o -name "string"来指定多个名称匹配参数。 -
在第三步中,我们使用
find来查找子目录,使用-type d,这些子目录通常出现在克隆或导出的 git 相关目录中。我们可以按照如下格式将find命令链式连接起来:$ cmd 1 && cmd2 && cmd3 && ...。这样可以确保如果前面的命令返回为 true,接下来的命令将会执行,依此类推。然后,我们引入了-exec标志,用于在找到匹配项后执行另一个命令。在这种情况下,我们首先查找所有文件,然后立即使用grep来在文件中进行搜索。请注意grep后面的{} +。这是因为{}会被find返回的结果替换。+字符表示exec命令的结束,并将结果追加,使得rm -rf执行的次数比找到/匹配的文件总数少。 -
在最后一步,我们使用两种方法来删除文件。第一种方法使用
-delete标志,可能并非所有的find实现或版本都支持,但一旦匹配,它将删除文件。与对大量文件执行子进程rm相比,它更高效。其次,使用-exec rm -rf {} \;,我们可以以一种便捷且可移植的方式轻松删除找到的文件。然而,\;和+之间是有区别的,区别在于使用\;时,rm -rf会对每个找到/匹配的文件执行一次。使用这个命令时要小心,因为它不是交互式的。
创建两个文件的 diff 并进行补丁操作
在什么情况下你需要知道什么是 diff?或者补丁(patch)?在 Linux 世界中,它是一种确定文件差异的方法,也用于解决操作系统层面的问题(特别是如果你在 Linux 内核中遇到坏的驱动程序)。然而,对于一本食谱(cookbook)来说,diff 和补丁主要有以下几个用途:
-
当确定某个脚本或配置文件是否被修改时
-
当绘制版本之间的差异,或者将数据从旧脚本迁移到新脚本时
那么,什么是差异(diff)或差分(differential)?差异是描述两个文件(文件 A 和文件 B)之间差异的输出。文件 A 是源文件,文件 B 是假定被修改的文件。如果没有生成差异输出,那么文件 A 和 B 要么为空,要么没有差异。统一格式的差异通常看起来像这样:
$ diff -urN fileA.txt fileB.txt
--- fileA.txt 2017-12-11 15:06:49.972849620 -0500
+++ fileB.txt 2017-12-11 15:08:09.201177398 -0500
@@ -1,3 +1,4 @@
12345
-abcdef
+abcZZZ
+789aaa
有多种格式的差异,但统一格式是最流行的格式之一(并且被 FOSS(自由和开源软件)社区广泛使用)。它包含关于两个文件(A 和 B)的信息,行号及每个文件中的行数,以及添加或更改的内容。如果我们查看前面的示例,我们可以看到在原始文件中,字符串abcdef被删除(-),然后作为abcdZZZ重新添加(+)。此外,还添加了一个新行,包含789aaa(在这里也可以看到:@@ -1,3 +1,4 @@)。
补丁是一个统一差异(unified diff),包含对一个或多个文件的更改,这些更改需要按照特定的顺序或方法应用,因此补丁的概念是应用一个补丁(其中包含差异信息)的过程。一个补丁也可以由几个差异串联在一起组成。
准备工作
除了打开终端,还需要安装这两个工具:
$ sudo apt-get install patch diff
接下来,我们创建一个假的配置文件,它是从一个真实文件复制的:
$ cp /etc/updatedb.conf ~/updatedb-v2.conf
打开updatedb-v2.conf并将内容更改为如下所示:
PRUNE_BIND_MOUNTS="yes"
# PRUNENAMES=".git .bzr .hg .svn"
PRUNEPATHS="/tmp /var/spool /media /home/.ecryptfs /var/lib/schroot /media /mount"
PRUNEFS="NFS nfs nfs4 rpc_pipefs afs binfmt_misc proc smbfs autofs iso9660 ncpfs coda devpts ftpfs devfs mfs shfs sysfs cifs lustre tmpfs usbfs udf fuse.glusterfs fuse.sshfs curlftpfs ecryptfs fusesmb devtmpfs"
如果你的updatedb-v2.conf文件看起来有很大不同,请将/media /mount添加到PRUNEPATHS变量中。注意它们之间用空格分隔。
如何操作...
- 打开终端,并运行以下命令以了解
diff命令:
$ diff /etc/updatedb.conf ~/updatedb-v2.conf
$ diff -urN /etc/updatedb.conf ~/updatedb-v2.conf
- 此时,只有差异信息被输出到控制台的标准输出中,尚未创建补丁文件。要创建实际的补丁文件,请执行以下命令:
$ diff -urN /etc/updatedb.conf ~/updatedb-v2.conf > 001-myfirst-patch-for-updatedb.patch
补丁可以有很多种形式,但通常它们具有.patch扩展名,并且在前面有一个数字和一个易于理解的名称。
- 现在,在应用补丁之前,还可以进行测试,以确保结果符合预期。尝试以下命令:
$ echo "NEW LINE" > ~/updatedb-v3.conf
$ cat ~/updatedb-v2.conf >> ~/updatedb-v3.conf
$ patch --verbose /etc/updatedb.conf < 001-myfirst-patch-for-updatedb.patch
- 让我们看看使用以下命令时,当补丁应用失败会发生什么:
$ patch --verbose --dry-run ~/updatedb-v1.conf < 001-myfirst-patch-for-updatedb.patch
$ patch --verbose ~/fileA.txt < 001-myfirst-patch-for-updatedb.patch
它是如何工作的...
跟我重复——“locate 简单且需要更新,find 在遇到问题时有效,但强大且难懂,且可能会破坏一些东西。”接下来,我们继续并开始解释:
- 第一个
diff命令以简单的差异格式输出更改。但是,在第二个运行diff命令时,我们使用了-urN标志。-u表示统一格式,-r表示递归,-N表示新文件:
$ diff /etc/updatedb.conf ~/updatedb-v2.conf
3c3
< PRUNEPATHS="/tmp /var/spool /media /home/.ecryptfs /var/lib/schroot"
---
> PRUNEPATHS="/tmp /var/spool /media /home/.ecryptfs /var/lib/schroot /media /mount"
$ diff -urN /etc/updatedb.conf ~/updatedb-v2.conf
--- /etc/updatedb.conf 2014-11-18 02:54:29.000000000 -0500
+++ /home/rbrash/updatedb-v2.conf 2017-12-11 15:26:33.172955754 -0500
@@ -1,4 +1,4 @@
PRUNE_BIND_MOUNTS="yes"
# PRUNENAMES=".git .bzr .hg .svn"
-PRUNEPATHS="/tmp /var/spool /media /home/.ecryptfs /var/lib/schroot"
+PRUNEPATHS="/tmp /var/spool /media /home/.ecryptfs /var/lib/schroot /media /mount"
PRUNEFS="NFS nfs nfs4 rpc_pipefs afs binfmt_misc proc smbfs autofs iso9660 ncpfs coda devpts ftpfs devfs mfs shfs sysfs cifs lustre tmpfs usbfs udf fuse.glusterfs fuse.sshfs curlftpfs ecryptfs fusesmb devtmpfs"
- 现在,我们已经通过将标准输出重定向到
001-myfirst-patch-for-updatedb.patch文件来创建了一个补丁:
$ diff -urN /etc/updatedb.conf ~/updatedb-v2.conf > 001-myfirst-patch-for-updatedb.patch
- 现在我们已经创建了
~/updatedb-v3的修改版本,注意到干运行时的任何情况吗?忽略/etc/updatedb.conf只有只读权限(我们只是为了举例而使用它,因为干运行本身不会改变内容),我们可以看到 HUNK #1 已经成功应用。hunk表示差异的一个部分,你可以为一个文件或多个文件拥有多个 hunk。在补丁中的行号没有完全匹配吗?它仍然应用了补丁,因为它知道足够的信息,并修正了数据,使其匹配以便成功应用。在处理大文件时要注意这种功能,这些文件可能有相似的匹配标准:
$ patch --verbose --dry-run /etc/updatedb.conf < 001-myfirst-patch-for-updatedb.patch
Hmm... Looks like a unified diff to me...
The text leading up to this was:
--------------------------
|--- /etc/updatedb.conf 2014-11-18 02:54:29.000000000 -0500
|+++ /home/rbrash/updatedb-v2.conf 2017-12-11 15:26:33.172955754 -0500
--------------------------
File /etc/updatedb.conf is read-only; trying to patch anyway
checking file /etc/updatedb.conf
Using Plan A...
Hunk #1 succeeded at 1.
done
- 如果我们尝试将补丁应用到与文件不匹配的文件上,它将失败,像下面的输出所示(如果指定了
--dry-run)。如果没有指定--dry-run,失败将被存储在一个拒绝文件中,正如这一行所述:1 out of 1 hunk FAILED -- saving rejects to file /home/rbrash/fileA.txt.rej:
$ patch --verbose --dry-run /etc/updatedb.conf1 < 001-myfirst-patch-for-updatedb.patch
Hmm... Looks like a unified diff to me...
The text leading up to this was:
--------------------------
|--- /etc/updatedb.conf 2014-11-18 02:54:29.000000000 -0500
|+++ /home/rbrash/updatedb-v2.conf 2017-12-11 15:26:33.172955754 -0500
--------------------------
checking file /etc/updatedb.conf1
Using Plan A...
Hunk #1 FAILED at 1.
1 out of 1 hunk FAILED
done
$
$ patch --verbose ~/fileA.txt < 001-myfirst-patch-for-updatedb.patch
Hmm... Looks like a unified diff to me...
The text leading up to this was:
--------------------------
|--- /etc/updatedb.conf 2014-11-18 02:54:29.000000000 -0500
|+++ /home/rbrash/updatedb-v2.conf 2017-12-11 15:26:33.172955754 -0500
--------------------------
patching file /home/rbrash/fileA.txt
Using Plan A...
Hunk #1 FAILED at 1.
1 out of 1 hunk FAILED -- saving rejects to file /home/rbrash/fileA.txt.rej
done
创建符号链接并有效使用它们
符号链接意味着快捷方式,对吧?如果你曾听过这个解释,它只是部分正确,它们出现在大多数现代操作系统中。事实上,从文件的角度来看,符号链接有两种类型:硬链接和软链接:
| 硬链接 | 软链接 |
|---|---|
| 仅链接文件 | 可以链接目录和文件 |
| 本地磁盘内的内容链接 | 可以跨磁盘或网络引用文件/文件夹 |
| 引用 inode/物理位置 | 如果原始文件被删除,硬链接将保留(在自己的 inode 中) |
| 移动文件仍然允许链接工作 | 如果链接文件被移动,链接将无法跟随原文件 |
软链接最可能符合你对快捷方式的预期,行为可能不会让你感到意外,但硬链接有什么用呢?使用硬链接的一个突出例子是当你不想通过移动它指向的文件来破坏链接时!软链接显然更加灵活,可以跨文件系统工作,而硬链接则不同,但如果文件被移动,软链接将无法工作。
除了创建快捷方式外,你还可以做一些巧妙的操作,比如在使用符号链接时重命名argv[0]。Busybox shell 就是一个例子,它包含通过指向./busybox的符号链接执行的applets。例如,ls指向与cd相同的二进制文件!它们都指向./busybox。这是一种巧妙的方法,可以节省空间并在不使用标志的情况下提高运行时标志的效率。
软链接也用于/usr/lib或/lib文件夹中的共享library。事实上,符号链接对于为路径创建别名或让软件与二进制文件中硬编码的路径一起工作非常有用。
如何操作...
- 打开终端,创建
whoami.sh脚本:
#!/bin/bash
VAR=$0
echo "I was ran as: $VAR"
- 执行
whoami.sh并观察发生了什么:
$ bash whoami.sh
- 接下来,使用
ln命令创建一个指向whoami.sh的软链接:
$ ln -s whoami.sh ghosts-there-be.sh
- 接下来,运行
ls-la。注意有任何不同吗?
ls -la ghosts-there-be.sh whoami.sh
- 接下来是硬链接,它是通过使用
ln命令以这种方式创建的:
$ ln ghosts-there-be.sh gentle-ghosts-there-be.sh
$ ln whoami.sh real-ghosts-there-be.sh
- 接下来,让我们看一下运行命令时结果的不同:
$ ls -la ghosts-there-be.sh whoami.sh real-ghosts-there-be.sh gentle-ghosts-there-be.sh
lrwxrwxrwx 1 rbrash rbrash 18 Dec 12 15:07 gentle-ghosts-there-be.sh -> ghosts-there-be.sh
lrwxrwxrwx 1 rbrash rbrash 9 Dec 12 14:57 ghosts-there-be.sh -> whoami.sh
-rw-rw-r-- 2 rbrash rbrash 45 Dec 12 14:56 real-ghosts-there-be.sh
-rw-rw-r-- 2 rbrash rbrash 45 Dec 12 14:56 whoami.sh
$ mv whoami.sh nobody.sh
$ bash ghosts-there-be.sh
bash: ghosts-there-be.sh: No such file or directory
$ bash real-ghosts-there-be.sh
I was ran as: real-ghosts-there-be.sh
$ bash gentle-ghosts-there-be.sh
bash: gentle-ghosts-there-be.sh: No such file or directory
它是如何工作的...
在第一步中,我们创建了whoami.sh。它类似于whoami命令,但不同之处在于,我们并没有打印$USER变量,而是打印参数0(通常被称为arg0)或$0。通俗地说,我们打印的是用于执行代码或脚本的名称。
当我们执行whoami.sh时,它会打印到控制台:
$ bash whoami.sh
I was ran as: whoami.sh
要创建符号软链接,我们使用带有-s标志(符号模式)的ln命令。ln命令需要按以下方式执行:$ ln -s 原始文件路径 新文件路径。
正如我们在以下代码中看到的,执行ghosts-there-be.sh会运行whoami.sh中的代码,但arg0是ghosts-there-be.sh。然后,当运行带有-l -a标志(-la)的ls命令时,我们可以看到指向whoami.sh的软链接。注意它只有 9 字节这么小!
$ bash ghosts-there-be.sh
I was ran as: ghosts-there-be.sh
$ ls -la ghosts-there-be.sh whoami.sh
lrwxrwxrwx 1 rbrash rbrash 9 Dec 12 14:57 ghosts-there-be.sh -> whoami.sh
-rw-rw-r-- 1 rbrash rbrash 45 Dec 12 14:56 whoami.sh
接下来,我们通过不带-s标志的ls命令创建一个硬链接。
硬链接real-ghosts-there-be.sh执行与ghosts-there-be.sh相同的内容,但指向whoami.sh的实际内容,即使它被移动并重命名为nobody.sh:
$ ls -la ghosts-there-be.sh whoami.sh real-ghosts-there-be.sh gentle-ghosts-there-be.sh
lrwxrwxrwx 1 rbrash rbrash 18 Dec 12 15:07 gentle-ghosts-there-be.sh -> ghosts-there-be.sh
lrwxrwxrwx 1 rbrash rbrash 9 Dec 12 14:57 ghosts-there-be.sh -> whoami.sh
-rw-rw-r-- 2 rbrash rbrash 45 Dec 12 14:56 real-ghosts-there-be.sh
-rw-rw-r-- 2 rbrash rbrash 45 Dec 12 14:56 whoami.sh
mv whoami.sh nobody.sh
$ bash ghosts-there-be.sh
bash: ghosts-there-be.sh: No such file or directory
$ bash real-ghosts-there-be.sh
I was ran as: real-ghosts-there-be.sh
$ bash gentle-ghosts-there-be.sh
bash: gentle-ghosts-there-be.sh: No such file or directory
$ bash gentle-ghosts-there-be.sh
bash: gentle-ghosts-there-be.sh: No such file or directory
爬取文件系统目录并打印树状结构
到目前为止,我们已经了解了 locate、find 和 grep(以及正则表达式),但如果我们想要创建一个简单的目录爬虫/抓取器/索引器呢?它肯定不是最快的,也没有优化,但我们可以使用递归功能和文件测试来打印树状结构。
这个练习是一个有趣的练习,当然也算是在重做“轮子”。通过运行 tree 命令可以轻松做到这一点,然而,这在接下来的练习中将会有用,我们将构建文件的数组数组。
准备工作
除了打开终端,让我们创建一些测试数据:
$ mkdir -p parentdir/child_with_kids
$ mkdir -p parentdir/second_child_with_kids
$ mkdir -p parentdir/child_with_kids/grand_kid/
$ touch parentdir/child.txt parentdir/child_with_kids/child.txt parentdir/child_with_kids/grand_kid/gkid1.txt
$ touch parentdir/second_child_with_kids/cousin1.txt parentdir/z_child.txt parentdir/child.txt parentdir/child2.txt
如何做...
- 打开终端并创建
mytree.sh脚本:
#!/bin/bash
CURRENT_LVL=0
function tab_creator() {
local X=0
local LVL=$1
local TABS="."
while [ $X -lt $LVL ]
do
# Concatonate strings
TABS="${TABS}${TABS}"
X=$[$X+1]
done
echo -en "$TABS"
}
function recursive_tree() {
local ENTRY=$1
for LEAF in ${ENTRY}/*
do
if [ -d $LEAF ];then
# If LEAF is a directory & not empty
TABS=$(tab_creator $CURRENT_LVL)
printf "%s\_ %s\n" "$TABS" "$LEAF"
CURRENT_LVL=$(( CURRENT_LVL + 1 ))
recursive_tree $LEAF $CURRENT_LVL
CURRENT_LVL=$(( CURRENT_LVL - 1 ))
elif [ -f $LEAF ];then
# Print only the bar and not the backwards slash
# And only if a file
TABS=$(tab_creator $CURRENT_LVL)
printf "%s|_%s\n" "$TABS" "$LEAF"
continue
fi
done
}
PARENTDIR=$1
recursive_tree $PARENTDIR 1
- 在你的终端中,现在运行:
$ bash mytree.sh parentdir
它是如何工作的...
创建mytree.sh是一个微不足道的任务,但其中的逻辑遵循递归函数。还有${CURRENT_LVL}的概念,它用于表示脚本从最初起点parentdir开始的深度(即期间的数量或层级)。在每个目录中,我们创建一个 for 循环来测试其中的每个文件/目录。逻辑会测试该条目是文件还是目录。如果是目录,我们递增${CURRENT_LVL},然后递归地执行recursive_tree函数中的相同逻辑,直到完成并返回。如果是文件,我们只是打印出来并继续。tab_creator函数根据${CURRENT_LVL}和拼接生成表示期间的变量字符串。
执行脚本时应产生类似如下的输出,但请注意脚本会记住它可能有多少层深度,并且目录显示时使用的是\_而不是|_:
$ bash mytree.sh parentdir
.|_parentdir/child2.txt
.|_parentdir/child.txt
.\_ parentdir/child_with_kids
..|_parentdir/child_with_kids/child.txt
..\_ parentdir/child_with_kids/grand_kid
....|_parentdir/child_with_kids/grand_kid/gkid1.txt
.\_ parentdir/empty_dir
.\_ parentdir/second_child_with_kids
..|_parentdir/second_child_with_kids/cousin1.txt
.|_parentdir/z_child.txt
查找和删除重复的文件或目录
曾经我们已经讨论过检查文件中字符串是否唯一以及是否可以对其进行排序,但我们还没有对文件进行类似的操作。然而,在深入之前,让我们对什么构成重复文件做一些假设:重复文件是指可能有不同名称,但内容与其他文件相同的文件。
调查文件内容的一种方法是移除所有空白字符,仅检查文件中包含的字符串,或者我们也可以仅使用SHA/512sum和MD/5sum等工具生成文件内容的唯一哈希值(可以理解为充满乱码的唯一字符串)。整体流程如下:
-
使用这个哈希值,我们可以将其与已经计算的哈希值列表进行比较。
-
如果哈希值匹配,我们已经见过这个文件的内容,因此可以将其删除。
-
如果哈希值是新的,我们可以记录该条目并继续计算下一个文件的哈希值,直到所有文件都被哈希化。
使用哈希不需要你了解数学是如何运作的,而是要了解它在安全实现的情况下应该如何工作,并且要有足够的可能性使得找到重复项在计算上不可行。哈希应该是单向的,这意味着它不同于加密/解密,因此一旦哈希值被创建,就不应该能够从哈希值本身确定原始输入。
MD5 哈希被认为是完全不安全的(尽管在安全要求较低的场合仍有其用途),而 SHA1/2 被认为可能会随着 SHA3 中的 SPONGE 算法的使用而逐渐失宠(在可能的情况下使用 SHA3)。更多信息,请参见NIST 指南。
准备工作
打开终端并使用dsetmkr.sh脚本创建一个包含多个文件的数据集:
$ #!/bin/bash
BDIR="files_galore"
rm -rf ${BDIR}
mkdir -p ${BDIR}
touch $BDIR/file1; echo "1111111111111111111111111111111" > $BDIR/file1;
touch $BDIR/file2; echo "2222222222222222222222222222222" > $BDIR/file2;
touch $BDIR/file3; echo "3333333333333333333333333333333" > $BDIR/file3;
touch $BDIR/file4; echo "4444444444444444444444444444444" > $BDIR/file4;
touch $BDIR/file5; echo "4444444444444444444444444444444" > $BDIR/file5;
touch $BDIR/sameas5; echo "4444444444444444444444444444444" > $BDIR/sameas5;
touch $BDIR/sameas1; echo "1111111111111111111111111111111" > $BDIR/sameas1;
然后,在开始编写脚本之前,需要讨论一个核心概念,即数组是静态的还是动态的;如果性能是目标,了解数组实现的核心原理是一个关键原则。
数组非常有用,但 Bash 脚本的性能通常不如编译程序或选择具有适当数据结构的语言。在 Bash 中,数组是链表且是动态的,这意味着如果你调整数组的大小,不会有巨大的性能损失。
对于我们的目的,我们将创建一个动态数组,一旦数组变得相当大,搜索该数组将成为性能瓶颈。这种简单的迭代方法通常在一个任意的数量(假设为N)之前效果良好,在这个点上,使用其他机制的好处可能会超过当前方法的简单性。对于那些想了解更多数据结构及其性能的人,可以查阅大 O 符号和复杂度理论。
如何做到……
- 打开终端,并创建
file-deduplicator.sh脚本。
以下是脚本的代码片段:
#!/bin/bash
declare -a FILE_ARRAY=()
function add_file() {
# echo $2 $1
local NUM_OR_ELEMENTS=${#FILE_ARRAY[@]}
FILE_ARRAY[$NUM_OR_ELEMENTS+1]=$1
}
function del_file() {
rm "$1" 2>/dev/null
}
- 如果还未执行,请运行
setup命令:运行$ bash dsetmkr.sh,然后运行$ bash ./file-deduplicator.sh。在提示符处输入files_galore/并按Enter键:
$ bash dsetmkr.sh
$ bash file-deduplicator.sh
Enter directory name to being searching and deduplicating:
Press [ENTER] when ready
files_galore/
#1 f559f33eee087ea5ac75b2639332e97512f305fc646cf422675927d4147500d4c4aa573bd3585bb866799d08c373c0427ece87b60a5c42dbee9c011640e04d75
#2 f7559990a03f2479bf49c85cb215daf60417cb59875b875a8a517c069716eb9417dfdb907e50c0fd5bd47127105b7df9e68a0c45a907dc5254ce6bc64d7ec82a
#3 2811ce292f38147613a84fdb406ef921929f864a627f78ef0ef16271d4996ed598d0f5c5f410f7ae75f9902ff0f63126b567e5f24882db3686be81f2a79f1bb3
#4 89f5df2b9f4908adca6a36f92b344d4a8ff96d04184e99d8dd31a86e96d45a1aa16a8b574d5815f17d649d521c9472670441a56f54dc1c2640e20567581d9b4e
- 审查结果并验证
files_galore的内容。
$ ls files_galore/
它是如何工作的……
在开始之前,请注意:file-deduplicator.sh脚本会删除它所针对目录中的重复文件。
- 在开始时(特别是使用
dsetmkr.sh脚本),我们将生成一个名为files_galore的目录,并且该目录包含几个文件:四个是唯一的,三个包含重复的内容:
$ bash dsetmkr.sh
密码学、安全性和数学的研究都是非常有趣且广泛的信息领域!哈希值有许多其他用途,例如文件的完整性检查、查找值以快速找到数据、唯一标识符等。
- 当你运行
file-deduplicator.sh时,它会首先通过read命令向用户请求输入,然后打印出四个不同的值,显示看似随机的字符字符串。看似随机是完全正确的——它们是 SHA512 哈希值!每个字符串是其内部内容的哈希值。即使内容仅有微小的差别(例如,一个比特从0翻转成1),也会生成一个完全不同的哈希值。再次强调,这个 bash 脚本利用了数组这一外部概念(使用全局数组变量意味着在脚本的任何地方都可以访问),并结合SHA512sum工具和awk来检索正确的值。这个脚本并不是递归的,它只查看files_galore中的文件,以生成一个文件列表,每个文件一个哈希值,并搜索一个包含所有已知哈希值的数组。如果一个哈希值是未知的,那么它代表一个新文件,并会被插入到数组中进行存储。否则,如果一个哈希值出现了两次,文件将被删除,因为它包含了重复的内容(即使文件名不同)。还有一个方面是使用返回值作为字符串。如你所记得,返回值只能返回数字值:
$ bash file-deduplicator.sh
Enter directory name to being searching and deduplicating:
Press [ENTER] when ready
files_galore/
#1 f559f33eee087ea5ac75b2639332e97512f305fc646cf422675927d4147500d4c4aa573bd3585bb866799d08c373c0427ece87b60a5c42dbee9c011640e04d75
#2 f7559990a03f2479bf49c85cb215daf60417cb59875b875a8a517c069716eb9417dfdb907e50c0fd5bd47127105b7df9e68a0c45a907dc5254ce6bc64d7ec82a
#3 2811ce292f38147613a84fdb406ef921929f864a627f78ef0ef16271d4996ed598d0f5c5f410f7ae75f9902ff0f63126b567e5f24882db3686be81f2a79f1bb3
#4 89f5df2b9f4908adca6a36f92b344d4a8ff96d04184e99d8dd31a86e96d45a1aa16a8b574d5815f17d649d521c9472670441a56f54dc1c2640e20567581d9b4e
- 执行操作后,我们可以看到
files_galore目录中只剩下四个文件,原本的七个文件中的重复数据已被移除!
$ ls files_galore/
file1 file2 file3 file4
在任意位置连接和拆分文件
让我们不要害羞!谁曾经因意外或故意使用应用程序打开一个大文件,结果没有按计划进行的?我肯定有过,而且我也确实见过一些限制,比如在 Excel 或 OpenOffice 计算器中加载的行数。在这些情况下,我们使用一个方便的工具,它可以在任意位置拆分文件,类似下面这样:
-
在 X 行之前
-
在 Z 字节/字符之前
在这个教程中,你将创建一个具有双重用途的脚本:一个可以使用输入文件并生成拆分或多个文件的脚本,另一个是使用合并方法将文件合并的脚本。传递字符串变量时有一些注意事项:
-
有时可能会丢失特殊字符,如换行符
-
(二进制文件)应该由与常规命令不同的工具处理
这个文件还重用了我们在第一章《Bash 快速入门》中看到的getopts参数解析,但它还引入了mktemp命令和带有PAGESIZE参数的getconf命令。Mktemp是一个有用的命令,因为它可以生成位于/tmp目录中的唯一临时文件,甚至可以生成遵循模板的唯一文件(注意XXX——它会被替换为随机值,但uniquefile.将保持不变):
$ mktemp uniquefile.XXXX
另一个有用的命令是getconf编程工具,它是一个符合标准的工具,用于获取有用的系统变量。特别是其中一个叫做PAGESIZE的变量,它有助于确定内存的块大小。显然,这是非常简单的描述,但选择合适的大小来写入数据在性能上是非常有益的。
准备就绪
除了打开一个终端外,还需要创建一个名为input-lines的文本文件,文件内容如下(每行一个字符):
1
2
3
4
5
6
7
8
9
0
a
b
c
d
e
f
g
h
i
j
k
接下来,创建另一个名为merge-lines的文件,内容如下:
It's -17 outside
如何做到...
打开终端并创建一个名为file-splitter.sh的脚本。
以下是代码片段:
#!/bin/bash
FNAME=""
LEN=10
TYPE="line"
OPT_ERROR=0
set -f
function determine_type_of_file() {
local FILE="$1"
file -b "${FILE}" | grep "ASCII text" > /dev/null
RES=$?
if [ $RES -eq 0 ]; then
echo "ASCII file - continuing"
else
echo "Not an ASCII file, perhaps it is Binary?"
fi
}
接下来,使用以下命令和标志(-i,-t,-l)运行file-splitter.sh:
$ bash file-splitter.sh -i input-lines -t line -l 10
查看输出并观察-t size与-l line使用时的差异。当使用-l 1或-l 100时会有什么不同?记得使用$ rm input-lines.*删除拆分后的文件:
$ rm input-lines.*
$ bash file-splitter.sh -i input-lines -t line -l 10
$ rm input-lines.*
$ bash file-splitter.sh -i input-lines -t line -l 1
$ rm input-lines.*
$ bash file-splitter.sh -i input-lines -t line -l 100
$ rm input-lines.*
$ bash file-splitter.sh -i input-lines -t size -l 10
在下一步中,创建另一个名为file-joiner.sh的脚本。
以下是代码片段:
#!/bin/bash
INAME=""
ONAME=""
FNAME=""
WHERE=""
OPT_ERROR=0
TMPFILE1=$(mktemp)
function determine_type_of_file() {
local FILE="$1"
file -b "${FILE}" | grep "ASCII text" > /dev/null
RES=$?
if [ $RES -eq 0 ]; then
echo "ASCII file - continuing"
else
echo "Not an ASCII file, perhaps it is Binary?"
fi
}
接下来,使用以下命令运行脚本:
$ bash file-joiner.sh -i input-lines -o merge-lines -f final-join.txt -w 2
它是如何工作的...
在继续之前,请注意,final-join.txt上的类型选项(-t)在一次读取字符时会忽略\n换行符。read足以满足本教程的需求,但读者应意识到,read/cat并不是这种工作类型的最佳工具。
-
创建这个脚本很简单,大部分情况下它看起来不会像是来自火星的作品。
-
运行命令
$ bash file-splitter.sh -i input-lines -t line -l 10应该会生成三个文件,文件名为 input-lines {1,...,3}。之所以有三个文件,是因为如果使用的是相同的输入,即 22 行数据,它会生成三个文件(10+10+2)。使用read和echo配合连接缓冲区(${BUFFER}),我们可以根据特定的标准(由-l提供)将数据写入文件。如果遇到EOF(文件结束)并且循环完成,我们需要将缓冲区写入文件,因为它可能低于写入标准的阈值——这会导致splitter脚本生成的最后一个文件中的字节丢失或缺失。
$ bash file-splitter.sh -i input-lines -t line -l 10
ASCII file - continuing
Wrote buffer to file: input-lines.1
Wrote buffer to file: input-lines.2
Wrote buffer to file: input-lines.3
-
根据
-l标志的使用情况,值为1时会为每一行生成一个文件,值为 100 时则会生成一个单独的文件,因为它符合阈值要求。使用辅助功能-t size,可以根据字节进行拆分,但read命令有一个不幸的副作用:当我们传递缓冲区时,它会被修改,导致新行丢失。如果我们使用类似dd的工具,这类操作会更好,因为dd更适合复制、写入以及创建原始数据到文件或设备中。 -
接下来,我们创建了一个名为
file-joiners.sh的脚本。它同样使用了getopts,并需要四个输入参数:-i originalFile -o,otherFileToMerge -f,finalMergedFile -w,以及whereInjectTheOtherFile。该脚本相对简单,但使用了mktemp命令来创建一个临时文件,作为存储缓冲区,而不会修改原文件。当完成操作后,我们可以使用mv命令将文件从/tmp移动到终端当前目录(.)。mv命令也可以用于重命名文件,并且通常比cp命令更快(虽然在这个例子中差别不大),因为它不执行复制操作,而只是进行文件系统级的重命名。 -
使用
cat查看final-join.txt应该显示如下输出:
$ cat final-join.txt
1
2
It's -17 outside
3
4
5
6
7
8
9
0
a
b
c
d
e
f
g
h
i
j
k
生成各种大小的数据集和随机文件
通常,模仿真实数据的数据总是最好的,但有时我们需要各种内容和大小的文件集合来进行验证测试,而不希望有任何延迟。想象一下,你有一个 Web 服务器,并且它正在运行某个应用程序,该应用程序接收文件进行存储。然而,文件的大小有限制。要是能够瞬间搞定一批文件,不是很棒吗?
为此,我们可以使用一些文件系统功能,比如/dev/random和一个有用的程序dd。dd命令是一个用于转换和复制文件的工具(包括设备,因为在 Linux 中一切皆文件)。它可以在后续的教程中用于备份 SD 卡上的数据(记得你最爱的 Raspberry Pi 项目吗?),或者逐字节读取文件而不丢失数据。dd的典型最小用法为$ dd if="inputFile" of="outputFile" bs=1M count=10。从这个命令中,我们可以看到:
-
if=:表示输入文件 -
of=:表示输出文件 -
bs=:表示块大小 -
count=:表示要复制的块数
如果要执行文件的纯复制(1:1),则选项bs=和count=是可选的,因为dd命令会尝试使用合理有效的参数来提供足够的性能。dd命令还具有许多其他选项,如seek=,将在另一个配方中介绍,用于执行低级备份时通常不需要 count 选项,因为通常复制整个文件而不是部分文件(在执行备份时)。
/dev/random是 Linux 中的一个设备(因此使用/dev路径),可用于生成用于脚本或应用程序中的随机数。还有其他/dev路径,如控制台和各种适配器(例如,USB 存储设备或鼠标),所有这些都可能是可访问的,建议您了解它们。
做好准备
为此配方做好准备,请按以下步骤安装dd命令,并创建一个名为qa-data/的新目录:
$ sudo apt-get install dd bsdmainutils
$ mkdir qa-data
本教程使用dmesg命令,该命令用于返回系统信息,如接口状态或系统引导过程。它几乎在所有系统上都存在,因此是合理的系统级“lorem ipsum”的良好替代。如果您希望使用其他类型的随机文本或词典,则可以轻松替换dmesg!另外使用的两个命令是seq和hexdump。seq命令可以使用指定的增量从起始点生成一个n个数字的数组,而hexdump会以十六进制格式生成二进制(或可执行)的人类可读表示。
如何执行它……
打开终端并创建一个名为data-maker.sh的新脚本。
以下是脚本的代码片段:
#!/bin/bash
N_FILES=3
TYPE=binary
DIRECTORY="qa-data"
NAME="garbage"
EXT=".bin"
UNIT="M"
RANDOM=$$
TMP_FILE="/tmp/tmp.datamaker.sh"
function get_random_number() {
SEED=$(($(date +%s%N)/100000))
RANDOM=$SEED
# Sleep is needed to make sure that the next time rnadom is ran, everything is good.
sleep 3
local STEP=$1
local VARIANCE=$2
local UPPER=$3
local LOWER=$VARIANCE
local ARR;
INC=0
for N in $( seq ${LOWER} ${STEP} ${UPPER} );
do
ARR[$INC]=$N
INC=$(($INC+1))
done
RAND=$[$RANDOM % ${#ARR[@]}]
echo $RAND
}
让我们使用以下命令开始执行脚本。它使用-t标志指定类型为text,使用-n来指定文件数量为5,-l设置下限为 1 个字符,-u设置为1000个字符:
$ bash data-maker.sh -t text -n 5 -l 1 -u 1000
要检查输出,请使用以下命令:
$ ls -la qa-data/*.txt
$ tail qa-data/garbage4.txt
再次运行data-maker.sh脚本,但是这次生成的是二进制文件。大小限制不再是 1 个字符(1 字节)或1000个字符(1000字节或略少于 1 千字节),而是以 MB 为单位,生成1到10 MB 的文件:
$ bash data-maker.sh -t binary -n 5 -l 1 -u 10
要查看输出,请使用以下命令。由于我们无法像处理“常规”ASCII 文本文件那样“转储”或“cat”二进制文件,因此使用了一个名为hexdump的新命令:
$ ls -la qa-data/*.bin
$ hexdump qa-data/garbage0.bin
0000000 0000 0000 0000 0000 0000 0000 0000 0000
*
它的工作原理……
让我们了解一下,事情是如何发生的:
-
首先,我们创建了
;data-maker.sh脚本。这个脚本引入了几个新概念,包括一直令人着迷的随机化概念。在计算机中,或者说在生活中的任何事情里,真正的随机事件或数字生成是无法实现的,它们需要几个数学原理,如熵。虽然这超出了本书的范围,但需要知道的是,无论是随机重复使用还是初次使用时,都应该给它一个唯一的初始化向量或种子。通过使用for循环,我们可以利用seq命令构建一个数字数组。一旦数组构建完成,我们从中选择一个“随机”的值。在每种类型的文件输出操作(无论是二进制文件还是文本文件)中,我们大致确定最小值(-l或下限)和最大值(-u或上限),以控制输出数据的大小。 -
在第 2 步中,我们使用
dmesg的输出和我们的伪随机化过程创建了5个文本文件。我们可以看到,我们通过dd命令迭代,直到创建了五个不同大小和起始点的文本文件。 -
在第 3 步中,我们验证了确实创建了五个文件,并且在第五个文件中,我们查看了
garbage4.txt文件的tail部分。 -
在第 4 步中,我们使用
dd命令创建了五个二进制文件(全是零)。我们没有使用字符数,而是使用了兆字节(MB)作为单位。 -
在第 5 步中,我们验证了确实创建了五个二进制文件,并且在第五个文件中,我们使用
hexdump命令查看了二进制文件的内容。hexdump命令创建了一个简化的“转储”,展示了garbage0.bin文件中的所有字节。
第四章:让脚本像守护进程一样运行
在本章中,我们将介绍以下主题:
-
使用循环结构或递归使程序持续运行(永远)
-
在注销后保持程序/脚本运行
-
在需要权限时调用命令
-
清理用户输入并确保结果可重复
-
使用 select 创建一个简单的多级用户菜单
-
生成和捕获信号以进行清理
-
在程序中使用临时文件和锁文件
-
利用超时等待命令完成
-
创建文件输入-文件输出程序并并行运行进程
-
在启动时执行脚本
介绍
本章内容是创建模拟应用程序功能的组件,例如菜单或守护进程。为了实现这一点,我们先停下来思考一下:什么定义了一个应用程序或守护进程?是菜单吗?是能够永远运行吗?还是能够在后台无头运行?所有这些都定义了一个应用程序可能表现出的行为,但没有什么能阻止脚本也具备这些行为!
例如,如果一个 bash 脚本没有扩展名(例如,.sh),并且没有显式通过 Bash 解释器运行,你怎么知道第一次检查时它是一个脚本而不是一个二进制文件?虽然有很多方法可以确定,比如打开文件或使用file命令,但表面上看,脚本和程序可能是一样的!
使用循环结构或递归使程序持续运行(永远)
到目前为止,本食谱大多展示了只执行单一任务并在任务完成后退出的脚本。这非常适合一次性脚本,但如果我们希望通过菜单执行多个脚本,或者在后台自动执行任务而无需每次通过调度进程(如 cron)执行,应该怎么做呢?本食谱介绍了几种方法,使脚本能够永远运行,直到被杀死或退出。
准备工作
除了保持终端开启,我们还需要记住几个概念:
-
递归函数与提示符结合(例如,
read命令)可以产生一个根据用户输入循环的脚本。 -
循环结构如
for、while和until可以以一种方式执行,使得条件永远无法满足,从而无法退出
因此,循环或某些会引发循环的东西会强制程序运行一个不确定的时间,直到发生退出事件。
在许多编程语言中,程序将通过主函数的概念来执行。在这个main函数中,程序员通常会创建所谓的运行循环,这使得程序能够永远运行(即使它什么也不做)。
使用递归方法时,操作可能如下所示:
-
脚本或程序进入递归函数
-
递归函数可以继续无限次调用自身,或者等待一个阻塞输入(例如,
read命令) -
基于
read命令提供的输入,您可以再次调用相同的函数。 -
回到步骤 1,直到退出
另外,循环机制相似,不同的是函数不一定会执行。一个有效不可达的条件下的循环将持续运行,除非某些东西中断了执行(例如,sleep)。
一个持续运行的循环将消耗 CPU 资源,这些资源本可以用于其他地方,或浪费 CPU 周期。如果你在电池供电或资源受限的平台上运行,最好避免不必要的额外 CPU 活动。
使用 sleep 命令是在简单脚本中使用循环时限制 CPU 使用率的绝佳方式。然而,如果你运行一个长时间的脚本,时间会累积!
如何操作...
让我们按以下步骤开始我们的活动:
- 打开终端并创建
recursive_read_input.sh脚本:
#!/bin/bash
function recursive_func() {
echo -n "Press anything to continue loop "
read input
recursive_func
}
recursive_func
exit 0
-
执行
$ bash recursive_read_input.sh脚本——在提示符下按 Enter 并等待下一个提示符。 -
使用 Ctrl + C 退出程序。
-
打开终端并创建
loop_for_input.sh脚本:
#!/bin/bash
for (( ; ; ))
do
echo "Shall run for ever"
sleep 1
done
exit 0
-
执行
$ bash loop_for_input.sh脚本——在提示符下按 Enter 并等待下一个提示符。 -
使用 Ctrl + C 退出程序。
-
打开终端并创建
loop_while_input.sh脚本:
#!/bin/bash
EXIT_PLEASE=0
while : # Notice no conditions?
do
echo "Pres CTRL+C to stop..."
sleep 1
if [ $EXIT_PLEASE != 0 ]; then
break
fi
done
exit 0
-
执行
$ bash loop_while_input.sh脚本——在提示符下按 Enter 并等待下一个提示符。 -
使用 Ctrl + C 退出程序。
-
打开终端并创建
loop_until_input.sh脚本:
#!/bin/bash
EXIT_PLEASE=0
until [ $EXIT_PLEASE != 0 ] # EXIT_PLEASE is set to 0, until will never be satisfied
do
echo "Pres CTRL+C to stop..."
sleep 1
done
exit 0
-
执行
$ bash loop_until_input.sh脚本——在提示符下按 Enter 并等待下一个提示符。 -
使用 Ctrl + C 退出程序。
它是如何工作的...
让我们详细了解一下我们的脚本:
-
创建
recursive_read_input.sh脚本是一个简单的过程。我们可以看到,read命令期待输入(并将其存储在$input变量中),然后脚本会在每次read执行完后再次调用recursive_func(),并且对 每次 输入都会如此。 -
执行
$ bash recursive_read_input.sh脚本将使脚本无限运行。无论输入什么,Ctrl + C 或终止脚本都会退出。 -
创建
loop_for_input.sh脚本相对来说也比较简单。我们可以注意到两点:for 循环没有参数,除了(( ; ; ))和sleep命令。这将使它永远运行,但在每次执行循环时,它会向控制台输出Shall run for ever,然后休眠一秒钟再继续下一次循环。 -
执行
$ bash loop_for_input.sh脚本将导致脚本永远循环。 -
Ctrl + C 将导致脚本退出。
-
使用
loop_while_input.sh脚本时,通过使用带有: noop命令的while循环。然而,存在一个小差异,即一个if语句(该语句永远不会为真),但它仍然可以在其他脚本中使用,以设置条件,使得脚本中断while循环并退出。 -
执行
$ bash loop_while_input.sh脚本将导致脚本永远循环。 -
Ctrl + C 将导致脚本退出。
-
loop_until_input.sh脚本类似于while循环永远运行的示例,但它有所不同,因为你也可以嵌入一个条件,该条件永远不会评估为true。这将导致脚本永远循环,除非$EXIT_PLEASE变量被设置为1。 -
执行
$ bash loop_until_input.sh脚本将使脚本永远循环。 -
Ctrl + C将使脚本退出。
在退出登录后保持程序/脚本运行
在让我们的脚本作为守护进程运行之前,我们需要了解如何在用户退出登录后保持命令运行(或者更好的是,让系统自己启动它们——我们稍后会更详细地讨论)。当用户登录时,会为该用户创建一个会话,但当他们退出登录时——除非系统拥有它——进程和脚本通常会被杀死或关闭。
这个方法是关于在你退出登录后,如何让你的脚本和活动在后台继续运行。
准备工作
除了打开一个终端,我们还需要记住一些概念:
-
当用户退出登录时,当前用户拥有的所有应用程序或进程将退出(shell 会发送信号)。
-
Shell 可以配置为不向进程发送关机信号。
-
应用程序和脚本使用 stdin 和 stdout 进行常规操作。
-
后台运行的应用程序或脚本可以被称为作业。
本章的目的是不仅展示进程管理,还要教你如何操作 shell 来保持程序运行。一个巧妙的方法是使用&,它的使用方式是:$ bash runforver.sh &。不幸的是,仅使用这种技术,我们回到了原点——当我们退出时,二进制文件仍然会停止运行。因此,我们需要使用诸如screen、disown和sighup之类的程序。
并非所有系统都支持 screen 命令。建议我们使用其他命令,以防screen缺失(但它仍然很有用!)。
如何操作...
我们按以下步骤开始活动:
- 打开一个终端并创建
loop_and_print.sh脚本:
#!/bin/bash
EXIT_PLEASE=0
INC=0
until [ ${EXIT_PLEASE} != 0 ] # EXIT_PLEASE is set to 0, until will never be satisfied
do
echo "Boo $INC" > /dev/null
INC=$((INC + 1))
sleep 1
done
exit 0
- 打开一个终端并运行以下命令:
$ bash loop_and_print.sh &
$ ps aux | grep loop_and_print.sh # Take note of the PID - write it down
- 接下来,退出登录,然后在新的终端中登录并运行以下命令:
$ ps aux | grep loop_and_print.sh # Take note of the PID - write it down
- 你能找到正在运行的进程吗?接下来,运行以下命令:
$ bash loop_and_print.sh & # note the PID againg
$ disown
- 接下来,退出登录,然后在新的终端中登录并运行以下命令:
$ ps aux | grep loop_and_print.sh # Take note of the PID - write it down
- 接下来,运行以下命令:
$ nohup bash loop_and_print.sh &
- 接下来,退出登录,然后在新的终端中登录并运行以下命令:
$ ps aux | grep loop_and_print.sh # Take note of the PID - write it down
它的工作原理...
-
在第 1 步中,我们打开了一个终端并创建了
loop_and_print.sh脚本。这个脚本只是简单地永远循环,并在循环过程中打印内容。 -
以下命令将使用
loop_and_print.sh脚本并在后台作为作业运行。ps命令输出进程信息,并通过 grep 简化输出。在命令中,我们可以看到用户名列旁边的进程 ID(PID)。记下 PID,以便你可以杀死僵尸进程或停止不必要的应用程序:
$ bash loop_and_print.sh &
[1]4510
$ ps aux | grep loop_and_print.sh # Take note of the PID - write it down
rbrash 4510 0.0 0.0 12548 3024 pts/2 S 12:58 0:00 bash loop_and_print.sh
-
重新登录并运行
ps命令将产生零结果。这是因为我们用&将脚本放到后台后,已经收到关闭或终止的信号。 -
再次运行
loop_and_print.sh脚本;该命令将其放入后台,而disown将后台进程从已知的作业列表中移除。这断开了脚本和所有输出与终端的连接。 -
重新登录并使用
ps命令时,你将看到命令的 PID:
rbrash 8097 0.0 0.0 12548 3024 pts/2 S 13:02 0:00 bash loop_and_print.sh
nohup命令类似于disown命令,不同之处在于它明确断开了脚本与当前 shell 的连接。它与disown的不同之处在于,nohup允许你在脚本执行后仍保留输出,并且可以通过其他应用程序访问位于nohup.out文件中的输出:
$ nohup bash loop_and_print.sh &
[2] 14256
$ nohup: ignoring input and appending output to 'nohup.out'
- 重新登录并使用
ps命令时,你将看到两个存活下来的脚本的 PID:
$ ps aux | grep loop_and_print.sh
rbrash 4510 0.0 0.0 12548 3024 pts/2 S 12:58 0:00 bash loop_and_print.sh
rbrash 14256 0.0 0.0 12548 3024 pts/2 S 13:02 0:00 bash loop_and_print.sh
执行需要权限的命令时
以 root 身份运行是危险的,尽管有时很方便——尤其是当你刚接触 Linux,且密码提示看起来很麻烦时。到目前为止,作为一个 Linux 用户,你可能已经见过 sudo 命令或 su 命令。这些命令可以让用户在控制台上切换用户或临时执行具有更高权限的命令(如果用户有 sudo 权限的话)。Sudo,或替代用户执行,使普通用户能够将其用户权限提升到更高的特权级别,仅针对某一条命令。
或者,替代用户命令 su 也允许你运行特权命令,甚至切换 shell(例如,成为 root 用户)。与 su 命令不同,sudo 不会激活 root shell,也不允许访问其他用户账户。
这里是这两个命令的一些示例用法:
$ sudo ls /root
$ su -c 'ls /root'
$ su -
尽管这两个命令都需要知道 root 密码,但 sudo 还要求执行 sudo 命令的用户在 /etc/sudoers 文件中列出:
$ sudo /etc/sudoers
[sudo] password for rbrash:
#
# This file MUST be edited with the 'visudo' command as root.
#
# Please consider adding local content in /etc/sudoers.d/ instead of
# directly modifying this file.
#
# See the man page for details on how to write a sudoers file.
#
Defaults env_reset
Defaults mail_badpass
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
# Host alias specification
# User alias specification
# Cmnd alias specification
# User privilege specification
root ALL=(ALL:ALL) ALL
# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL
# Allow members of group sudo to execute any command
%sudo ALL=(ALL:ALL) ALL
# See sudoers(5) for more information on "#include" directives:
#includedir /etc/sudoers.d
在前面的标准 Ubuntu sudoers 文件中,我们可以看到管理员组的用户可以使用 sudo 命令(这也是你能够使用的原因,且无需调整)。我们还可以看到可以为特定用户配置权限执行:
root ALL=(ALL:ALL) ALL
这表示 root 用户可以运行系统上所有可用的命令。实际上,我们可以为名为 rbrash 的用户添加一行,比如 rbrash ALL=(ALL) ALL。
/etc/sudoers 可以通过具有 root 权限的用户使用 visudo 命令进行编辑:
$ sudo visudo
在为用户添加权限或进行修改时要小心。如果账户不安全,这可能会成为一个安全隐患!
最终,你可能会想知道为什么这对 Bash 脚本如此重要(除了能够提升权限之外)。嗯,想象一下你可能有一个系统,它执行持续集成或持续构建软件的过程(例如,Jenkins)——可能希望在没有你输入的情况下运行各种命令,因此需要授予用户访问特定命令的权限(特别是当它们是沙箱或位于虚拟机中时)。
准备工作
除了保持终端打开,我们还需要记住一些概念:
-
sudo需要密码(除非特别指定) -
sudo还可以限制特定的命令、用户或主机 -
sudo命令也会记录在/var/log/secure或/var/log/auth.log中:
Dec 23 16:16:19 moon sudo: rbrash : TTY=pts/2 ; PWD=/home/rbrash/Desktop/book ; USER=root ; COMMAND=/usr/bin/vi /var/log/auth.log
Dec 23 16:16:19 moon sudo: pam_unix(sudo:session): session opened for user root by (uid=0)
此外,我们还可以为此步骤创建一个新用户:
$ sudo useradd bob
$ sudo passwd bob #use password
如何实现...
我们的活动开始如下:
- 在新的终端中运行命令,不要以
root身份运行,也不要使用任何先前的sudo授权:
$ shutdown -h 10
$ shutdown -c
- 现在,执行
$ sudo visudo命令,并编辑脚本以包含以下行:
$ sudo visudo
[sudo] password for rbrash:
#
Defaults env_reset
Defaults mail_badpass
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
# Host alias specification
# User alias specification
# Cmnd alias specification
Cmnd_Alias READ_CMDS = /sbin/halt, /sbin/shutdown
# User privilege specification
root ALL=(ALL:ALL) ALL
bob ALL=(ALL:ALL) NOPASSWD: READ_CMDS
# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL
# Allow members of group sudo to execute any command
%sudo ALL=(ALL:ALL) ALL
# See sudoers(5) for more information on "#include" directives:
#includedir /etc/sudoers.d
- 在新的终端中运行命令,不要以
root身份运行,也不要使用任何先前的sudo授权:
$ shutdown -h 10
$ shutdown -c
- 注意到有什么不同吗?现在,确保使用之前的命令取消关机:
$ shutdown -c。
它是如何工作的...
上述步骤虽然简洁,但需要一些关于sudo的假设和知识。首先,要小心。其次,更小心。最后,要确保通过适当的密码策略保护你的账户安全:
-
在第一步中,我们尝试运行两个需要用户权限的命令。通常,重新启动或关闭系统需要权限提升(除非通过 GUI 完成)。
shutdown -c命令取消关机。如果现在使用shutdown -h,系统将立即关闭,且无法停止。 -
在第二步中,我们使用新的
visudo命令对/etc/sudoers文件进行编辑。加粗部分Cmnd_Alias允许你定义一组命令,但你必须使用二进制文件的完整路径。用户 Bob 也被分配到这个别名中。NOPASSWD:用于指定这些命令不需要密码。 -
在第三步中,可以运行关机命令而无需输入密码。
-
最后一步是确保意外关机被取消。
清理用户输入并确保结果可重复
脚本(或程序)的最佳实践之一是控制用户输入,这不仅仅是出于安全考虑,更是为了控制功能,使输入产生可预测的结果。例如,假设一个用户输入了一个数字而不是一个字符串。你检查过吗?会导致脚本提前退出吗?或者会发生一些意外事件,例如用户输入了rm -rf /*而不是有效的用户名?
无论如何,限制程序用户输入对你作为作者也是有益的,因为它可以限制用户的操作路径,并减少未定义行为或程序错误。因此,如果质量保证很重要,测试用例和输入/输出验证可以减少。
准备工作
这个过程可能会让一些读者接触到他们想避免的概念:软件工程。确实,你可能是在编写脚本以快速完成任务,但如果脚本需要由其他人使用(或者长期使用),最好在错误发生时及早捕获并防止程序出现异常行为。
即使没有正式的计算机科学或工程训练,使用案例的概念也是基于有一个特定的功能片段,通过 X 输入,查看 Y 是否按预期工作。有时可以施加限制或范围,某个操作可能完成或失败,并且任何比较的结果可以得出“用例”是否通过的结论。
让我们通过一个逐步示例来看一下,使用一个程序,通过提示来echo执行脚本的用户的用户名:
-
脚本期望通过
read命令将输入读取到一个变量中(例如)。 -
该变量假定为字符串,但它也可以是用户的姓名、数字、外国地址、电子邮件,甚至是恶意命令。
-
脚本读取变量并运行
echo命令。 -
返回的结果可能是垃圾数据,但也可能被其他脚本执行——这会出什么问题呢?
例如,默认情况下,用户名应该包含字母和数字,但除了下划线、点、破折号和名字结尾的美元符号($)外,不应包含其他特殊字符。
如果安全性不重要,那么应用程序的健壮性可能就显得不那么重要了!
如何操作...
让我们开始我们的活动,步骤如下:
- 首先,打开终端并创建一个名为
bad_input.sh的新 shell 脚本,内容如下:
#!/bin/bash
FILE_NAME=$1
echo $FILE_NAME
ls $FILE_NAME
- 现在,运行以下命令:
$ touch TEST.txt
$ mkdir new_dir/
$ bash bad_input.sh "."
$ bash bad_input.sh "../"
- 创建一个名为
better_input.sh的第二个脚本:
#!/bin/bash
FILE_NAME=$1
# first, strip underscores
FILE_NAME_CLEAN=${FILE_NAME//_/}
FILE_NAME_CLEAN=$(sed 's/..//g' <<< ${FILE_NAME_CLEAN})
# next, replace spaces with underscores
FILE_NAME_CLEAN=${FILE_NAME_CLEAN// /_}
# now, clean out anything that's not alphanumeric or an underscore
FILE_NAME_CLEAN=${FILE_NAME_CLEAN//[^a-zA-Z0-9_.]/}
# here you should check to see if the file exists before running the command
ls "${FILE_NAME_CLEAN}"
- 接下来,使用以下命令运行脚本,而不是输出结果:
$ bash better_input.sh "."
$ bash better_input.sh "../"
$ bash better_input.sh "anyfile"
- 接下来,创建一个名为
validate_email.sh的新脚本,用于验证电子邮件地址(类似于验证 DNS 名称的方式):
#!/bin/bash
EMAIL=$1
echo "${EMAIL}" | grep '^[a-zA-Z0-9._]*@[a-zA-Z0-9]*\.[a-zA-Z0-9]*
RES=$?
if [ $RES -ne 1 ]; then
echo "${EMAIL} is valid"
else
echo "${EMAIL} is NOT valid"
fi >/dev/null
RES=$?
if [ $RES -ne 1 ]; then
echo "${EMAIL} is valid"
else
echo "${EMAIL} is NOT valid"
fi
- 同样,我们可以测试输出:
$ bash validate_email.sh ron.brash@somedomain.com
ron.brash@somedomain.com is valid
$ bash validate_email.sh ron.brashsomedomain.com
ron.brashsomedomain.com is NOT valid
- 另一个常见的任务是验证 IP 地址。创建另一个名为
validate_ip.sh的脚本,内容如下:
#!/bin/bash
IP_ADDR=$1
IFS=.
if echo "$IP_ADDR" | { read octet1 octet2 octet3 octet4 extra;
[[ "$octet1" == *[[:digit:]]* ]] &&
test "$octet1" -ge 0 && test "$octet1" -le 255 &&
[[ "$octet2" == *[[:digit:]]* ]] &&
test "$octet2" -ge 0 && test "$octet2" -le 255 &&
[[ "$octet3" == *[[:digit:]]* ]] &&
test "$octet3" -ge 0 && test "$octet3" -le 255 &&
[[ "$octet4" == *[[:digit:]]* ]] &&
test "$octet4" -ge 0 && test "$octet4" -le 255 &&
test -z "$extra" 2> /dev/null; }; then
echo "${IP_ADDR} is valid"
else
echo "${IP_ADDR} is NOT valid"
fi
- 尝试运行以下命令:
$ bash validate_ip.sh "a.a.a.a"
$ bash validate_ip.sh "0.a.a.a"
$ bash validate_ip.sh "255.255.255.255"
$ bash validate_ip.sh "0.0.0.0"
$ bash validate_ip.sh "192.168.0.10"
它是如何工作的...
让我们详细了解我们的脚本:
-
首先,我们从创建
bad_input.sh脚本开始——它接受$1(或参数 1),并运行列表或ls命令。 -
运行以下命令时,我们可以列出目录、子目录中的所有内容,甚至可以向后遍历目录!这显然是不好的,安全漏洞甚至允许恶意黑客穿透 Web 服务器——其思路是将输入限制在可预测的结果内,并控制输入,而不是允许任何输入:
$ touch TEST.txt
$ mkdir new_dir/
$ bash bad_input.sh "."
...
$ bash bad_input.sh "../"
../all the files backwards
-
在第二个脚本
better_input.sh中,输入通过以下步骤进行了清理。此外,还可以检查列出的文件是否确实存在:-
删除任何下划线(必要)。
-
删除任何一对双空格。
-
将空格替换为下划线。
-
删除任何非字母数字字符或其他不是下划线的内容。
-
然后,运行
ls命令。
-
-
接下来,运行
better_input.sh将允许我们查看当前工作目录或其中包含的任何文件。通配符已经被移除,现在我们无法遍历目录。 -
要验证电子邮件的格式,我们使用
grep命令结合正则表达式。我们只是检查电子邮件账户名的格式、一个@符号,以及一个域名,格式如 acme.x。需要注意的是,我们并不是要验证电子邮件是否真正有效,或者是否能到达预期的目的地,而只是检查它是否符合电子邮件的格式。通过测试域名的 MX 或 DNS 邮件记录等额外测试,可以扩展此功能,以提高用户输入有效电子邮件的可能性。 -
在下一步中,我们测试两个域名——一个没有
@符号(无效),一个有@符号(有效)。可以尝试多个组合。 -
验证 IP 地址通常可以通过正则表达式来完成,但为了方便使用的工具来完成任务,读取和简单的测试使用test(和评估)就可以了。IP 地址的基本形式由四个八位字节(或通俗来说,四个由句点分隔的值)组成。在不深入探讨什么是一个真正有效的 IP 地址的情况下,通常一个有效的八位字节值在
0到255之间(既不大于也不小于)。IP 地址可以有不同的类别和子类,称为子网。 -
在我们的示例中,我们知道包含字母字符的 IP 地址不是有效的 IP 地址(排除句点),并且每个八位字节的值范围在
0到255之间。192.168.0.x(或192.168.1.x)是许多人在家用路由器上看到的 IP 子网。
使用 select 制作一个简单的多级用户菜单
在本书的前面部分,我们看到你可以创建一个使用递归函数和条件逻辑的脚本来制作一个简单的菜单。它有效,但另一个可以使用的工具是select。Select 通过提供的列表工作(例如,可以是文件的通配符选择),并将为你提供一个列表,如下所示:
Select a file from the list:
1.) myfirst.file
2.) mysecond.file
You chose: mysecond.file
显然,像“关于”这样的菜单非常简单;它对于实用功能以及诸如删除用户或修改文件/归档等可重复的子任务非常有用。
简单的 select 脚本还可以用于许多活动,例如挂载 Dropbox、解密或挂载驱动器,或者生成管理报告。
准备就绪
Select 已经是 Bash shell 的一部分,但它有一些不太显而易见的要点。Select 依赖于三个变量:
-
PS3:在创建菜单之前回显给用户的提示符 -
REPLY:从数组中选择的项的索引 -
opt:从数组中选择的项的值——而不是索引
从技术上讲,opt不是必须的,但它是我们示例中由 Select 迭代的元素的值。你可以使用其他名称,例如element。
如何操作...
我们的活动从以下开始:
- 打开终端并创建一个名为
select_menu.sh的脚本,内容如下:
#!/bin/bash
TITLE="Select file menu"
PROMPT="Pick a task:"
OPTIONS=("list" "delete" "modify" "create")
function list_files() {
PS3="Choose a file from the list or type \"back\" to go back to the main: "
select OPT in *; do
if [[ $REPLY -le ${#OPT[@]} ]]; then
if [[ "$REPLY" == "back" ]]; then
return
fi
echo "$OPT was selected"
else
list_files
fi
done
}
function main_menu() {
echo "${TITLE}"
PS3="${PROMPT} "
select OPT in "${OPTIONS[@]}" "quit"; do
case "$REPLY" in
1 )
# List
list_files
main_menu # Recursive call to regenerate the menu
;;
2 )
echo "not used"
;;
3 )
echo "not used"
;;
4 )
echo "not used"
;;
$(( ${#OPTIONS[@]}+1 )) ) echo "Exiting!"; break;;
*) echo "Invalid option. Try another one.";continue;;
esac
done
}
main_menu # Enter recursive loop
- 使用以下命令执行脚本:
$ bash select_menu.sh
- 按1进入文件列表功能。在菜单中输入任何文件的编号。一旦满意,输入“back”并按Enter返回主菜单。
它是如何工作的...
让我们详细了解我们的脚本:
-
创建
select_menu.sh脚本非常简单,但除了使用 select 外,一些概念应该看起来很熟悉:函数、返回、case 语句和递归。 -
脚本通过调用
main_menu函数进入菜单,然后使用 select 从${OPTIONS}数组生成菜单。硬编码的变量PS3将在菜单之前输出提示符,而$REPLY包含所选项的索引。 -
按1并按Enter将导致 select 遍历各个项,然后执行
list_files函数。此函数使用 select 第二次列出目录中的所有文件,创建一个子菜单。选择任何目录都会返回$OPT was selected消息,但如果输入back,则脚本将从此函数返回,并在其中调用main_menu(递归)。此时,你可以选择主菜单中的任何项。
生成并捕获信号以进行清理
在本书中,你可能在不知情的情况下按过Ctrl + C或Ctrl + Z——这就像在其他操作系统中按Ctrl + Alt + Delete对吧?嗯,从某种程度上来说,是的——它是一个信号,但在 Linux 中,它的操作非常不同。硬件层面的信号类似于一个标志或某种即时通知,表示嘿——这里发生了某些事情。如果已设置适当的监听器,该信号可以执行某种功能。
另一方面,软件信号要灵活得多,我们可以将信号作为简单的通知机制,它们比硬件信号更具灵活性。在 Linux 中,Ctrl + C等同于 SIGINT(程序中断),它通常会退出程序。它可以被停止,并且可以执行其他功能,比如清理。Ctrl + Z或 SIGTSTP(键盘停止)通常会告诉程序被挂起并推到后台(更多关于作业的内容将在后面部分讲解),但它也可以被阻止——就像 SIGINT 一样。
SIGHUP 是我们已经熟悉的信号——与 SIGKILL 相同。当我们使用 disown 或退出一个 shell 时,我们就会看到它们。有关信号的更多信息,请参阅关于信号的详细手册页。
准备工作
除了在程序中使用键盘,我们还可以通过 kill 命令向程序发送信号。kill 命令可以终止程序,但这些信号也可以用于重新加载配置或发送用户定义的信号。你可能使用的最常见信号有:SIGHUP (1)、SIGINT (2)、SIGKILL (9)、SIGTERM (15)、SIGSTOP (17,18,23)、SIGSEGV (12) 和 SIGUSR1 (10)/SIGUSR2 (12)。后两个信号可以在你的程序中定义,或者被其他开发者利用。
kill 命令可以像下面这样轻松使用:
$ kill -s SIGUSR1 <processID>
$ kill -9 <processID>
$ kill -9 `pidof myprogram.sh`
kill 命令可以通过信号编号或信号名称来引用。它确实需要一个进程 ID 来定位目标。你可以通过 ps | grep X 来搜索,也可以通过使用前面的 final 和 pidof 来查找。
如何做到……
让我们开始进行以下操作:
- 打开终端,创建一个新的脚本,命名为
mytrap.sh,内容如下:
#!/bin/bash
function setup() {
trap "cleanup" SIGINT SIGTERM
echo "PID of script is $$"
}
function cleanup() {
echo "cleaning up"
exit 1
}
setup
# Loop forever with a noop (:)
while :
do
sleep 1
done
-
使用
$ bash mtrap.sh执行脚本。 -
按 Enter 多次并观察程序的行为。
-
按 Ctrl + C;注意到有什么不同吗?
它是如何工作的……
让我们详细了解一下脚本:
-
mytrap.sh脚本利用了函数和trap调用。在setup函数中,我们设置了通过trap命令调用的函数。因此,当按下 Ctrl + C 时,cleanup函数会被执行。 -
运行脚本会导致脚本在打印出 PID 后一直运行。
-
按常规键如 Enter 对程序不会产生影响。
-
按 Ctrl + C 会在控制台上输出
cleanup,并且脚本会使用exit命令退出。
在程序中使用临时文件和锁文件
另一个程序和脚本常用的机制或组件是锁文件。它通常是临时的(存储在 /tmp 目录下),有时用于多个实体依赖单一数据源或需要知道其他程序存在的情况。有时,仅仅是文件的存在、文件上的特定时间戳,或其他简单的工件。
有几种方法可以测试文件是否存在,但有一个重要的属性尚未演示或探讨,那就是 隐藏 文件的概念。Linux 中的隐藏文件并不像 Windows 中那样真正“隐藏”,但通常不会显示,除非运行了特定的标志或命令。例如,ls 命令不会返回隐藏文件,而 ls 命令加上 -a 标志(-a 代表所有)则会显示它们。
大多数文件浏览器默认不会显示隐藏文件。在 Ubuntu 中,按 Ctrl + H 可以切换该功能。
要创建一个隐藏文件,文件名的开头需要有一个 .(点):
$ touch .myfirsthiddenfile.txt
除了常规文件外,我们还可以使用mktemp命令来创建锁文件。
准备工作
我们简要提到过临时文件可以存放在/tmp目录中。通常,/tmp存放的是短暂的文件,如锁文件或可以被销毁(在电源事件发生时不会对系统造成任何损害)的信息。它通常也是基于 RAM 的,这可以提供性能上的好处,特别是当它作为进程间通信系统的一部分时(更多内容将在另一个示例中讲解)。
然而,重要的是要知道其他程序可以访问/tmp目录中的文件,因此应该使用足够的权限来保护它。它还应该给定一个足够随机的名称,以避免文件名冲突。
如何操作…
让我们按如下方式开始我们的活动:
- 打开一个新的终端,创建一个名为
mylock.sh的新脚本,内容如下:
#!/bin.bash
LOCKFILE="/tmp/mylock"
function setup() {
# $$ will provide the process ID
TMPFILE="${LOCKFILE}.$$"
echo "$$" > "${TMPFILE}"
# Now we use hard links for atomic file operations
if ln "${TMPFILE}" "${LOCKFILE}" 2>&- ; then
echo "Created tmp lock"
else
echo "Locked by" $(<$LOCKFILE)
rm "${TMPFILE}"
exit 1
fi
trap "rm ${TMPFILE} ${LOCKFILE}" SIGINT SIGTERM SIGKILL
}
setup
echo "Door was left unlocked"
exit 0
-
执行脚本
$ bash mylock.sh并查看控制台输出。 -
接下来,我们知道脚本正在寻找一个特定的锁文件。那么,当我们创建锁文件并重新运行脚本时会发生什么?
$ echo "1000" > /tmp/mylock
$ bash mylock.sh
$ rm /tmp/mylock
$ bash mylock.sh
它是如何工作的…
让我们详细了解一下我们的脚本:
-
mylock.sh脚本重新使用了我们已经熟悉的几个概念:捕捉信号(traps)和符号链接(symbolic links)。我们知道,如果一个捕捉信号被调用,或者说它捕获了一个特定的信号,它可以清理锁文件(正如这个脚本中的情况)。符号链接被使用是因为它们可以在网络文件系统上的原子操作中存活。如果LOCKFILE位置存在文件,则表示锁定已发生。如果LOCKFILE缺失,则表示门已打开。 -
当我们运行
mylock.sh时,由于尚不存在锁文件(包括任何临时文件),我们将看到以下内容:
$ bash mylock.sh
Created tmp lock
Door was left unlocked
- 由于前面的脚本正确退出,
SIGKILL信号被处理并且临时锁文件被删除。在这种情况下,我们希望创建自己的锁文件,绕过这个机制。创建一个假设 PID 为1000的锁文件;运行脚本将返回Locked by 1000,删除锁文件后,常规行为将恢复(门被解锁)。
在等待命令完成时利用超时
有时,等待命令完成执行或忽略命令直到完成可能不是脚本中的最佳实践,尽管它确实有应用场景:
-
在某些命令执行需要不确定时间的情况下(例如,ping 一个网络主机)
-
当任务或命令以某种方式执行时,主脚本会等待多个操作的成功或失败。
然而,重要的是要注意,超时/等待需要一个进程,甚至是一个子 shell,以便它可以通过进程 ID(PID)进行监控。在本食谱中,我们将演示如何使用超时命令(该命令已被添加到 coreutils 包 7.0 中)来等待子 shell,并展示如何通过 trap 和 kill 命令(用于警报/计时器)实现这一点。
准备工作
在早期的食谱中,我们介绍了使用trap捕获信号,以及使用kill向进程发送信号。这些内容将在本食谱中进一步解释,但这里有三个新的本地 Bash 变量:
-
$$:返回当前脚本的 PID。 -
$?:返回最后一个被发送到后台的作业的 PID。 -
$@:返回输入变量的数组(例如,$!、$2)。
我们正在绕开作业、任务、后台和前台的概念,它们将在稍后的管理食谱中详细出现。
如何实现...
我们开始这本食谱时知道,Bash shell 中有一个叫做timeout的命令。然而,它无法提供在脚本中函数的超时功能。通过使用trap、kill和信号,我们可以设置定时器或警报(ALRM),以便从失控的函数或命令中执行干净的退出。让我们开始:
- 打开一个新的终端,并创建一个名为
mytimeout.sh的新脚本,内容如下:
#!/bin/bash
SUBPID=0
function func_timer() {
trap "clean_up" SIGALRM
sleep $1& wait
kill -s SIGALRM $$
}
function clean_up() {
trap - ALRM
kill -s SIGALRM $SUBPID
kill $! 2>/dev/null
}
# Call function for timer & notice we record the job's PID
func_timer $1& SUBPID=$!
# Shift the parameters to ignore $0 and $1
shift
# Setup the trap for signals SIGALRM and SIGINT
trap "clean_up" ALRM INT
# Execute the command passed and then wait for a signal
"$@" & wait $!
# kill the running subpid and wait before exit
kill -ALRM $SUBPID
wait $SUBPID
exit 0
- 使用带有变量次数的命令(
ping),我们可以使用第一个参数作为超时变量来测试mytimeout.sh!
$ bash mytimeout.sh 1 ping -c 10 google.ca
$ bash mytimeout.sh 10 ping -c 10 google.ca
它是如何工作的...
你可能会问自己,我可以将函数放到后台吗? 当然可以——你甚至可以使用一个名为export并带有-f 标志的命令(尽管它可能在所有环境中不被支持)。如果你使用超时命令,你必须要么只运行你希望监控的命令,要么将该函数放到一个第二个脚本中,通过超时命令调用。显然,在某些情况下,这并不理想。在本食谱中,我们使用信号,或者更准确地说,使用alarm信号作为定时器。当我们设置一个特定变量的警报时,它将在计时器到期后触发SIGALRM!如果进程仍然存活,我们只需杀死它并退出脚本(如果我们还没有退出的话)。
-
在步骤 1 中,我们创建了
mytimeout.sh脚本。它使用了我们的一些新原语,例如$!,来监控我们发送到后台作为作业(或者在这个情况下是子 shell)执行的函数的 PID。我们“启动”计时器,然后继续执行脚本。接着,我们使用 shift 命令字面上移动传递给脚本的参数,忽略$1(或超时变量)。最后,我们监视SIGALRM信号,并在必要时执行清理操作。 -
在步骤 2 中,
mytimeout.sh使用ping命令执行两次,目标是google.ca。第一次执行时,使用1秒的超时,第二次执行时,使用10秒的超时。在这两次操作中,Ping 将进行 10 次 ping 操作(例如,ping 一次,往返到响应 ICMP 请求的任何主机,针对 google.ca 的 DNS 条目)。第一次会较早执行,而第二次则允许 10 次 ping 操作顺利执行并退出:
$ bash mytimeout.sh 1 ping -c 10 google.ca
PING google.ca (172.217.13.99) 56(84) bytes of data.
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=1 ttl=57 time=10.0 ms
$ bash mytimeout.sh 10 ping -c 10 google.ca
PING google.ca (172.217.13.99) 56(84) bytes of data.
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=1 ttl=57 time=11.8 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=2 ttl=57 time=14.5 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=3 ttl=57 time=10.8 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=4 ttl=57 time=13.1 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=5 ttl=57 time=12.7 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=6 ttl=57 time=13.4 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=7 ttl=57 time=9.15 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=8 ttl=57 time=14.0 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=9 ttl=57 time=12.0 ms
64 bytes from yul02s04-in-f3.1e100.net (172.217.13.99): icmp_seq=10 ttl=57 time=11.2 ms
--- google.ca ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9015ms
rtt min/avg/max/mdev = 9.155/12.307/14.520/1.545 ms
请注意,google.ca 可以替换为其他 DNS 名称,但执行时间可能因您的位置而异。因此,10 次 PING 可能实际上无法完全执行。
创建文件进文件出程序并并行运行进程
在这个方案中,我们使用了一个叫做文件进文件出(FIFO)的概念,也叫做管道,通过它将参数传递给多个“工作”脚本。这些工作脚本并行运行(换句话说,主要独立于主进程),读取输入并执行命令。FIFO 非常有用,因为它们可以减少文件系统操作或输入/输出(IO),数据可以直接流向监听者或接收者。它们在文件系统中表现为文件,并且是双向的——可以同时读取和写入。
准备工作
要创建 FIFO,我们使用mkinfo命令来创建一个看似文件的东西(在 Linux 中,一切皆文件)。不过,这个文件有一个特殊的属性,与普通文件不同,也与我们之前使用的管道不同:在这种情况下,管道允许多个读者和写者!
与任何文件一样,您还可以使用-m标志提供权限,例如:-m a=rw,或者使用mknod命令(这里不再详细说明,因为它需要您使用第二个命令chown来更改权限,这是文件创建后的操作)。
如何操作...
为了开始本练习,我们将引入两个术语:领导者和跟随者,或者主机和工作者。在这种情况下,主机(中央主机)将创建工作者(或从属进程)。尽管这个方案稍显牵强,但它应该是一个简单的命名管道或 FIFO 模式的易用模板。从本质上讲,主机创建了五个工作者,而这些新创建的工作者通过命名管道回显出提供给它们的数据:
-
为了开始,打开一个新的终端并创建两个新的脚本:
master.sh和worker.sh。 -
在
master.sh中,添加以下内容:
#!/bin/bash
FIFO_FILE=/tmp/WORK_QUEUE_FIFO
mkfifo "${FIFO_FILE}"
NUM_WORKERS=5
I=0
while [ $I -lt $NUM_WORKERS ]; do
bash worker.sh "$I" &
I=$((I+1))
done
I=0
while [ $I -lt $NUM_WORKERS ]; do
echo "$I" > "${FIFO_FILE}"
I=$((I+1))
done
sleep 5
rm -rf "${FIFO_FILE}"
exit 0
- 在
worker.sh中,添加以下内容:
#!/bin/bash
FIFO_FILE=/tmp/WORK_QUEUE_FIFO
BUFFER=""
echo "WORKER started: $1"
while :
do
read BUFFER < "${FIFO_FILE}"
if [ "${BUFFER}" != "" ]; then
echo "Worker received: $BUFFER"
exit 1
fi
done
exit 0
- 在终端中运行以下命令,并观察输出:
$ bash master.sh
它是如何工作的...
本方案的理念是,如果你有多个重复性任务,例如批量操作和可能的多核处理,你可以并行执行任务(在 Linux 世界中通常称为作业)。该方案创建一个主脚本,并将多个工作脚本分配到后台,它们等待来自命名管道的输入。一旦它们从命名管道读取到输入,就会将其回显到屏幕并退出。最终,主脚本也会退出,并将管道一同删除:
-
在第 1 步中,我们打开一个新的终端并创建两个脚本:
master.sh和worker.sh。 -
在第 2 步中,我们创建了
master.sh脚本。它使用两个 while 循环创建 n 个工作脚本,并为它们分配$I标识符,然后将相同数量的值发送到 FIFO 队列中,之后休眠或退出。 -
在第 3 步中,我们创建了
worker.sh脚本,它回显初始化消息,并等待直到$BUFFER不为空(有时也称为 NULL)。一旦$BUFFER被填满,或者说,包含了一个消息,它会将其回显到控制台并退出脚本。 -
在第 4 步中,控制台应该输出类似以下内容:
$ bash master.sh
WORKER started: 0
WORKER started: 1
We got 0
We got 1
WORKER started: 4
We got 2
WORKER started: 2
We got 3
WORKER started: 3
We got 4
通过这两个脚本在 FIFO 上协同工作,一个数字值在它们之间传递,工作脚本则执行它们的 工作。这些值或消息可以轻松修改,使得工作脚本执行命令!
请注意,输出的顺序可能不同。这是因为 Linux 是非确定性的,生成进程或读取 FIFO 可能会被阻塞,或者某些进程可能会先到达(由于调度的原因)。请记住,FIFO 也不是原子操作或同步的——如果你希望指定哪个消息发送到哪个主机,你可以创建一个标识符或消息传递方案。
在启动时执行脚本
本方案不仅限于在启动时运行应用程序或服务,还可以在系统启动(开机)时启动脚本。例如,如果你的系统启动后,你希望对操作系统应用一些调整,如性能增强或电池优化,你可以通过 systemd 或 init.d 脚本在启动时完成这些调整。另一个例子是运行一个永不结束的脚本,生成日志事件,就像电子版的脉搏监视器一样。
简而言之,Linux 或大多数 *NIX 系统使用古老的 rc.d 系统或较新且争议较大的 systemd 系统来管理系统资源的启动和停止。无需深入了解整个 Linux 启动序列,以下是其工作原理:
-
Linux 内核被加载并挂载根文件系统。
-
根文件系统在特定路径下包含一个 shell(初始化级别)。
-
然后,systemd 会依次启动一系列服务(运行级别)。
如果添加了服务或脚本,它很可能会被添加到 运行级别。它还可以随时通过命令行启动、停止、重载和重启。当系统启动时,它仅使用 init.d 或 system.d 脚本提供的启动功能。然而,尽管 rc.d 或 systemd 系统的语义有所不同,它们仍然需要以下内容:
-
脚本或服务需要为特定的系统启动级别启用。
-
要启动和/或停止的脚本可以配置为按特定顺序启动。
-
启动(至少)、停止、重启和/或重载的指令会根据执行这些操作(或块)时的参数执行。例如,调用 start 时,也可以执行多个命令。
当你在网上寻找资源时,可能会注意到有许多不同的初始化系统:upstart、SysVinit、rc.d、procd,等等。你可以查阅你的发行版文档,了解当前使用的启动系统的相关说明。
准备工作
在 Ubuntu 16.04 LTS(及其他发行版)中,使用 systemd。了解 init.d 和 systemd 服务控制系统是非常值得的,因为许多嵌入式系统使用 Busybox。BusyBox 使用 init.d 系统而不是 systemd。
在进入“如何做”部分之前,我们将创建一个模板初始化脚本,以便以后参考,并确保你在遇到它们时了解它。它将被命名为 myscript,并将运行 myscript.sh。至少,一个兼容 system.d 的脚本看起来应该如下所示:
#!/bin/sh
### BEGIN INIT INFO
# Provides: myscript
# Required-Start: $local_fs
# Required-Stop: $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start script or daemons example
### END INIT INFO
PATH=/sbin:/bin:/usr/sbin:/usr/bin
DAEMON_NAME=myscript.sh
DESC=myscript
DAEMON=/usr/sbin/$DAEMON_NAME
case $1 in
start)
log_daemon_msg "Starting $DESC"
$DAEMON_NAME
log_end_msg 0
;;
stop)
log_daemon_msg "Stopping $DESC"
killall $DAEMON_NAME
log_end_msg 0
;;
restart|force-reload)
$0 stop
sleep 1
$0 start
;;
status)
status_of_proc "$DAEMON" "$DESC" && exit 0 || exit $?
;;
*)
N=/etc/init.d/$DESC
echo "Usage: $N {start|stop|restart|force-reload|status}" >&2
exit 1
;;
esac
exit 0
# vim:noet
如果你已经按照本手册的内容进行过操作,那么这些内容应该比较容易理解。它依赖于头部的注释来确定名称、运行级别、顺序和依赖关系。在此之后,它使用一个 switch 语句,查找一些预定的/标准化的参数:start、stop、restart、force-reload 和 status。在 start 的 case 中,我们启动二进制文件,而在 stop 的 case 中,我们使用 killall 函数来停止二进制文件。
需要知道的是,单纯安装初始化脚本并不能保证它会在启动时执行。必须有一个过程来启用该服务或脚本。在较旧的系统(SysV)中,你可能听说过/见过使用 chkconfig 命令。在 systemd 中,你可以使用 systemctl 命令来启用/禁用服务。在本节中,我们只关注 systemd。
SysV 会根据文件名中的数字顺序(例如,S99-myinit)按顺序执行脚本。Systemd 则不同,因为它还会检查依赖关系并等待其完成。
如何做...
让我们按如下方式开始我们的活动:
- 创建一个名为
myscript.sh的脚本,内容如下:
#!/bin/bash
for (( ; ; ))
do
sleep 1
done
- 接下来,让我们为脚本添加正确的权限,以便我们可以使用它创建 systemd 服务。请注意使用
sudo命令——在适当的地方输入密码:
$ sudo cp myscript.sh /usr/bin
$ sudo chmod a+x /usr/bin/myscript.sh
- 现在我们有了启动时执行的内容,我们需要创建一个服务配置文件来描述我们的服务;在这个示例中我们使用了
vi和sudo(记住它的位置):
$ sudo vi /etc/systemd/system/myscript.service
[Unit]
Description=myscript
[Service]
ExecStart=/usr/bin/myscript.sh
ExecStop=killall myscript.sh
[Install]
WantedBy=multi-user.target
- 要启用
myscript服务,请运行以下命令:
$ sudo systemctl enable myscript # disable instead of enable will disable the service
- 要启动并验证进程的存在,请运行以下命令:
$ sudo systemctl start myscript
$ sudo systemctl status myscript
- 您可以重新启动系统以查看我们的服务在启动时的运行情况。
工作原理...
让我们详细了解我们的脚本:
-
在步骤 1 中,我们创建了一个简单的循环程序,用于系统启动时运行,名为
myscript.sh。 -
在步骤 2 中,我们将脚本复制到
/usr/bin目录,并使用chmod命令为所有人添加执行权限(chmod a+x myscript.sh)。注意使用sudo权限在此目录中创建文件并应用权限。 -
在第三步中,我们创建了描述 systemd 服务单元的服务配置文件。它的名字叫做
myscript,在[Service]指令中,有两个最重要的参数:ExecStart和ExecStop。注意启动和停止部分看起来类似于 SysV/init.d 的方法。 -
接下来,我们使用
systemctl命令来启用myscript。相反,它可以用以下方式来禁用myscript:$systemctl disable myscript`.` -
然后,我们使用
systemctl来启动myscript并验证我们脚本的状态。你应该会得到类似以下的输出(请注意我们使用ps进行了双重检查):
$ sudo systemctl status myscript
myscript.service - myscript
Loaded: loaded (/etc/systemd/system/myscript.service; enabled; vendor preset:
Active: active (running) since Tue 2017-12-26 14:28:51 EST; 6min ago
Main PID: 17966 (myscript.sh)
CGroup: /system.slice/myscript.service
├─17966 /bin/bash /usr/bin/myscript.sh
└─18600 sleep 1
Dec 26 14:28:51 moon systemd[1]: Started myscript.
$ ps aux | grep myscript
root 17966 0.0 0.0 20992 3324 ? Ss 14:28 0:00 /bin/bash /usr/bin/myscript.sh
rbrash 18608 0.0 0.0 14228 1016 pts/20 S+ 14:35 0:00 grep --color=auto myscript
- 在重启时,如果启用了,我们的脚本将按预期运行。
第五章:系统管理任务的脚本
在本章中,我们将介绍以下主题:
-
收集和聚合系统信息
-
收集网络信息和连接诊断
-
配置基础网络连接
-
监控目录和文件
-
压缩和归档文件
-
将日志文件从 RAM 转移到存储器进行日志轮转
-
使用 Linux iptables 作为防火墙
-
远程或本地访问 SQL 数据库
-
为无密码远程访问创建 SSH 密钥
-
为任务调度创建和配置 cron 作业
-
系统地创建用户和组
介绍
本章将讨论执行几乎所有用户常见的系统管理任务,我们将查看日志、归档日志、作业/任务管理、网络连接、使用防火墙(IPtables)保护系统、监控目录变化、创建用户。我们还将承认,用户和管理员经常需要从其他系统访问资源,例如 SQL,或者他们必须使用 SSH 通过加密密钥登录到另一个系统——无需密码!
本章还充当了关于今天计算环境中一些关键组件的速成课程:网络。用户可能不知道端口是什么,IP 地址是什么,或者如何查找计算机上的网络接口(NICs)。本章结束时,初学者应该能够配置网络,并且在使用网络术语时能够提高他们的技能水平。
收集和聚合系统信息
在本节中,我们将讨论 dmidecode Linux 工具,它将收集系统信息,如 CPU 信息、服务器、内存和网络。
准备工作
除了保持终端打开外,我们还需要记住一些概念:
-
我们将使用一些命令,这些命令将为我们提供关于内核、Linux 发行版、物理服务器信息、系统运行时间、网络信息、内存信息和 CPU 信息等方面的信息。
-
通过使用此方法,任何人都可以创建脚本来收集系统信息。
如何操作...
- 我们可以获取关于您正在使用的 Linux 发行版的详细信息。这些发行版有一个发行文件,您可以在
/etc/文件夹中找到它。现在,打开终端并输入以下命令来获取有关您正在使用的 Linux 发行版的信息:
cat /etc/*-release
- 上述选项有一个替代方案,替代方案是位于
/proc文件夹中的版本文件。所以,运行以下命令:
cat /proc/version
- 现在,运行以下命令以获取内核信息:
uname -a
- 系统
uptime:有关此信息,请创建一个名为server_uptime.sh的脚本:
server_uptime=`uptime | awk '{print $3,$4}'| sed 's/,//'| grep "day"`;
if [[ -z "$server_uptime" ]]; then
server_uptime=`uptime | awk '{print $3}'| sed 's/,//'`
echo $server_uptime
else
:;
fi;
- 现在,我们将运行以下命令来获取物理服务器的信息:
$ sudo dmidecode -s system-manufacture
$ sudo dmidecode -s system-product-name
$ sudo dmidecode -s system-serial-number
- 然后,我们将运行以下命令来获取 CPU 的信息:
sudo dmidecode -t4|awk '/Handle / {print $2}' |sed 's/,//'
您可以从/proc目录中的cpuinfo文件获取每个 CPU 的信息。
- 网络信息:我们将通过运行以下命令来获取 IP 地址:
ip a
要获取 MAC 地址,请运行以下命令:
ip addr show ens33
你可以根据你的网络接口信息将ens33替换为eth0。
- 内存信息:要获取插槽总数,请运行以下命令:
sudo dmidecode -t17 |awk '/Handle / {print $2}'|wc -l
要获取已填充的插槽总数,请运行以下命令:
sudo dmidecode -t17 |awk '/Size:/'|awk '!/No/'|wc -l
要获取未填充的插槽总数,请运行以下命令:
sudo dmidecode -t17 |awk '/Size:/'|awk '/No/'|wc -l
它是如何工作的...
在运行cat /etc/*-release命令后,按Enter键,你将根据你的 Linux 发行版得到相应的输出。在我的情况下,输出如下:
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.4 LTS"
NAME="Ubuntu"
VERSION="16.04.4 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=Debian
PRETTY_NAME="Ubuntu 16.04.4 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
获取系统信息的另一种方式是通过运行/proc目录下的版本文件。运行命令后,按Enter键,你将根据你的 Linux 发行版得到相应的输出。在我的情况下,输出如下:
Linux version 4.13.0-45-generic (buildd@lgw01-amd64-011) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)) #50~16.04.1-Ubuntu SMP Wed May 30 11:18:27 UTC 2018
uname是我们用来收集这些信息的工具。运行命令后按Enter键,你将得到内核信息。在我的情况下,输出如下:
Linux ubuntu 4.13.0-45-generic #50~16.04.1-Ubuntu SMP Wed May 30 11:18:27 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
执行$ bash server_uptime.sh脚本——在提示符下按Enter键,你将得到服务器的运行时间。
为了获取物理服务器信息,我们使用了dmidecode工具。第一个命令用于获取制造商信息,第二个命令用于获取型号名称,第三个命令用于获取序列号。在运行dmidecode命令时,你必须是 root 用户。
输出片段如下所示:
0x0004
0x0005
0x0006
0x0007
为了获取每个 CPU 的信息,我们创建了一个脚本。执行$ sudo bash cpu_info.sh脚本——在提示符下按Enter键,你将得到每个 CPU 的信息。
输出片段如下所示:
CPU#000
Unknown
GenuineIntel
1800MHz
3.3V
1
CPU#001
Unknown
GenuineIntel
1800MHz
3.3V
1
CPU#002
Unknown
GenuineIntel
1800MHz
3.3V
1
CPU#003
Unknown
GenuineIntel
1800MHz
3.3V
1
网络信息:我们运行了ip命令以获取 IP 地址和 MAC 地址。
内存信息:我们使用dmidecode工具来获取内存信息,如插槽的总数,以及已填充和未填充的插槽数量。你必须是 root 用户才能运行此命令。
收集网络信息和连接诊断
在本节中,我们将测试 IPv4 的连接性并为其编写脚本。
准备工作
除了打开终端外,我们还需要记住一些概念:
-
Shell 脚本中的 If..Else 条件语句
-
设备的 IP 地址
-
curl命令必须已安装(你可以通过运行以下命令来安装它:sudo apt install curl)
本节的目的是向你展示如何检查网络连接。
如何操作...
- 打开终端并创建
test_ipv4.sh脚本:
if ping -q -c 1 -W 1 8.8.8.8 >/dev/null; then
echo "IPv4 is up"
else
echo "IPv4 is down"
fi
- 现在,为了测试 IP 连接性和 DNS,创建一个名为
test_ip_dns.sh的脚本:
if ping -q -c 1 -W 1 google.com >/dev/null
then
echo "The network is up"
else
echo "The network is down"
fi
- 最后,创建一个名为
test_web.sh的脚本来测试网页连接:
case "$(curl -s --max-time 2 -I http://google.com | sed 's/^[^ ]* *\([0-9]\).*/\1/; 1q')" in
[23]) echo "HTTP connectivity is up";;
5) echo "The web proxy won't let us through";;
*) echo "The network is down or very slow";;
esac
它是如何工作的...
-
执行脚本命令
$ bash test_ipv4.sh。在这里,我们通过8.8.8.8IP 地址检查连接。我们使用ping命令在if条件中进行测试。如果条件为true,我们将在屏幕上显示if块中的语句。如果不是,else中的语句将被打印出来。 -
执行脚本命令
$ bash test_ip_dns.sh。在这里,我们通过主机名测试网络连接。同时,我们在if条件中传递ping命令,检查网络是否正常。如果条件为true,我们将在屏幕上打印if块中的语句。如果不是,else中的语句将被打印出来。 -
执行脚本命令
$ bash test_web.sh。在这里,我们测试 Web 连接。我们在这里使用case语句。我们使用 curl 工具进行数据传输,它可以与服务器进行数据交换。
配置基本的网络连接
在这一部分,我们将使用wpa_supplicant配置基本的网络连接。
准备工作
除了打开一个终端,我们还需要记住几个概念:
-
检查
wpa_supplicant是否已安装。 -
你应该知道 SSID 和密码。
-
记住,你必须以 root 用户身份运行你的程序。
如何操作...
创建一个名为wifi_conn.sh的脚本,并在其中写入以下代码:
#!/bin/bash
ifdown wlan0
rm /etc/network/interfaces
touch /etc/network/interfaces
echo 'auto lo' >> /etc/network/interfaces
echo 'iface lo inet loopback' >> /etc/network/interfaces
echo 'iface eth0 inet dhcp' >> /etc/network/interfaces
echo 'allow-hotplug wlan0' >> /etc/network/interfaces
echo 'iface wlan0 inet manual' >> /etc/network/interfaces
echo 'wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf' >> /etc/network/interfaces
echo 'iface default inet dhcp' >> /etc/network/interfaces
rm /etc/wpa_supplicant/wpa_supplicant.conf
touch /etc/wpa_supplicant/wpa_supplicant.conf
echo 'ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev' >> /etc/wpa_supplicant/wpa_supplicant.conf
echo 'update_config=1' >> /etc/wpa_supplicant/wpa_supplicant.conf
wpa_passphrase $1 $2 >> /etc/wpa_supplicant/wpa_supplicant.conf
ifup wlan0
它是如何工作的...
使用sudo执行脚本:
sudo bash wifi_conn.sh <SSID> <password>
监控目录和文件
inotify是 Linux 中的一个工具,用于报告文件系统事件的发生。使用inotify,你可以监控单独的文件或目录。
准备工作
确保你的系统已安装inotify工具。
如何操作...
创建一个名为inotify_example.sh的脚本:
#! /bin/bash
folder=~/Desktop/abc
cdate=$(date +"%Y-%m-%d-%H:%M")
inotifywait -m -q -e create -r --format '%:e %w%f' $folder | while read file
do
mv ~/Desktop/abc/output.txt ~/Desktop/Old_abc/${cdate}-output.txt
done
它是如何工作的...
inotifywait命令主要用于 Shell 脚本中。inotify工具的主要目的是监控目录和新文件。它还监控文件的变化。
压缩和归档文件
压缩文件和归档文件是不同的。那么,什么是归档文件呢?它是将多个文件和目录存储在一个文件中的集合。归档文件不是压缩文件。
什么是压缩文件?这是一个将文件和目录存储在一个文件中的集合。它使用更少的磁盘空间进行存储。
在这一部分,我们将讨论两个压缩工具:
-
bzip2 -
zip
准备工作
你应该有文件可以进行归档和压缩。
如何操作...
首先,让我们看看如何压缩文件:
- 我们将通过
bzip2来进行压缩。选择要压缩的文件并运行以下命令:
$ bzip2 *filename*
使用bzip2,我们可以同时压缩多个文件和目录。只需在每个文件之间加上空格即可。你可以这样压缩:
$ bzip2 file1 file2 file3 /student/work
- 我们将使用
zip进行压缩。使用 zip 压缩工具时,文件是单独压缩的:
$ zip -r file_name.zip files_dir
执行上述命令后,将被压缩的文件或目录(如files_zip)将被压缩,得到一个名为file_name.zip的压缩文件。-r选项递归包括files_dir目录中的所有文件。
现在,让我们讨论如何归档文件:
归档文件包含多个文件或目录。Tar 用于无压缩的文件归档。
以下是两种归档模式:
-
-x:解压归档文件 -
-c:创建归档文件
选项:
-
-f:归档文件的文件名——除非使用磁带驱动器进行归档,否则必须指定此项 -
-v:详细显示,列出所有正在归档/解压的文件 -
-z:使用 gzip/gunzip 创建/解压归档文件 -
-j:使用 bzip2/bunzip2 创建/解压归档文件 -
-J:使用 XZ 创建/解压归档文件
要创建一个 TAR 文件,使用以下命令:
$ tar -cvf filename.tar directory/file
在这个示例中,我们表示一个文件,该文件是在使用tar命令时创建的。它的名字是filename.tar。我们可以指定一个目录或文件来存放归档文件。我们必须在输入filename.tar之后指定它。多个文件和目录可以同时归档,只需在文件和目录名称之间留一个空格:
$ tar -cvf filename.tar /home/student/work /home/student/school
执行上述命令后,所有来自工作和学校目录的内容将被存储在新文件filename.tar中。
执行上述命令后,所有来自工作和学校子目录的内容将被存储在新文件filename.tar中。
以下命令用于列出 TAR 文件的内容:
$ tar -tvf filename.tar
要提取 TAR 文件的内容,使用以下命令:
$ tar -xvf filename.tar
这个命令用于解压,它不会删除 TAR 文件。然而,在当前工作目录中,你将找到解压后的内容。
它是如何工作的……
bzip2 用于同时压缩文件和文件夹。只需在终端中输入命令,文件将被压缩。压缩后,压缩文件将获得.bz2扩展名。
zip 将文件单独压缩,然后将它们汇集到一个文件中。
TAR 文件是一个包含不同文件和目录的集合,它将这些文件和目录合并成一个文件。TAR 用于创建备份和归档。
将文件从 RAM 转移到存储设备进行日志轮换
在本节中,我们将讨论 logrotate Linux 工具。使用此工具,系统管理变得更加轻松。系统会生成大量的日志文件。它允许自动轮换、删除、压缩和发送日志文件。
我们可以处理每一个日志文件。我们可以按天、周、月来处理它们。通过使用此工具,我们可以在节省磁盘空间的同时,保持日志文件更长时间。默认的配置文件是/etc/logrotate.conf。运行以下命令查看此文件的内容:
$ cat /etc/logrotate.conf
你将看到以下内容:
weekly
rotate 4
create
include /etc/logrotate.d
# no packages own wtmp, or btmp -- we'll rotate them here
/var/log/wtmp {
missingok
monthly
create 0664 root utmp
rotate 1
}
/var/log/btmp {
missingok
monthly
create 0660 root utmp
rotate 1
}
# system-specific logs may be configured here
准备工作
要使用 logrotator,必须了解logrotate命令。
如何操作……
我们将查看一个示例配置。
我们有两种管理日志文件的选项:
-
创建一个新的配置文件并将其存储在
/etc/logrotate.d/中。这个配置文件会与其他标准任务一起每日执行,并且会使用根用户权限。 -
创建一个新的配置文件并独立执行。这样会以非根用户权限执行。通过这种方式,你可以手动执行,随时按需求执行。
将配置添加到 /etc/logrotate.d/
现在,我们将配置一个 Web 服务器。它将像 access.log 和 error.log 这样的信息放入 /var/log/example-app。它将充当数据用户或组。
要向 /etc/logrotate.d/ 添加配置,首先打开一个新文件:
$ sudo nano /etc/logrotate.d/example-app
在 example-app 中写入以下代码:
/var/log/example-app/*.log {
daily
missingok
rotate 14
compress
notifempty
create 0640 www-data www-data
sharedscripts
postrotate
systemctl reload example-app
endscript
}
此文件中的一些新配置指令如下:
-
create 0640 www-data www-data:旋转后,这将创建一个新的空日志文件,并为所有者和组设置指定的权限。 -
sharedscripts:这意味着配置脚本在每次运行时仅执行一次,而不是为每个文件进行轮换。 -
postrotate到endscript:这个特定的块包含在日志文件旋转后执行的脚本。使用这个,我们的应用程序可以切换到新创建的日志文件。
它是如何工作的...
我们可以根据需要自定义 .config 文件,然后将其保存在 /etc/logrotate.d 中。为此,运行以下命令:
$ sudo logrotate /etc/logrotate.conf --debug
运行此命令后,logrotate 将指向标准配置文件,然后进入调试模式。它会提供有关 logrotate 正在处理的文件的信息。
使用 Linux iptables 作为防火墙
在这一部分中,我们将使用 iptables 设置防火墙。iptables 是大多数 Linux 发行版中存在的标准防火墙软件。我们将使用这些规则来过滤网络流量。通过指定源或目标 IP 地址、端口地址、协议类型、网络接口等,可以通过过滤数据包来保护服务器免受不必要的流量。我们可以配置它来接受、拒绝或转发网络数据包。
规则按链排列。默认情况下,有三个链(input、output 和 forward)。输入链处理传入流量,而输出链处理传出流量。转发链处理路由流量。如果网络数据包与链中的任何策略不匹配,则每个链都有一个默认策略。
准备工作
请在继续下一项活动之前检查以下要求是否已满足:
-
根用户权限
-
SSH 访问(命令行访问服务器)
-
确保你的 Linux 环境中已安装 gt 和 looptools
-
在 Linux 环境中工作的基本技能
如何操作...
现在,我们将查看一些 iptables 命令:
- 运行以下命令列出服务器上设置的所有规则:
$ sudo iptables -L
- 要允许来自特定端口的传入流量,请使用以下命令:
$ sudo iptables -A INPUT -p tcp --dport 4321 -j ACCEPT
这条规则将允许来自端口4321的传入流量。需要重启防火墙才能使此规则生效。
使用 iptables,你可以阻止传入的流量。为此,运行以下命令:
$ sudo iptables -A INPUT -j DROP
- 如果在
iptables中添加了新的规则,我们应该先保存它们。否则,在系统重启后,它们将消失。添加新规则后,运行以下命令以保存iptables:
$ sudo iptables-save
-
默认保存规则的文件可能会有所不同,这取决于你使用的 Linux 发行版。
-
我们可以使用以下命令将规则保存到特定文件中:
$ sudo iptables-save > /path/to/the/file
- 你可以恢复保存在文件中的这些规则。运行以下命令:
$ sudo iptables-restore > /path/to/the/file
它是如何工作的...
使用 iptables,我们可以控制传入的流量、丢弃特定端口的流量、添加新规则并保存它们。
远程或本地访问 SQL 数据库
在本节中,我们将学习如何通过使用 shell 脚本连接到服务器来自动化 SQL 查询。Bash 脚本用于自动化操作。
准备就绪
确保已经安装了 mysql、postgres 和 sqlite。确保 MySQL 中已创建用户,并且你已经为该用户授予了权限。
如何操作...
- 脚本中的 MySQL 查询:我们将编写一个名为
mysql_version.sh的脚本,以获取 MySQL 的最新版本:
#!/bin/bash
mysql -u root -pTraining2@^ <<MY_QUERY
SELECT VERSION();
MY_QUERY
现在,我们将创建一个名为 create_database.sh 的脚本来创建数据库:
#!/bin/bash
mysql -u root -pTraining2@^ <<MY_QUERY
create database testdb;
MY_QUERY
- 脚本中的 SQLite 查询:现在,我们将创建一个
sqlite数据库。你只需写下sqlite3和数据库的名称即可创建sqlite数据库。例如:
$ sqlite3 testdb
现在,我们将在 sqlite 控制台中创建一个表。输入 sqlite3 testdb 并按 Enter——你将看到 sqlite3 控制台。现在,写入 create table 命令以创建表:
$ sqlite3 testdb
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite> .databases
seq name file
--- --------------- ----------------------------------------------------------
0 main /home/student/testdb
sqlite> CREATE TABLE bookslist(title text, author text);
sqlite> .tables
bookslist
- 脚本中的 Postgres 查询:现在,我们将检查 PostgreSQL 数据库的版本。这里,
testdb是我们的数据库名称,也是我们之前创建的。为此,运行以下命令:
student@ubuntu:~$ sudo -i -u postgres
postgres@ubuntu:~$ psql
psql (9.5.13)
Type "help" for help.
postgres=# create database testdb;
CREATE DATABASE
postgres=# \quit
postgres@ubuntu:~$ psql testdb
psql (9.5.13)
Type "help" for help.
testdb=# select version();
version
-------------------------------------------------------------------------------------------
PostgreSQL 9.5.13 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609, 64-bit
(1 row)
- 现在,我们将创建一个表。为此,运行以下命令:
postgres@ubuntu:~$ psql testdb
psql (9.5.13)
Type "help" for help.
testdb=# create table employee(id integer, name text, address text, designation text, salary integer);
CREATE TABLE
testdb=#
它是如何工作的...
-
我们正在编写 bash 脚本,以检查数据库的版本并创建新数据库。在这些脚本中,我们使用 root 用户,并且该用户的密码会紧随
-p后。你可以使用root用户,或者可以创建一个新用户,分配密码,并在脚本中使用该用户。 -
SQLite 软件为我们提供了一个简单的命令行界面。通过这个界面,我们可以手动输入和执行 SQL 命令。你可以使用点(
.)操作符列出数据库。.databases和.tables用于列出数据库中的所有表。 -
在 PostgreSQL 中,首先我们将用户从 student 更改为 postgres。然后,输入
psql启动postgres命令行控制台。在该控制台中,我们必须创建testdb数据库。要退出控制台,请运行\quit命令。现在,再次启动testdb控制台,输入psql testdb并按 Enter. 现在,在该数据库中创建一个表。
为密码无障碍远程访问创建 SSH 密钥
在本节中,我们将学习如何使用 SSH 无密码登录。SSH 是一个开源网络协议,用于登录远程服务器执行某些操作。我们可以使用 SSH 协议将文件从一台计算机传输到另一台计算机。SSH 使用公钥加密。
准备就绪
确保您具有 SSH 访问权限。
如何做...
- 首先,我们将创建一个 SSH 密钥。使用
ssh-keygen命令来创建 SSH 密钥。运行以下命令:
$ ssh-keygen
您将获得以下输出:
Generating public/private rsa key pair.
Enter file in which to save the key (/home/student/.ssh/id_rsa): /home/student/keytext
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/student/keytext.
Your public key has been saved in /home/student/keytext.pub.
The key fingerprint is:
SHA256:6wmj6l9EcjufZhvwQ+iKIqEchO1mtEwC/x5rMyoKyeY student@ubuntu
The key's randomart image is:
+---[RSA 2048]----+
| |
|. |
|oo . o |
|o.= + o |
|.* o * S |
|oo* oo * o |
|=*.. o= X |
|O. .*+ * = |
|+E==+o + |
+----[SHA256]-----+
- 现在,我们将 SSH 公钥复制到远程主机。运行以下命令:
$ ssh-copy-id remote_hostname
- 现在,您可以无密码登录到远程主机。运行以下
ssh命令:
$ ssh remote_hostname
创建和配置 cron 作业以进行任务调度
在本节中,我们将学习如何配置 Cron 作业。我们将使用 crontab 设置 Cron 作业。
如何做...
-
打开您的终端并转到
/etc文件夹并检查/cron文件夹。您将看到以下 cron 文件夹:-
/etc/cron.hourly -
/etc/cron.daily -
/etc/cron.weekly -
/etc/cron.monthly
-
-
现在,我们将把我们的 Shell 脚本复制到其中一个上述文件夹中。
-
如果需要每天运行您的 Shell 脚本,请将其放在
cron.daily文件夹中。如果需要每小时运行,请将其放在cron.hourly文件夹中,依此类推。 -
示例:编写一个脚本并将其放在
cron.daily文件夹中。通过授予必要的权限使脚本可执行。 -
现在,运行
crontab命令:
$ crontab -e
-
按下Enter,它将询问您的编辑器类型。默认情况下,它将打开
vi编辑器。在我的情况下,我选择了nano。现在,创建一个cron命令。创建cron命令的语法是:-
小时后的分钟数(0 到 59)
-
小时的军用时间格式(24 小时制)(0 到 23)
-
月份的日期(1 到 31)
-
月份(1 到 12)
-
一周的日期(0 或 7 是星期日,或使用适当的名称)
-
要运行的命令
-
-
如果在脚本名称之前的所有选项中输入
*,则脚本将每分钟执行,每小时执行一次,每月的每一天执行一次,每年的每个月和每周的每一天执行一次。 -
现在,在您的脚本中添加以下行:
* * * * * /etc/name_of_cron_folder/script.sh
-
保存文件并退出。
-
您可以通过运行以下命令列出现有作业:
$ crontab -l
- 要删除现有的 Cron 作业,请删除包含您的 Cron 作业的行并保存它。运行以下命令:
$ crontab -e
工作原理...
我们使用 crontab 命令添加和删除 Cron 作业。使用适当的设置每天、每小时、每月和每周执行您的脚本。
有系统地创建用户和组
在本节中,我们将学习如何通过 shell 脚本创建用户和组。
如何实现...
现在,我们将创建一个脚本来添加用户。useradd 命令用于创建用户。我们将使用 while 循环,它将读取我们的 .csv 文件,并且我们将使用 for 循环来添加 .csv 文件中存在的每个用户。
使用 add_user.sh 创建脚本:
#!/bin/bash
#set -x
MY_INPUT='/home/mansijoshi/Desktop'
declare -a SURNAME
declare -a NAME
declare -a USERNAME
declare -a DEPARTMENT
declare -a PASSWORD
while IFS=, read -r COL1 COL2 COL3 COL4 COL5 TRASH;
do
SURNAME+=("$COL1")
NAME+=("$COL2")
USERNAME+=("$COL3")
DEPARTMENT+=("$COL4")
PASSWORD+=("$COL5")
done <"$MY_INPUT"
for index in "${!USERNAME[@]}"; do
useradd -g "${DEPARTMENT[$index]}" -d "/home/${USERNAME[$index]}" -s /bin/bash -p "$(echo "${PASSWORD[$index]}" | openssl passwd -1 -stdin)" "${USERNAME[$index]}"
done
它是如何工作的...
在这个例子中,我们使用了 while 和 for 循环。while 循环将读取我们的 .csv 文件,它将为每一列创建数组。我们使用了 -d 来指定主目录,-s 来指定 bash shell,-p 来指定密码。
第六章:高级用户的脚本
本章将涵盖以下内容:
-
创建 Syslog 条目并生成警报
-
使用 DD 备份和擦除媒体、磁盘和分区
-
在 CLI 上创建图形和演示文稿
-
检查文件完整性和篡改
-
挂载网络文件系统并检索文件
-
从 CLI 浏览网页
-
无头捕获网络流量
-
查找二进制依赖关系
-
从不同位置获取时间
-
从脚本加密/解密文件
简介
本章将帮助高级用户了解如何通过 shell 脚本执行特定任务。这些任务包括使用logger命令创建 syslog 条目、备份、在 CLI 上创建图形和演示文稿、检查文件完整性和篡改、挂载网络文件系统并检索文件、浏览网页、捕获网络流量、查找二进制依赖关系,以及加密和解密文件。在本章中,用户将学习如何使用脚本来完成这些任务。
创建 Syslog 条目并生成警报
在本节中,我们将讨论 syslog 协议。我们还将学习logger命令,它是一个 shell 命令,并充当 syslog 模块的接口。logger命令可以在系统日志中创建条目。在本节中,我们还将通过脚本创建一个警报。
准备工作
除了打开终端,我们还需要确保你有一个文件来进行条目记录。
如何操作...
- 我们将使用
logger命令将file_name输入到syslog文件中。运行以下命令:
$ logger -f file_name
- 现在我们将编写一个脚本来创建一个警报。创建一个
create_alarm.sh脚本并在其中写入以下代码:
#!/bin/bash
declare -i H
declare -i M
declare -i cur_H
declare -i cur_M
declare -i min_left
declare -i hour_left
echo -e "What time do you Wake Up?"
read H
echo -e "and Minutes?"
read M
cur_H=`date +%H`
cur_M=`date +%M`
echo "You Selected "
echo "$H:$M"
echo -e "\nIt is Currently $cur_H:$cur_M"
if [ $cur_H -lt $H ]; then
hour_left=`expr $H - $cur_H`
echo "$H - $cur_H means You Have: $hour_left hours still"
fi
if [ $cur_H -gt $H ]; then
hour_left=`expr $cur_H - $H`
echo -e "\n$cur_H - $H means you have $hour_left hours left \n"
fi
if [ $cur_H == $H ]; then
hour_left=0
echo -e "Taking a nap?\n"
fi
if [ $cur_M -lt $M ]; then
min_left=`expr $M - $cur_M`
echo -e "$M -$cur_M you have: $min_left minutes still"
fi
if [ $cur_M -gt $M ]; then
min_left=`expr $cur_M - $M`
echo -e "$cur_M - $M you have $min_left minutes left \n"
fi
if [ $cur_M == $M ]; then
min_left=0
echo -e "and no minutes\n"
fi
echo -e "Sleeping for $hour_left hours and $min_left minutes \n"
sleep $hour_left\h
sleep $min_left\m
mplayer ~/.alarm/alarm.mp3
它是如何工作的...
现在我们将看到我们刚刚编写的logger命令和create_alarm脚本的描述:
-
logger命令在syslog文件中创建了关于你文件的条目,该文件位于你系统的/var/log目录下。你可以检查该文件。进入/var/log目录并运行nano syslog,你将找到该文件中的条目。 -
我们创建了一个脚本来创建一个警报。我们使用了
date命令获取日期和时间。我们还使用了sleep命令来阻止警报在特定时间触发。
使用 DD 备份和擦除媒体、磁盘和分区
在本节中,我们将讨论dd命令。dd命令代表数据复制器,主要用于转换和复制文件。在本节中,我们将学习如何备份和擦除媒体文件。
准备工作
除了打开终端,我们还需要确保你在当前目录中有必要的文件来进行备份、复制以及类似任务。
如何操作...
dd命令主要用于转换和复制文件。if参数代表输入文件,它是源文件。of代表输出文件,它是我们想要粘贴数据的目标。
- 运行以下命令将一个文件的内容复制到另一个文件:
# create a file 01.txt and add some content in that file.
# create another file 02.txt and add some content in that file.
$ dd if=/home/student/work/01.txt of=/home/student/work/02.txt bs=512 count=1
- 运行以下命令备份分区或硬盘:
$ sudo dd if=/dev/sda2 of=/home/student/hdbackup.img
dd命令也可用于擦除磁盘的所有内容。运行以下命令删除内容:
$ sudo dd if=/home/student/work/1.sh
它是如何工作的……
现在我们将看到之前的命令如何工作:
-
我们使用
dd命令将01.txt文件的内容复制到了02.txt文件中。 -
要运行此命令,我们必须具有超级用户权限。使用
dd命令,我们创建了备份并将其存储在hdbackup.img文件中。 -
使用
dd命令,我们清除了1.sh文件的内容。
在 CLI 上创建图形和演示文稿
在这一部分,我们将学习如何制作演示文稿以及如何在 CLI 上创建图形。为此,我们将使用名为 dialog 的工具。dialog 是一个 Linux 命令行工具,用于从用户那里获取输入并创建消息框。
准备工作
除了打开终端外,请确保你的系统中已安装对话工具。使用 apt 命令安装它。APT 代表高级软件包工具。通过 apt 命令,你可以从命令行管理 Debian 系列 Linux 的软件。apt 命令可以轻松与 dpkg 包管理系统交互。
如何操作……
- 我们将编写一个
Yes/No框的脚本。在该脚本中,我们将使用if条件。创建yes_no.sh脚本,并将以下内容添加到其中:
dialog --yesno "Do you wish to continue?" 0 0
a=$?
if [ "${a}" == "0" ]; then
echo Yes
else
echo No
fi
- 我们将使用
dialog的日历功能。创建一个calendar_dialog.sh脚本。在其中,我们将选择一个特定的日期:
dialog --calendar "Select a date... " 0 0 1 1 2018
val=$?
- 我们将使用
dialog的清单选项。创建一个checklist_dialog.sh脚本。在其中,我们将选择多个选项:
dialog --stdout --checklist "Enable the account options you want:" 10 40 3 \
1 "Home directory" on \
2 "Signature file" off \
3 "Simple password" off
- 现在,我们将编写一个脚本来提高图像的边框。创建一个
raise_border.sh脚本。我们将使用convert命令并加上raise选项:
convert -raise 5x5 mountain.png mountain-raised.png
它是如何工作的……
现在我们将看到前面脚本中选项和命令的描述:
-
我们使用 Linux 中的对话工具编写了一个
Yes/No框的代码。我们使用if条件来获取Yes或No的回答。 -
我们使用了对话工具的
--calendar选项,它要求选择一个日期。我们选择了 2018 年的一个日期。 -
我们使用了对话工具的
checklist选项,制作了一个包含三个选项的清单:主目录、签名文件和简单密码。 -
我们使用
convert命令和–raise选项提高了图像的边框,然后将新图像保存为mountain-raised.png。
检查文件的完整性和篡改
在这一部分,我们将学习如何检查文件的完整性以及如何通过编写简单的 Shell 脚本检查文件是否被篡改。为什么我们需要检查完整性?答案很简单:当服务器上存在密码和库文件,或者文件包含高度敏感数据时,管理员需要检查完整性。
准备工作
除了打开终端,还需要确保必要的文件和目录已准备好。
如何操作...
- 我们将编写一个脚本来检查目录中的文件是否被篡改。创建一个
integrity_check.sh脚本,并将以下代码添加到其中:
#!/bin/bash
E_DIR_NOMATCH=50
E_BAD_DBFILE=51
dbfile=Filerec.md5
# storing records.
set_up_database ()
{
echo ""$directory"" > "$dbfile"
# Write directory name to first line of file.
md5sum "$directory"/* >> "$dbfile"
# Append md5 checksums and filenames.
}
check_database ()
{
local n=0
local filename
local checksum
if [ ! -r "$dbfile" ]
then
echo "Unable to read checksum database file!"
exit $E_BAD_DBFILE
fi
while read rec[n]
do
directory_checked="${rec[0]}"
if [ "$directory_checked" != "$directory" ]
then
echo "Directories do not match up!"
# Tried to use file for a different directory.
exit $E_DIR_NOMATCH
fi
if [ "$n" -gt 0 ]
then
filename[n]=$( echo ${rec[$n]} | awk '{ print $2 }' )
# md5sum writes recs backwards,
#+ checksum first, then filename.
checksum[n]=$( md5sum "${filename[n]}" )
if [ "${rec[n]}" = "${checksum[n]}" ]
then
echo "${filename[n]} unchanged."
else
echo "${filename[n]} : CHECKSUM ERROR!"
fi
fi
let "n+=1"
done <"$dbfile" # Read from checksum database file.
}
if [ -z "$1" ]
then
directory="$PWD" # If not specified,
else
directory="$1"
fi
clear
if [ ! -r "$dbfile" ]
then
echo "Setting up database file, \""$directory"/"$dbfile"\".";
echo
set_up_database
fi
check_database
echo
exit 0
它是如何工作的...
当我们运行这个脚本时,它将创建一个名为filerec.md5的数据库文件,其中包含该目录中所有文件的数据。我们将使用这些文件作为参考。
挂载网络文件系统并获取文件
在本节中,我们将学习mount命令。要将文件系统挂载到文件系统树上,使用mount命令。此命令会指示内核挂载在特定设备上找到的文件系统。树中每个挂载的分区都有一个挂载点。
准备工作
除了打开终端,还需确保必要的文件和目录已准备好以进行挂载
如何操作...
- 我们将使用
mount命令来挂载文件系统。然后,我们将使用ro和noexec选项进行挂载:
$ mount -t ext4 /directorytobemounted /directoryinwhichitismounted -o ro,noexec
- 我们也可以使用默认选项挂载设备。运行以下命令使用默认选项挂载设备:
$ mount -t ext4 /directorytobemounted /directoryinwhichitismounted -o defaults
scp命令用于在两台主机之间安全传输文件。我们可以将文件从本地主机传输到远程主机,也可以在两台远程主机之间传输文件。运行以下命令将文件从远程主机传输到本地主机:
$ scp *from_host_name*:filename ***/local_directory_name***
它是如何工作的...
-
我们使用了
ext4文件系统。在mount命令中,我们首先指定了要挂载的目录,然后是我们要挂载它的目标目录,并使用了ro和noexec选项。 -
我们使用默认选项挂载了目录。
-
我们使用了
scp命令将文件从远程主机复制到本地主机。
从命令行浏览网页
在本节中,我们将学习如何通过命令行浏览网页。我们将使用 w3m 和 ELinks 浏览器通过命令行浏览网页。
w3m是一个基于文本的网页浏览器。使用 w3m,我们可以通过终端窗口浏览网页。
ELinks也是一个基于文本的网页浏览器。它支持基于菜单的配置、框架、表格、浏览和后台下载。ELinks 可以处理远程 URL 和本地文件。
准备工作
除了打开终端,我们还需要记住几点:
-
确保你已经安装了 w3m
-
确保你已经安装了 ELinks
如何操作...
- 我们将看到如何使用
w3m从命令行浏览网页。安装成功后,只需打开终端窗口,输入w3m,后跟网站名称:
$ w3m google.com
- 我们将看到如何使用
elinks从命令行浏览网页。安装成功后,只需打开终端窗口,输入elinks,后跟网站名称:
$ elinks google.com
它是如何工作的...
-
要浏览网站,请使用以下键盘组合:
-
Shift + U:此组合将打开一个新网页
-
Shift + B:此组合将使你返回上一网页
-
Shift + T:此组合将打开一个新标签页
-
-
以下是使用 ELinks 浏览网站的键盘快捷键:
-
转到网址 - g
-
打开新标签页 - t
-
向前 - u
-
返回 - 左
-
退出 - q
-
上一个标签页 - <
-
下一个标签页 - >
-
关闭标签页 - c
-
无头捕获网络流量
在本节中,我们将学习如何捕获流量。我们将使用一个名为tcpdump的抓包工具来捕获网络流量。该工具用于过滤或捕获通过网络传输或接收的 TCP/IP 数据包。
准备工作
除了打开终端外,我们还需要记住一些概念:
- 确保你的机器上安装了 tcpdump 工具
如何操作...
现在我们将使用一些tcpdump命令来捕获数据包:
- 要从某个接口捕获数据包,请使用以下代码:
$ sudo tcpdump -i eth0
- 要以 ASCII 值格式打印捕获的数据包,请使用以下代码:
$ sudo tcpdump -A -i eth0
- 要捕获特定数量的数据包,请使用以下代码:
$ sudo tcpdump -c 10 -i eth0
- 要以 HEX 和 ASCII 格式打印捕获的数据包,请使用以下代码:
$ sudo tcpdump -XX -i eth0
- 要在特定文件中捕获并保存数据包,请使用以下代码:
$ sudo tcpdump -w 111.pcap -i eth0
- 要捕获 IP 地址数据包,请使用以下代码:
$ sudo tcpdump -n -i eth0
- 要读取捕获的数据包,请使用以下代码:
$ sudo tcpdump -r 111.pcap
现在我们将查看tcpdump及我们使用的命令的解释。
它是如何工作的...
我们使用了 tcpdump Linux 工具,它用于捕获或过滤数据包。tcpdump 用于捕获特定接口上的数据包。我们为此使用了-i选项。我们可以将捕获的数据包保存到文件中。只需指定文件名并在tcpdump命令中指定-w选项。我们可以通过在tcpdump命令中指定-r选项来读取该文件。
查找二进制依赖项
在本节中,我们将检查可执行文件。我们将通过使用string命令来查找其中的字符串。
准备工作
除了打开终端,还要确保目录中有二进制文件。
如何操作...
- 首先,我们将检查可执行文件。运行以下命令:
$ file binary_name
- 现在我们将编写一个命令来查找二进制文件中的字符串。运行以下命令:
$ strings binary_name
- 我们可以通过运行以下命令对文件进行十六进制转储:
$ od -tx1 binary_name
- 我们可以通过运行以下命令列出二进制文件中的符号:
$ nm binary_name
- 你可以通过运行以下命令检查它已链接到哪个共享库:
$ ldd binary_name
它是如何工作的...
现在我们将查看之前命令的解释:
-
我们使用了
file命令来获取二进制文件的信息。我们还通过运行file命令获取了架构信息。 -
string命令将返回该二进制文件中的字符串。 -
通过运行
od命令,你将获得文件的十六进制转储。 -
二进制文件中存在符号。你可以通过运行
nm命令列出这些符号。 -
通过运行
ldd命令,你可以检查你的二进制文件链接了哪些共享库。
从不同地点获取时间
在本节中,我们将学习如何使用 date 命令从不同的时区获取时间。
准备开始
除了打开终端,你还需要具备基本的 date 命令知识。
如何操作...
- 我们将编写一个 shell 脚本来确定不同时间区域的时间。为此,我们将使用
date命令。创建一个timezones.shshell 脚本并在其中写入以下代码:
TZ=":Antarctica/Casey" date
TZ=":Atlantic/Bermuda" date
TZ=":Asia/Calcutta" date
TZ=":Europe/Amsterdam" date
如何操作...
在前面的脚本中,我们使用了 date 命令来获取时间。我们从四个不同的大陆——南极洲、大西洋、亚洲和欧洲获取了时间。
你可以在系统的 /usr/share/zoneinfo 文件夹中找到所有的时区。
从脚本中加密/解密文件
在本节中,我们将学习 OpenSSL。在本节中,我们将使用 OpenSSL 加密和解密消息和文件。
准备开始
除了打开终端,你还需要具备基本的编码和解码方案知识。
如何操作...
- 我们将加密和解密简单的消息。我们将使用 -base64 编码方案。首先,我们将加密一条消息。请在终端中运行以下命令:
$ echo "Welcome to Bash Cookbook" | openssl enc -base64
- 要解密消息,请在终端中运行以下命令:
$ echo " V2VsY29tZSB0byBCYXNoIENvb2tib29rCg==" | openssl enc -base64 -d
- 现在我们将加密和解密文件。首先,我们将加密一个文件。请在终端中运行以下命令:
$ openssl enc -aes-256-cbc -in /etc/services -out enc_services.dat
- 现在我们将解密一个文件。请在终端中运行以下命令:
$ openssl enc -aes-256-cbc -d -in enc_services.dat > services.txt
如何工作...
OpenSSL 是用于加密和解密消息、文件和目录的工具。在前面的例子中,我们使用了 -base64 编码方案。-d 选项用于解密。
要加密一个文件,我们使用了 -in 选项,后面跟上我们要加密的文件,-out 选项指示 OpenSSL 存储该文件。然后它将加密后的文件按照指定的名称存储。
第七章:编写 Bash 赢得和利润
在本章中,我们将介绍以下主题:
-
创建一个简陋的实用 HTTP 服务器
-
解析 RSS 订阅并输出 HTML
-
网络爬虫和文件收集
-
制作一个简单的 IRC 聊天机器人日志记录器
-
阻止由于 SSH 尝试失败而导致的 IP 地址
-
从 Bash 播放和管理音频
-
创建一个简单的 NAT 和 DMZ 防火墙
-
解析 GitHub 项目并生成报告
-
创建一个简易的增量远程备份系统
-
使用 Bash 脚本监控 udev 输入
-
使用 Bash 监控电池寿命并进行优化
-
使用 chroot 和受限 Bash shell 来保护脚本
介绍
本章将帮助您学习如何为多个任务使用命令和脚本。您将了解如何编写监控特定任务的 Bash 脚本。您将学习如何托管文件,解析 RSS 订阅并输出 HTML。您还将学习如何从网站复制内容,创建简单的 IRC 聊天机器人日志记录器,阻止 IP 地址,以及如何从命令行播放和管理音频,使用 iptables 创建简单的 NAT 防火墙和 DMZ,解析 GitHub 项目,使用 rsync 创建备份,监控设备事件、系统电池寿命和电源事件,并使用 chroot 提升安全性。
创建一个简陋的实用 HTTP 服务器
在此示例中,我们将讨论 Linux 中的 cURL 工具。cURL 用于从服务器传输数据。它支持许多协议,http 是其中之一。cURL 用于从 URL 传输数据。它有许多技巧可提供,如 http post、ftp post、用户认证、代理支持、文件传输、SSL 连接、cookies 等。
准备工作
除了打开终端,您需要确保系统上安装了 curl。
如何操作…
我们将学习如何在 Linux 中使用 curl 学习 HTTP GET 和 POST 方法。现在,我们将看到 GET 和 POST 的示例:
GET:HTTP GET 方法用于请求指定资源的数据。该命令是 HTTP GET 的示例,接受 JSON 格式的数据:
$ curl -H "Content-Type:application/json" -H "Accept:application/json" -X GET http://host/resource/name
另一个接受 XML 格式数据的 HTTP GET 示例如下所示:
$ curl -H "Content-Type:application/xml" -H "Accept:application/xml" -X GET http://host/resource/name
POST:HTTP POST 方法用于向服务器发送数据。它用于创建和更新资源。
一个简单的 POST 示例如下所示:
$ curl --data "name1=value1&name2=value2" http://host/resource/name
运行以下 curl 命令来上传文件:
$ curl -T "{File,names,separated,by,comma}" http://host/resource/name
它是如何工作的…
现在我们将学习 curl 命令中使用的选项:
-
首先,我们学习了
curl命令中的GET方法。我们接受两种类型的数据:JSON 和 XML。我们使用了-X选项并指定了请求。-H指定了头部。 -
其次,我们学习了
curl命令中的POST方法。--data标志用于POST请求。在简单示例中,我们仅仅是发送了简单的数据,在下一个示例中,我们使用-T选项来上传文件。
解析 RSS 订阅并输出 HTML
在此示例中,我们将使用 Linux 的 curl 和 xml2 工具来解析 RSS 订阅。
准备工作
-
您需要确保系统上安装了
curl。 -
你需要确保系统上已安装
xml2
如何操作…
我们将在curl中提供一个 URL,并使用xml2工具解析 RSS 提要。在终端中运行以下命令:
$ curl -sL https://imdb.com | xml2 | head
它是如何工作的…
我们使用了xml2和curlLinux 工具来解析 XML 格式的 RSS 提要;xml2将 XML 文档转换为文本。在输出中,每一行都是一个键,即 XML 路径,值通过=分隔。
爬取网页并收集文件
在这个教程中,我们将学习如何通过网络爬虫收集数据。我们将为此编写一个脚本。
准备工作
除了打开终端,你还需要掌握基本的grep和wget命令。
如何操作…
现在,我们将编写一个脚本来抓取imdb.com的内容。我们将在脚本中使用grep和wget命令来获取内容。创建scrap_contents.sh脚本,并写入以下代码:
$ mkdir -p data
$ cd data
$ wget -q -r -l5 -x 5 https://imdb.com
$ cd ..
$ grep -r -Po -h '(?<=href=")[^"]*' data/ > links.csv
$ grep "^http" links.csv > links_filtered.csv
$ sort -u links_filtered.csv > links_final.csv
$ rm -rf data links.csv links_filtered.csv
它是如何工作的…
在前面的脚本中,我们编写了代码来获取网站的内容。wget工具用于通过http、https和ftp协议从网络上下载文件。在这个例子中,我们从imdb.com获取数据,因此我们在wget中指定了网站名称。grep是一个命令行工具,用于搜索匹配正则表达式的数据。在这里,我们正在搜索特定的链接,这些链接会在网页抓取后保存在link_final.csv中。
创建一个简单的 IRC 聊天机器人日志记录器
在这个教程中,我们将创建一个简单的机器人日志记录器。这个脚本将记录几个频道并处理 pings。
准备工作
除了打开终端,你还需要掌握 IRC 的基本知识。
如何操作…
现在,我们将编写一个 IRC 日志记录机器人脚本。创建logging_bot.sh脚本,并写入以下代码:
#!/bin/bash
nick="blb$$"
channel=testchannel
server=irc.freenode.net
config=/tmp/irclog
[ -n "$1" ] && channel=$1
[ -n "$2" ] && server=$2
config="${config}_${channel}"
echo "NICK $nick" > $config
echo "USER $nick +i * :$0" >> $config
echo "JOIN #$channel" >> $config
trap "rm -f $config;exit 0" INT TERM EXIT
tail -f $config | nc $server 6667 | while read MESSAGE
do
case "$MESSAGE" in
PING*) echo "PONG${MESSAGE#PING}" >> $config;; *QUIT*) ;;
*PART*) ;;
*JOIN*) ;;
*NICK*) ;;
*PRIVMSG*) echo "${MESSAGE}" | sed -nr "s/^:([^!]+).*PRIVMSG[^:]+:(.*)/[$(date '+%R')] \1> \2/p";;
*) echo "${MESSAGE}";;
esac
done
它是如何工作的…
在这个脚本中,我们处理了 pings,同时也在记录一些频道。
阻止 SSH 登录失败的 IP 地址
在这个教程中,我们将学习如何找到失败的 SSH 登录尝试并阻止这些 IP 地址。为了查找失败的尝试,我们将使用grep和cat命令。SSH 服务器的登录尝试会被追踪并记录到rsyslog守护进程中。
准备工作
除了打开终端,我们需要记住一些概念:
-
grep和cat命令的基础知识 -
确保安装了
grep
如何操作…
我们将使用grep和cat命令查找失败的 SSH 登录尝试。首先,成为 root 用户,输入sudo su命令。接下来,运行以下命令,通过grep命令获取失败的尝试:
# grep "Failed password" /var/log/auth.log
你也可以使用cat命令来完成这个操作。运行以下命令:
# cat /var/log/auth.log | grep "Failed password"
你可以使用 tcp-wrapper 阻止某个特定 IP 地址的 SSH 登录失败尝试。首先进入/etc目录,查找hosts.deny文件,在文件中添加以下内容并保存:
sshd: 192.168.0.1/255.255.255.0
它是如何工作的…
在这其中,我们使用了cat和grep命令。cat命令最常见的用途是显示文件内容,而grep是一个 Linux 工具,用于在文件中搜索特定模式;然后,它将显示包含该特定模式的行。
在前面的示例中,我们在查找失败的登录尝试。我们使用grep命令匹配这些关键字,然后用cat命令显示它们。
要阻止一个 IP 地址,我们只需在hosts.deny文件中添加一行,这将阻止该特定的 IP 地址。
从 Bash 播放和管理音频
在本教程中,我们将了解如何使用一个名为SoX的命令行播放器从命令行播放音乐。SoX 支持大多数音频格式,如 mp3、wav、mpg 等。
准备工作
除了打开终端,我们还需要记住一些概念:
-
确保您的系统中已安装 SoX
-
确保您已安装 sox libsox-fmt-all
如何操作…
- 我们将从命令行播放音频。为此,我们将使用 SoX 命令行播放器。在成功安装
sox和libsox-fmt-all后,导航到存有音频文件的目录,并运行以下命令以播放所有.mp3文件:
$ play *mp3
- 要播放特定的歌曲,请运行以下命令:
$ play file_name.mp3
工作原理…
SoX 用于读取和写入音频文件。libsox库是 SoX 工具的核心。play命令用于播放音频文件。要播放所有 mp3 文件,我们使用*mp3。要播放特定文件,只需在play命令后写入文件名及扩展名。
创建一个简单的 NAT 和 DMZ 防火墙
在本教程中,我们将使用 iptables 创建一个简单的 NAT 防火墙和 DMZ。
准备工作
除了打开终端,您还需要确保您的机器中已安装iptables。
如何操作…
我们将编写一个脚本来使用iptables设置 DMZ。创建一个dmz_iptables.sh脚本,并在其中写入以下代码:
# set the default policy to DROP
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -P FORWARD DROP
# to configure the system as a router, enable ip forwarding by
sysctl -w net.ipv4.ip_forward=1
# allow traffic from internal (eth0) to DMZ (eth2)
iptables -t filter -A FORWARD -i eth0 -o eth2 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -t filter -A FORWARD -i eth2 -o eth0 -m state --state ESTABLISHED,RELATED -j ACCEPT
# allow traffic from internet (ens33) to DMZ (eth2)
iptables -t filter -A FORWARD -i ens33 -o eth2 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -t filter -A FORWARD -i eth2 -o ens33 -m state --state ESTABLISHED,RELATED -j ACCEPT
#redirect incoming web requests at ens33 (200.0.0.1) of FIREWALL to web server at 192.168.20.2
iptables -t nat -A PREROUTING -p tcp -i ens33 -d 200.0.0.1 --dport 80 -j DNAT --to-dest 192.168.20.2
iptables -t nat -A PREROUTING -p tcp -i ens33 -d 200.0.0.1 --dport 443 -j DNAT --to-dest 192.168.20.2
#redirect incoming mail (SMTP) requests at ens33 (200.0.0.1) of FIREWALL to Mail server at 192.168.20.3
iptables -t nat -A PREROUTING -p tcp -i ens33 -d 200.0.0.1 --dport 25 -j DNAT --to-dest 192.168.20.3
#redirect incoming DNS requests at ens33 (200.0.0.1) of FIREWALL to DNS server at 192.168.20.4
iptables -t nat -A PREROUTING -p udp -i ens33 -d 200.0.0.1 --dport 53 -j DNAT --to-dest 192.168.20.4
iptables -t nat -A PREROUTING -p tcp -i ens33 -d 200.0.0.1 --dport 53 -j DNAT --to-dest 192.168.20.4
工作原理…
在前面的代码中,我们使用了iptables来设置 DMZ。在这个脚本中,我们允许从互联网到 DMZ 的内部流量。
解析 GitHub 项目并生成报告
在本教程中,我们将使用 git 命令解析 GitHub 项目。
准备工作
除了打开终端,您还需要确保系统中已安装git。
如何操作…
我们将使用 git 工具来解析项目。运行此命令来解析一个项目:
$ git https://github.com/torvalds/linux.git
工作原理…
工具 git 用于处理大型项目。此命令用于从服务器克隆 git 仓库。
创建一个简易的增量远程备份
在本教程中,我们将学习如何创建备份和增量备份。我们将编写一个脚本来获取增量备份。
准备工作
除了打开终端,我们还需要记住一些概念:
-
tar、gunzip和gzip命令的基础知识。 -
确保你的系统中存在必要的目录。
如何操作……
- 首先,选择一个你希望备份的目录。我们将使用
tar命令。假设你要备份/work目录:
$ tar cvfz work.tar.gz /work
- 现在,我们将编写一个脚本来执行增量备份。创建一个
incr_backup.sh脚本并在其中编写以下代码:
#!/bin/bash
gunzip /work/tar.gz
tar uvf /work.tar /work/
gzip /work.tar
它是如何工作的……
现在,我们将了解前面命令中使用的选项以及脚本:
-
在命令中,我们使用的选项如下:
-
c:此选项将创建一个归档文件。 -
v:此选项用于详细模式。我们可以查看正在归档的文件。 -
f:此选项将带你回到文件。 -
z:此选项用于用 gzip 压缩文件。
-
-
在此脚本中,我们使用的选项如下:
-
u:此选项用于更新归档。 -
v:表示详细模式。 -
f:此选项用于返回文件。
-
使用 Bash 脚本监控 udev 输入
在本教程中,我们将学习 evtest Linux 工具。此工具用于监控输入设备事件。
准备就绪
除了打开一个终端外,你还需要确保系统中已安装 evtest。
如何操作……
evtest 是一个命令行工具。它将显示输入设备的信息。它会显示设备支持的所有事件,然后监控该设备。我们只需以超级用户权限运行 evtest 命令。按照如下方式运行该命令:
$ sudo evtest /dev/input/event3
它是如何工作的……
evtest 命令将输出如下内容:
Input driver version is 1.0.1
Input device ID: bus 0x11 vendor 0x2 product 0x13 version 0x6
Input device name: "VirtualPS/2 VMware VMMouse"
Supported events:
Event type 0 (EV_SYN)
Event type 1 (EV_KEY)
Event code 272 (BTN_LEFT)
Event code 273 (BTN_RIGHT)
Event type 2 (EV_REL)
Event code 0 (REL_X)
Event code 1 (REL_Y)
Event code 8 (REL_WHEEL)
Properties:
Property type 0 (INPUT_PROP_POINTER)
Testing ... (interrupt to exit)
输出显示由内核呈现的信息。
使用 Bash 监控电池寿命并优化其性能
在本教程中,我们将学习 TLP Linux 工具。TLP 是一个命令行工具,用于电源管理,可以优化电池寿命。
准备就绪
除了打开一个终端外,你还需要确保系统中已安装 TLP。
如何操作……
TLP 的配置文件位于 /etc/default/ 目录下,文件名为 tlp。安装后,它会自动作为服务启动。我们可以通过运行 systemctl 命令检查它是否正在运行,如下所示:
$ sudo systemctl status tlp
运行以下命令以获取操作模式:
$ sudo tlp start
要获取系统信息以及 TLP 状态,运行以下命令:
$ sudo tlp-stat -s
要查看 TLP 配置,运行以下命令:
$ sudo tlp-stat -c
要获取所有电源配置,请运行以下命令:
$ sudo tlp-stat
要获取电池信息,请使用以下命令:
$ sudo tlp-stat -b
要获取系统的风扇速度和温度,运行下一个命令:
$ sudo tlp-stat -t
要获取处理器数据,运行以下命令:
$ sudo tlp-stat -p
它是如何工作的……
TLP 是一个命令行工具,具有自动化后台任务。TLP 有助于优化 Linux 操作系统笔记本电脑的电池寿命。
我们通过运行 sudo tlp-stat 并使用各种选项来获取电池寿命、处理器数据、温度和风扇速度的信息。tlp-stat 显示电源管理设置。我们与 tlp-stat 一起使用的选项如下:
-
-b:电池 -
-t:温度 -
-p:处理器数据 -
-c:配置 -
-s:系统信息
使用 chroot 和受限的 Bash Shell 来确保脚本的安全
在本食谱中,我们将学习 chroot 和受限 bash(rbash)。chroot命令用于更改根目录。使用 rbash,我们可以限制 bash shell 的某些功能,以达到一定的安全目的。
准备就绪
除了打开终端外,您还需要确保系统中已安装rbash。
如何操作……
- 现在,我们来看看启动
rbash的命令。运行以下命令:
$ bash -r
or
$ rbash
- 现在我们将测试一些限制。首先,我们将尝试更改目录。运行以下命令:
$ cd work/
接下来,我们将尝试向文件中写入一些内容。运行给定的命令将内容写入文件:
ls > log.txt
它是如何工作的……
使用rbash后,系统的访问将受到限制。在之前的示例中,我们通过输入bash -r或rbash来启动受限 shell。
接下来,我们尝试更改目录,但我们收到rbash: cd: restricted的消息,因此无法在rbash中更改目录。另外,我们也无法向文件中写入内容。
第八章:高级脚本编写技巧
本章将介绍以下内容:
-
计算和减少脚本的运行时间
-
编写单行条件语句和循环
-
避免“命令未找到”警告/错误并提高可移植性
-
创建配置文件并将其与脚本配合使用
-
改进你的 Shell – GCC 和命令行颜色
-
添加别名,修改用户路径/变量
-
将输出回显到原始终端设备
-
为 Bash 脚本创建简单的前端 GUI
-
编译和安装你自己的 Bash Shell
-
记录终端会话以实现自动化
-
通过示例编写高质量的脚本
介绍
本章将帮助读者学习高级脚本编写技巧,以及如何自定义他们的 Shell。用户将学习如何计算并减少脚本的运行时间。编写单行循环和条件语句将变得非常容易。用户将学习如何编写脚本以避免警告和错误,学习如何创建配置文件并使用它。用户还将学会如何改进 Shell,添加别名,修改路径变量,并将输出回显到终端设备。用户还将了解如何记录终端会话和编写脚本。
计算和减少脚本的运行时间
在这个示例中,我们将学习如何计算并减少脚本的运行时间。一个简单的time命令将有助于计算执行时间。
准备工作
除了打开终端外,请确保系统中已经存在必要的脚本。
如何操作…
现在,我们将编写一个简单的脚本,包含几个命令,然后使用time命令来获取该脚本的运行时间。创建一个名为cal_runtime.sh的脚本,并在其中写入以下代码。
clear
ls -l
date
sudo apt install python3
工作原理…
现在,我们已经编写了一个脚本cal_runtime.sh,并在该脚本中包含了四个命令:clear、ls、date和一个python3安装命令。按如下方式运行你的脚本:
$ time bash cal_runtime.sh
执行后,你将在输出的底部看到脚本的运行时间。
编写单行条件语句和循环
在这个示例中,我们将编写包含单行条件语句和循环语句的脚本。
准备工作
你需要具备基本的条件语句和循环语句知识。
如何操作…
现在我们将编写一个包含单行条件语句的脚本。在这个脚本中,我们将编写一个简单的if条件。创建一个名为if_oneline.sh的脚本,并在其中写入以下代码:
a=100
if [ $a -eq 100 ]; then echo "a is equal to $a"; fi
接下来,我们将编写一个包含单行loop语句的脚本。在其中,我们将编写一个执行 10 次的命令。创建一个名为for_online.sh的脚本,并在其中写入以下代码:
for i in {1..10}; do echo "Hello World"; done
现在,我们将编写一个包含单行while语句的脚本。这个脚本将是一个无限循环。创建一个名为while_oneline.sh的脚本,并在其中写入以下代码:
x=10
while [ $x -eq 10 ]; do echo $x; sleep 2; done
工作原理…
在本教程中,我们为一行 if 语句、for 循环和 while 循环编写了三个脚本。在 if 示例中,条件将被检查,然后输出将显示在屏幕上。
在 for 循环示例中,echo 命令将执行 10 次,"Hello world" 会在屏幕上显示 10 次。在 while 示例中,它将是一个无限循环。我们已设置了 2 秒的休眠时间,以便每 2 秒执行一次输出 10 的操作。
避免 "command not found" 警告/错误并提高可移植性
在本教程中,我们将学习如何避免 Shell 脚本中的警告和错误。为此,我们将使用重定向的概念。
准备中
除了打开终端外,你还需要具备重定向技术的基本知识。
如何操作...
有时,在调试你的 Shell 脚本时,你可能不想查看错误或警告信息以及标准输出。因此,我们将使用重定向技术。现在,我们将在终端中输入 lynda 命令,按如下方式运行该命令。
$ lynda

你将遇到 command not found 错误。我们可以通过运行以下命令来避免这个错误。
$ lynda 2> log.txt
我们将编写一个声明变量时语法错误的脚本。并将该错误信息重定向到 log.txt。创建一个名为 avoid_error.sh 的脚本,并在其中写入以下内容。
echo "Hello World"
a = 100
b=20
c=$((a+b))
echo $a
在第二行,a = 100,我们会遇到一个错误。
它是如何工作的...
无论你按下 Enter 后遇到什么错误信息,那个错误都会被存储在 log.txt 中。现在,按照以下方式查看 log.txt 的内容:
$ cat log.txt

通过这种方式,你可以避免警告和错误。现在,按照以下方式运行脚本:
$ bash avoid_error.sh 2> log.txt
错误信息将被存储在 log.txt 中。你可以通过运行 cat log.txt 命令查看它。
创建一个配置文件并与脚本一起使用
在本教程中,我们将创建一个配置文件,并在我们的 Shell 脚本中使用它。
准备中
除了打开终端外,你还需要具备创建脚本和配置文件的基本知识。
如何操作...
现在,我们将创建一个脚本和配置文件。配置文件的扩展名为 .conf。创建一个名为 sample_script.sh 的脚本,并在其中写入以下代码:
#!/bin/bash
typeset -A config
config=(
[username]="student"
[password]=""
[hostname]="ubuntu"
)
while read line
do
if echo $line | grep -F = &>/dev/null
then
varname=$(echo "$line" | cut -d '=' -f 1)
config[$varname]=$(echo "$line" | cut -d '=' -f 2-)
fi
done < sampleconfig.conf
echo ${config[username]}
echo ${config[password]}
echo ${config[hostname]}
echo ${config[PROMPT_COMMAND]}
我们现在将创建一个配置文件。创建一个名为 sampleconfig.conf 的文件,并在其中写入以下代码:
password=training
echo rm -rf /
PROMPT_COMMAND='ls -l'
hostname=ubuntu; echo rm -rf /
它是如何工作的...
在运行脚本后,用户名、密码和主机名将会显示在我们在 PROMPT_COMMAND 中提到的命令里。
改善你的 Shell – GCC 和命令行颜色
在本教程中,我们将学习如何改进 Shell。我们将使用 PS1 Bash 环境变量来实现这一点。
准备中
除了终端之外,你还需要具备基本的 PS1 知识。
如何操作...
终端外观由 PS1 shell 变量控制。PS1 中允许包含反斜杠转义特殊字符。
首先,我们将查看系统中 PS1 的当前内容。为此,请运行以下命令:
$ echo $PS1

这里是反斜杠转义特殊字符:
-
\u:当前用户名 -
\h:主机名 -
\W:当前工作目录 -
\$: 如果用户是 root,则显示#`;否则仅显示 $ -
\@:当前时间,12 小时制 AM/PM 格式
现在,我们将修改我们的 Bash。运行以下命令:
$ PS1="[\\u@\\h \\W \\@]\\$"

现在,我们将编写一个命令来更改颜色。
要使文本颜色为蓝色,请运行以下命令:
$ PS1="[\\u@\\h \\W \\@]\\$\\e0;34m"
![
现在我们来看看 tput 命令。运行以下命令:
$ PS1="\[$(tput setaf 3)\]\u@\h:\w $ \[$(tput sgr0)\]"

$ PS1="\[$(tput setaf 6)\]\u@\h:\w $ \[$(tput sgr0)\]"

工作原理...
我们使用 PS1 shell 变量来改进我们的 shell。我们在 PS1 变量中添加了颜色并更改了颜色。我们还使用了 tput 命令。此命令还用于修改设置。setaf 设置前景色,setab 设置背景色。tput 命令的颜色代码如下:
| 代码 | 颜色 |
|---|---|
| 0 | 黑色 |
| 1 | 红色 |
| 2 | 绿色 |
| 3 | 黄色 |
| 4 | 蓝色 |
| 5 | 洋红色 |
| 6 | 青色 |
| 7 | 白色 |
添加别名,并改变用户路径/变量
在这个示例中,我们将创建一个命令的别名并改变用户路径变量。我们将学习 alias 命令。使用 alias 命令,我们将为其他命令创建别名。
准备工作
除了打开一个终端外,我们还需要基本的别名命令知识。
如何做...
- 我们将为
pwd命令创建一个别名。运行此命令:
$ alias p=pwd
- 现在,我们将为
ls命令创建一个别名。运行以下命令:
$ alias l=”ls -l”
工作原理...
alias 命令用于创建常用命令的快捷方式。
-
我们为
pwd命令创建了别名p。因此,只需运行p命令即可获取当前工作目录。 -
我们为
ls命令创建了别名l。因此,只需运行l命令即可获取列表。
将输出回显到原始终端设备
在这个示例中,我们将学习如何将一个终端的输出回显到另一个终端。为了实现这一点,我们将使用 tty。
准备工作
除了打开一个终端外,您还需要基本的 tty 知识。
如何做...
tty 意味着 电传打字机。tty 显示与标准输入连接的终端的文件名。在 Linux 中,一切都是文件。因此,tty 打印连接到标准输入的终端的文件名。
现在,打开一个终端并运行 tty:
$ tty
运行此命令后,将显示当前 tty 会话。
打开另一个终端 B 并执行相同操作;您将获取该终端的 tty 会话。
现在我有两个 tty 会话如下:
-
终端 A =
/dev/pts/4 -
终端 B =
/dev/pts/7
在终端 A 中,运行以下命令:
$ echo "Hello World" > /dev/pts/7
现在检查终端 B;Hello world 将显示在终端上。再次运行以下命令,将另一个字符串发送到终端 B。
$ echo "Hello This is John" > /dev/pts/7
它是如何工作的…
以下是在终端 B 上的输出。
Hello World
Hello This is John
为 Bash 脚本创建简单的前端 GUI
在这个教程中,我们将创建一个简单的 GUI。我们将使用 zenity 工具来实现它。
准备就绪
除了打开终端外,请确保你的系统中安装了 zenity。
如何实现…
Zenity 用于通过一个命令为 Shell 脚本添加图形界面。Zenity 默认包含在 Ubuntu 中。如果没有,请按以下方式安装:
$ sudo apt install zenity
首先,我们将在 Shell 脚本中捕获 yes/no 响应,然后根据按钮执行不同的命令。运行以下命令以获取 yes/no 响应。
$ zenity --question --title="Query" --text="Would you like to run the script?"

运行以下命令以获取错误消息框:
$ zenity --error --title="An Error Occurred" --text="A problem occurred while running the shell script."

运行以下命令以获取文本输入框:
$ zenity --entry --title="Favorite Website" --text="What is your favorite website?"

现在我们将创建一个脚本,用户需要输入一个时间,直到该时间过去,用户才可以继续。当等待时间结束时,用户将收到一条消息。创建一个脚本 user_wait.sh,并在其中写入以下内容。
#!/bin/bash
time=$(zenity --entry --title="Timer" --text="Enter a duration for the timer.\n\n Use 10s for 10 seconds, 20m for 20 minutes, or 3h for 3 hours.")
sleep $time
zenity --info --title="Timer Complete" --text="The timer is over.\n\n It has been $time."
使用 10s 表示 10 秒,20m 表示 20 分钟,或 3h 表示 3 小时,如下截图所示:


定时器已结束:

它是如何工作的…
Zenity 是一个开源应用程序,通过 Shell 脚本和命令行显示对话框。使用 zenity,用户与 Shell 之间的通信将变得简单:
-
--question:显示问题对话框 -
--error:显示错误对话框 -
--entry:显示文本输入对话框 -
--info:显示信息对话框
在脚本 user_wait.sh 中,我们使用文本输入对话框和信息对话框创建了一个定时器。我们创建了一个名为 time 的变量。我们让用户输入一个时间段,该值将存储在 time 变量中。然后,我们将该变量传递给 sleep。因此,在此期间,用户需要等待。时间结束后,用户将收到信息对话框,表示时间已到。
编译并安装你自己的 Bash shell
在这个教程中,我们将学习如何编译并安装 Bash shell。我们将使用 SHC,它是一个 Shell 脚本编译器。
准备就绪
除了打开终端外,请确保你的系统中安装了 SHC。
如何实现…
现在我们将编写一个简单的 Shell 脚本来打印 “Hello World”。使用 SHC,Shell 脚本将直接转换为二进制文件。创建一个脚本 hello.sh,并在其中写入以下内容。
#!/bin/bash
echo "Hello World"
a=10
b=20
c=$((a+b))
echo $c
现在,要记录所有命令,请运行以下 logger 命令:
$ logger -f hello.sh
它是如何工作的…
执行脚本后,将创建两个额外的文件。文件包括:
-
hello.sh.x:此文件是以二进制格式加密的 Shell 脚本的二进制版本 -
hello.sh.x.c:该文件是hello.sh的 C 源代码
现在,按如下方式执行加密的 Shell 脚本:
$ ./hello.sh.x
日志记录命令将在你的系统的/var/log目录下的 syslog 文件中记录关于你的文件的条目。你可以查看该文件。导航到/var/log目录并运行nano syslog;你将在其中找到相关条目。
录制终端会话以实现自动化
在本教程中,我们将学习如何记录终端会话。我们将使用ttyrec工具来实现。
准备工作
除了打开终端外,确保你的系统已安装 ttyrec。
如何操作……
要记录终端数据,我们使用 ttyrec 工具。你还可以播放录制的数据。现在,输入ttyrec命令以记录终端会话。你可以通过输入exit来结束录制。运行以下命令以记录终端会话:
$ ttyrec
它是如何工作的……
当你运行ttyrec命令时,终端会话的记录将开始。录制会一直持续,直到你输入exit。一旦输入exit,录制将停止,并且一个文件将在当前工作目录中创建。文件名将是ttyrecord。你可以通过运行ttyplay命令来播放此文件。如下所示运行ttyrecord文件:
$ ttyplay ttyrecord
通过示例编写高质量脚本
在本教程中,我们将看到 Shell 脚本中的函数。我们将看到如何在程序的各个部分逐步进行测试,使用函数。函数有助于提高程序的可读性。
准备工作
除了打开终端外,你还需要具备基本的函数知识。
如何操作……
我们将在 Shell 脚本中编写一个简单的函数来返回当前的日期和时间。创建一个脚本function_example.sh,并在其中编写如下代码:
#!/bin/bash
print_date()
{
echo "Today is `date`"
return
}
print_date
现在我们将创建另一个脚本,其中包含两个相同名称的函数。创建一个脚本function2.sh,并在其中编写以下内容。
#!/bin/bash
display ( ) {
echo 'First Block'
echo 'Number 1'
}
display ( ) {
echo 'Second Block'
echo 'Number 2'
}
display
exit 0
它是如何工作的……
在第一个脚本中,我们创建了一个名为print_date()的函数,并通过该函数打印了一个日期。
在第二个脚本中,我们编写了两个相同名称的函数。然而,执行后,最后一个值会被打印到屏幕上。所以在这种情况下,第二块编号 2 会被打印到屏幕上。


浙公网安备 33010602011771号