红帽企业-Linux-故障排除指南-全-
红帽企业 Linux 故障排除指南(全)
原文:
zh.annas-archive.org/md5/4376391B1DCEF164F3ED989478713CD5
译者:飞龙
前言
《精通 Linux Shell 脚本编程》将成为你的圣经,也是一个手册,用于在 Linux、OS X 或 Unix 中创建和编辑 bash shell 脚本。从基础知识开始,我们迅速帮助你用实际示例创建有用的脚本。这样,你的学习变得高效而迅速。每一章中,我们都提供了代码的解释和示例,因此这本书不仅是一本学习书,还可以作为一个现成的参考书,如果你需要了解如何编写特定任务的程序。
本书内容
第一章,“Bash 脚本的什么和为什么”,解释了如何创建和命名脚本。一旦你创建了脚本,你就可以将其设置为可执行,并欢迎自己进入这个世界。如果你对脚本几乎一无所知,那么你可以从这里开始。
第二章,“创建交互式脚本”,介绍了我们需要以更灵活的方式工作并在脚本执行过程中接受参数甚至提示用户输入的脚本。我相信你已经看到过类似的脚本,询问安装目录或要连接的服务器。
第三章,“附加条件”,介绍了关键词的使用,比如“if”,以及像“test”这样的命令。它告诉我们如何在代码中开始创建决策结构,然后在没有提供参数的情况下提示用户输入;否则,我们可以静默运行。
第四章,“创建代码片段”,介绍了非常强大的 vim 文本编辑器,还有语法高亮帮助我们编辑脚本。然而,我们也可以读取当前脚本的文件。通过这种方式,我们可以创建代表常用代码块的代码片段。
第五章,“替代语法”,告诉我们如何将测试命令缩写为单个,我们还可以根据需要使用[[和((。
第六章,“循环迭代”,介绍了循环也是条件语句。我们可以在条件为真或假时重复一段代码。通过使用 for、while 或 until,我们可以让脚本完成重复的代码序列。
第七章,“使用函数创建构建块”,介绍了函数如何封装我们在脚本中需要重复的代码。这可以提高可读性,以及脚本的易维护性。
第八章,“介绍 sed”,流编辑器,告诉我们如何使用 sed 动态编辑文件并在脚本中实现它。在这一章中,我们将学习如何使用和处理 sed。
第九章,“自动化 Apache 虚拟主机”,介绍了当我们创建一个脚本来在 Apache HTTPD 服务器上创建虚拟主机时,我们可以带走的实用配方。我们在脚本中使用 sed 来编辑用于定义虚拟主机的模板。
第十章,“Awk 基础”,介绍了我们如何开始处理命令行中的文本数据,使用 awk 是 Linux 中另一个非常强大的工具。
[第十一章,使用 Awk 总结日志,告诉我们关于我们在 awk 中查看的第一个实际示例,允许我们处理 Web 服务器上的日志文件。它还介绍了如何报告最经常访问服务器的 IP 地址,以及发生了多少错误以及错误的类型。
第十二章,使用 Awk 进行更好的 lastlog,查看了我们可以在 awk 中使用的更多示例,以过滤和格式化 lastlog 命令提供的数据。它深入到我们想要的具体信息,并删除我们不需要的信息。
第十三章,使用 Perl 作为 Bash 脚本的替代方案,介绍了 Perl 脚本语言及其提供的优势。我们不仅限于使用 bash,还有 Perl 作为脚本语言。
第十四章,使用 Python 作为 Bash 脚本的替代方案,向您介绍了 Python 和 Python 之禅,这将帮助您学习所有编程语言。与 Perl 一样,Python 是一种可以扩展脚本功能的脚本语言。
本书所需内容
使用带有 bash shell 的任何 Linux 发行版应该足以完成本书。在本书中,我们使用的是在 Raspberry Pi 上使用 Raspbian 发行版生成的示例;但是,任何 Linux 发行版都应该足够。如果您在苹果系统的 OS X 命令行中,则应该能够完成大部分练习,而无需 Linux。
本书适合人群
精通 Linux Shell 脚本是为那些想要在日常生活中自动化任务、节省时间和精力的 Linux 管理员编写的。您需要具有命令行经验,并熟悉需要自动化的任务。预期具有基本的脚本知识。
约定
在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"我们再次看到basename
首先被评估,但我们没有看到运行该命令所涉及的更详细的步骤。"
代码块设置如下:
#!/bin/bash
echo "You are using $0"
echo "Hello $*"
exit 0
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
#!/bin/bash
echo "You are using $0"
echo "Hello $*"
exit 0
任何命令行输入或输出都将按以下方式编写:
$ bash -x $HOME/bin/hello2.sh fred
新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:"单击下一步按钮将您移至下一个屏幕。"
注意
警告或重要说明会以这样的方式出现在一个框中。
提示
提示和技巧会以这种方式出现。
读者反馈
我们的读者的反馈总是受欢迎的。让我们知道您对本书的看法——您喜欢或不喜欢什么。读者的反馈对我们很重要,因为它有助于我们开发您真正能够充分利用的书籍。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>
,并在您的消息主题中提及书名。
如果您在某个专题上有专业知识,并且有兴趣编写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的自豪所有者,我们有很多事情可以帮助您充分利用您的购买。
下载示例代码
您可以从www.packtpub.com
的帐户中下载您购买的所有 Packt Publishing 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support
注册,直接将文件发送到您的电子邮件。
下载本书的彩色图片
我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。彩色图片将帮助您更好地理解输出中的变化。您可以从以下网址下载此文件:www.packtpub.com/sites/default/files/downloads/MasteringLinuxShellScripting_ColorImages.pdf
。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误还是会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata
报告,选择您的书,点击勘误提交表链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该书标题的勘误部分的任何现有勘误列表中。
要查看先前提交的勘误,请转到www.packtpub.com/books/content/support
并在搜索字段中输入书名。所需信息将出现在勘误部分下。
盗版
互联网上侵犯版权材料的盗版问题是所有媒体的持续问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何形式的非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。
请通过链接<copyright@packtpub.com>
与我们联系,提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值的内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,可以通过<questions@packtpub.com>
与我们联系,我们将尽力解决问题。
第一章:使用 Bash 脚本的“什么”和“为什么”
欢迎来到 bash 脚本的“什么”和“为什么”。我的名字是 Andrew Mallett,我是一个 bash 脚本迷,或者更准确地说是一个脚本迷。作为管理员,我看不出手动执行重复任务的必要性。当我们选择脚本来执行我们不喜欢的繁琐任务时,我们就有更多时间做更有趣的事情。在本章中,我们将向您介绍 bash 脚本的“什么”和“为什么”。如果您是新手,它将帮助您熟悉脚本,并为那些有更多经验并希望提高技能的人提供一些很好的见解。在本章中,每个元素都旨在增加您的知识,以帮助您实现您的目标。在这个过程中,我们将涵盖以下主题:
-
Bash 漏洞
-
bash 命令层次结构
-
为脚本准备文本编辑器
-
创建和执行脚本
-
调试您的脚本
Bash 漏洞
对于本书,我将完全在运行 Raspbian 的 Raspberry Pi 2 上工作,Raspbian 是类似于 Debian 和 Ubuntu 的 Linux 发行版;尽管对您来说,您选择使用的操作系统和 bash 的版本都是无关紧要的,实际上,我使用的 bash 版本是 4.2.37(1)。如果您使用的是 OS X 操作系统,默认的命令行环境是bash。
要返回正在使用的操作系统,请输入以下命令(如果已安装):
$ lsb_release -a
我的系统的输出如下截图所示:
确定您正在使用的 bash 版本的最简单方法是打印一个变量的值。以下命令将显示您的 bash 版本:
$ echo $BASH_VERSION
以下截图显示了我的系统的输出:
2014 年,bash 中出现了一个广为人知的 bug,这个 bug 已经存在多年了——shell-shock bug。如果您的系统保持最新状态,那么这可能不是一个问题,但值得检查。该 bug 允许恶意代码从格式不正确的函数中执行。作为标准用户,您可以运行以下代码来测试系统上的漏洞。这段代码来自 Red Hat,不是恶意的,但如果您不确定,请寻求建议。
以下是来自 Red Hat 的用于测试漏洞的代码:
$ env 'x=() { :;}; echo vulnerable''BASH_FUNC_x()=() { :;}; echo vulnerable' bash -c "echo test"
如果您的系统没有这个第一个漏洞,输出应该如下截图所示:
要测试这个 bug 的最后一个漏洞,我们可以使用以下测试,同样来自 Red Hat:
cd /tmp; rm -f /tmp/echo; env 'x=() { (a)=>\' bash -c "echo date"; cat /tmp/echo
修补版本的 bash 的输出应该如下截图所示:
如果这两个命令行的输出不同,那么您的系统可能容易受到 shell-shock 的影响,我建议更新 bash,或者至少向安全专业人员寻求进一步建议。
bash 命令层次结构
当在 bash shell 上工作时,当您舒适地坐在提示符前急切地等待输入命令时,您很可能会认为只需输入并按下Enter键就是一件简单的事情。您应该知道,事情从来不会像我们想象的那么简单。
命令类型
例如,如果我们输入ls
来列出文件,我们可能会认为我们正在运行该命令。这是可能的,但我们经常运行别名。别名存在于内存中,作为命令或带有选项的快捷方式;在检查文件之前,我们使用这些别名。bash shell 内置命令type
可以在这里帮助我们。type
命令将显示在命令行输入的给定单词的命令类型。命令类型如下所示:
-
别名
-
功能
-
Shell 内置
-
关键词
-
文件
这个列表也代表了它们被搜索的顺序。正如我们所看到的,直到最后才搜索可执行文件ls
。
以下命令演示了简单使用type
:
$ type ls
ls is aliased to `ls --color=auto'
我们可以进一步扩展这一点,以显示给定命令的所有匹配项:
$ type -a ls
ls is aliased to `ls --color=auto'
ls is /bin/ls
如果我们只需要输入输出,我们可以使用-t
选项。当我们需要从脚本内部测试命令类型并且只需要返回类型时,这是有用的。这将排除多余的信息;因此,使我们人类更容易阅读。考虑以下命令和输出:
$ type -t ls
alias
输出清晰简单,正是计算机或脚本所需的。
内置的type
也可以用于识别 shell 关键字,如 if、case、function 等。以下命令显示了type
被用于多个参数和类型:
$ type ls quote pwd do id
命令的输出显示在以下屏幕截图中:
当使用type
时,我们还会看到函数定义被打印出来。
命令 PATH
只有当提供程序的完整路径或相对路径时,Linux 才会在PATH
环境中检查可执行文件。一般来说,除非它在PATH
中,否则不会搜索当前目录。通过将目录添加到PATH
变量中,我们可以将当前目录包含在PATH
中。这在以下代码示例中显示:
$ export PATH=$PATH:.
这将当前目录附加到PATH
变量的值中,每个PATH
项都使用冒号分隔。现在,您的PATH
已更新以包括当前工作目录,并且每次更改目录时,脚本都可以轻松执行。一般来说,将脚本组织到结构化的目录层次结构中可能是一个好主意。考虑在您的主目录中创建一个名为bin
的子目录,并将脚本添加到该文件夹中。将$HOME/bin
添加到您的PATH
变量将使您能够通过名称找到脚本,而无需文件路径。
以下命令行列表只会在目录不存在时创建该目录:
$ test -d $HOME/bin || mkdir $HOME/bin
尽管上述命令行列表并不是严格必要的,但它确实显示了在 bash 中进行脚本编写不仅限于实际脚本,我们还可以直接在命令行中使用条件语句和其他语法。从我们的角度来看,我们知道前面的命令将在您是否有bin
目录的情况下工作。使用$HOME
变量确保命令将在不考虑当前文件系统上下文的情况下工作。
在本书中,我们将把脚本添加到$HOME/bin
目录中,以便无论我们的工作目录如何,都可以执行它们。
为脚本准备文本编辑器
在整本书中,我将在树莓派的命令行上工作,这将包括创建和编辑脚本。当然,您可以选择您希望编辑脚本的方式,并且可能更喜欢使用图形编辑器,我将在 gedit 中展示一些设置。我将进行一次到 Red Hat 系统的旅行,以展示本章中 gedit 的屏幕截图。
为了帮助使命令行编辑器更易于使用,我们可以启用选项,并且可以通过隐藏的配置文件持久化这些选项。gedit 和其他 GUI 编辑器及其菜单将提供类似的功能。
配置 vim
编辑命令行通常是必须的,也是我日常生活的一部分。在编辑器中设置使生活更轻松的常见选项,给我们提供了所需的可靠性和一致性,有点像脚本本身。我们将在 vi 或 vim 编辑器文件$HOME/.vimrc
中设置一些有用的选项。
我们设置的选项在以下列表中详细说明:
-
showmode:确保我们在插入模式下看到
-
nohlsearch:不会突出显示我们搜索的单词
-
autoindent:我们经常缩进我们的代码;这使我们可以返回到最后的缩进级别,而不是在每次换行时返回到新行的开头
-
tabstop=4:将制表符设置为四个空格
-
expandtab:将制表符转换为空格,在文件移动到其他系统时非常有用
-
syntax on:请注意,这不使用 set 命令,而是用于打开语法高亮
当这些选项设置时,$HOME/.vimrc
文件应该看起来类似于这样:
setshowmodenohlsearch
setautoindenttabstop=4
setexpandtab
syntax on
配置 nano
nano 文本编辑器的重要性正在增加,并且它是许多系统中的默认编辑器。就我个人而言,我不喜欢它的导航或缺乏导航功能。它可以像 vim 一样进行自定义。这次我们将编辑$HOME/.nanorc
文件。您编辑后的文件应该看起来像下面的样子:
setautoindent
settabsize 4
include /usr/share/nano/sh.nanorc
最后一行启用了 shell 脚本的语法高亮。
配置 gedit
图形编辑器,如 gedit,可以使用首选项菜单进行配置,非常简单直接。
启用制表符间距设置为4个空格,并将制表符扩展为空格,可以使用首选项 | 编辑器选项卡,如下截图所示:
提示
下载示例代码
您可以从您在www.packtpub.com
的帐户中下载示例代码文件,用于您购买的所有 Packt Publishing 图书。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support
并注册,文件将直接通过电子邮件发送给您。
另一个非常有用的功能可以在首选项 | 插件选项卡中找到。在这里,我们可以启用片段插件,用于插入代码示例。如下截图所示:
在本书的其余部分,我们将在命令行和 vim 中工作;请随意使用您最擅长的编辑器。我们现在已经奠定了创建良好脚本的基础,尽管在 bash 脚本中,空白、制表符和空格并不重要;但是一个布局良好、间距一致的文件易于阅读。当我们在本书的后面看 Python 时,您将意识到在某些语言中,空白对语言是重要的,因此最好尽早养成良好的习惯。
创建和执行脚本
有了我们准备好的编辑器,我们现在可以快速地创建和执行我们的脚本。如果您在阅读本书时具有一些先前的经验,我会警告您,我们将从基础知识开始,但我们也将包括查看位置参数;请随时按照自己的步调前进。
你好,世界!
如你所知,几乎是必须以hello world
脚本开始,就这一点而言,我们不会让你失望。我们将首先创建一个新的脚本$HOME/bin/hello1.sh
。文件的内容应该如下截图所示:
我希望你没有太多困难;毕竟只有三行。我鼓励您在阅读时运行示例,以帮助您真正通过实践来巩固信息。
-
#!/bin/bash
:通常,这总是脚本的第一行,并被称为 shebang。shebang 以注释开头,但系统仍然使用这一行。在 shell 脚本中,注释使用#
符号。shebang 指示系统执行脚本的解释器。我们在 shell 脚本中使用 bash,根据需要,我们可能会使用 PHP 或 Perl 来执行其他脚本。如果我们不添加这一行,那么命令将在当前 shell 中运行;如果我们运行另一个 shell,可能会出现问题。 -
echo "Hello World"
:echo
命令将在内置 shell 中被捕获,并可用于编写标准输出STDOUT
,默认为屏幕。要打印的信息用双引号括起来,稍后将会有更多关于引号的内容。 -
exit 0
:exit
命令是一个内置的 shell 命令,用于离开或退出脚本。exit
代码作为整数参数提供。除了0
之外的任何值都将指示脚本执行中的某种错误。
执行脚本
将脚本保存在我们的PATH
环境中,它仍然不能作为独立的脚本执行。我们需要根据需要为文件分配和执行权限。对于一个简单的测试,我们可以直接用 bash 运行文件。以下命令向您展示了如何做到这一点:
$ bash $HOME/bin/hello1.sh
我们应该得到Hello World
文本显示在我们的屏幕上。这不是一个长期的解决方案,因为我们需要将脚本放在$HOME/bin
目录中,具体来说,以便在任何位置轻松运行脚本而不必输入完整路径。我们需要添加执行权限,如下面的代码所示:
$ chmod +x $HOME/bin/hello1.sh
现在我们应该能够简单地运行脚本,如下面的截图所示:
检查退出状态
这个脚本很简单,但我们仍然需要知道如何使用脚本和其他应用程序的退出代码。我们在创建$HOME/bin
目录时生成的命令行列表,就是如何使用退出代码的一个很好的例子:
$ command1 || command 2
在前面的例子中,只有在command1
以某种方式失败时才会执行command2
。具体来说,只有当command1
以除0
以外的状态代码退出时,command2
才会运行。
同样,在以下摘录中:
$ command1 && command2
只有在command1
成功并发出0
的退出代码时,我们才会执行command2
。
要明确从我们的脚本中读取退出代码,我们可以查看$?
变量,如下面的例子所示:
$ hello1.sh
$ echo $?
预期的输出是0
,因为这是我们添加到文件最后一行的内容,几乎没有其他任何可能出错导致我们无法达到那一行。
确保唯一的名称
现在我们可以创建和执行一个简单的脚本,但是我们需要考虑一下名字。在这种情况下,hello1.sh
就足够好,不太可能与系统上的其他任何东西冲突。我们应该避免使用可能与现有别名、函数、关键字和构建命令冲突的名称,以及避免使用已经在使用中的程序的名称。
向文件添加sh
后缀并不能保证名称是唯一的,但在 Linux 中,我们不使用文件扩展名,后缀是文件名的一部分。这有助于为您的脚本提供一个唯一的标识。此外,后缀被编辑器用来帮助您识别文件以进行语法高亮。如果您还记得,我们特意向 nano 文本编辑器添加了语法高亮文件sh.nanorc
。每个文件都是特定于后缀和后续语言的。
回顾本章中的命令层次结构,我们可以使用类型来确定文件hello.sh
的位置和类型:
$ type hello1.sh #To determine the type and path
$ type -a hello1.sh #To print all commands found if the name is NOT unique
$ type -t hello1.sh ~To print the simple type of the command
这些命令和输出可以在以下截图中看到:
你好,多莉!
可能我们在脚本中需要更多的内容而不仅仅是一个简单的固定消息。静态消息内容确实有其存在的价值,但我们可以通过增加一些灵活性使这个脚本更加有用。
在本章中,我们将看一下我们可以向脚本提供的位置参数或参数,下一章我们将看到如何使脚本交互,并在运行时提示用户输入。
带参数运行脚本
我们可以带参数运行脚本,毕竟这是一个自由的世界,Linux 鼓励您自由地使用代码做您想做的事情。但是,如果脚本不使用这些参数,它们将被默默地忽略。以下代码显示了带有单个参数运行脚本:
$ hello1.shfred
脚本仍然会运行,不会产生错误。输出也不会改变,仍然会打印 hello world:
参数标识符 | 描述 |
---|---|
$0 |
脚本本身的名称,通常在使用说明中使用。 |
$1 |
位置参数,传递给脚本的第一个参数。 |
${10} |
需要两个或更多位数来表示参数位置。大括号用于将变量名称与任何其他内容分隔开。预期是单个数字。 |
$# |
当我们需要设置正确脚本执行所需的参数数量时,参数计数特别有用。 |
$* |
指代所有参数。 |
为了使脚本使用参数,我们可以稍微更改脚本内容。让我们首先复制脚本,添加执行权限,然后编辑新的hello2.sh
:
$ cp $HOME/bin/hello1.sh $HOME/bin/hello2.sh
$ chmod +x $HOME/bin/hello2.sh
我们需要编辑hello2.sh
文件,以便在命令行传递参数时使用参数。以下屏幕截图显示了允许我们现在拥有自定义消息的命令行参数的最简单用法。
现在运行脚本,我们可以按以下方式提供参数:
$ hello2.sh fred
现在输出应该是Hello fred。如果我们不提供参数,那么变量将为空,只会打印Hello。您可以参考以下屏幕截图查看执行参数和输出:
如果我们调整脚本以使用$*
,则会打印所有参数。我们将看到Hello,然后是所有提供的参数列表。如果我们编辑脚本并将echo
行替换为以下内容:
echo "Hello $*"
使用以下参数执行脚本:
$ hello2.shfredwilma betty barney
将导致以下屏幕截图中显示的输出:
如果我们想要打印Hello <name>
,每个都在单独的行上,我们需要等一会儿,直到我们涵盖循环结构。for 循环将很好地实现这一点。
正确引用的重要性
到目前为止,我们已经使用了简单的双引号机制来包裹我们想要在 echo 中使用的字符串。
在第一个脚本中,无论我们使用单引号还是双引号都无所谓。echo "Hello World"
将与echo 'Hello World'
完全相同。
然而,在第二个脚本中情况就不同了,因此了解 bash 中可用的引用机制非常重要。
正如我们所见,使用双引号echo "Hello $1"
将导致Hello fred或提供的值。而如果我们使用单引号echo 'Hello $1'
,则屏幕上打印的输出将是Hello $1,我们看到变量名称而不是其值。
引号的作用是保护特殊字符,例如两个单词之间的空格;两个引号都保护空格不被解释。空格通常被 shell 读取为默认字段,由 shell 分隔。换句话说,所有字符都被 shell 读取为没有特殊含义的文字。这会导致$
符号打印其文字格式,而不是允许 bash 扩展其值。由于被单引号保护,bash shell 无法扩展变量的值。
这就是双引号拯救我们的地方。双引号将保护除$
之外的所有字符,允许 bash 扩展存储的值。
如果我们需要在带引号的字符串中使用文字$
以及需要扩展的变量;我们可以使用双引号,但用反斜杠(\
)转义所需的$
。例如,echo "$USER earns \$4"
将打印为Fred earns $4,如果当前用户是 Fred 的话。
尝试在命令行中使用所有引用机制尝试以下示例。随时根据需要提高您的小时费率:
$ echo "$USER earns $4"
$ echo '$USER earns $4'
$ echo "$USER earns \$4"
以下屏幕截图显示了输出:
打印脚本名称
$0
变量代表脚本名称,通常在使用说明中使用。由于我们还没有看条件语句,所以脚本名称将打印在显示的名称上方。
编辑你的脚本,使其读取 $HOME/bin/hello2.sh
的以下完整代码块:
#!/bin/bash
echo "You are using $0"
echo "Hello $*"
exit 0
命令的输出如下截图所示:
如果我们不想打印路径,只想显示脚本的名称,我们可以使用 basename
命令,该命令从路径中提取名称。调整脚本,使第二行现在读取如下:
echo "You are using $(basename $0)"
$(….)
语法用于评估内部命令的输出。我们首先运行 basename $0
并将结果输入到一个未命名的变量中,用 $
表示。
新的输出将如下截图所示:
使用反引号也可以实现相同的结果,这样不太容易阅读,但我们提到这一点是因为你可能需要理解和修改其他人编写的脚本。$(….)
语法的替代方法如下例所示:
echo "You are using 'basename $0'"
请注意,使用的字符是反引号,而不是单引号。在英国和美国键盘上,这些字符位于数字 1 键旁边的左上部分。
调试你的脚本
到目前为止,我们看到的脚本非常简单,几乎不会出错或需要调试。随着脚本的增长和包含条件语句的决策路径,我们可能需要使用一定级别的调试来更好地分析脚本的进展。
Bash 为我们提供了两个选项,-v
和 -x
。
如果我们想查看脚本的详细输出以及脚本逐行评估的详细信息,我们可以使用 -v
选项。这可以在 shebang 中使用,但直接使用 bash 运行脚本通常更容易:
$ bash -v $HOME/bin/hello2.sh fred
在这个例子中,这是特别有用的,因为我们可以看到嵌入式 basename
命令的每个元素是如何处理的。第一步是删除引号,然后是括号。看一下以下输出:
更常用的是 -x
选项,它显示命令的执行过程。了解脚本选择的决策分支是很有用的。以下是使用情况:
$ bash -x $HOME/bin/hello2.sh fred
我们再次看到首先评估了 basename
,但我们没有看到运行该命令所涉及的更详细的步骤。接下来的截图捕获了命令和输出:
总结
这标志着本章的结束,我相信你可能会发现这很有用。特别是对于那些刚开始使用 bash 脚本的人来说,本章一定已经为你打下了坚实的基础,你可以在此基础上建立你的知识。
我们首先确保 bash 是安全的,不容易受到嵌入式函数 shell-shock 的影响。有了安全的 bash,我们考虑了别名、函数等在命令之前检查的执行层次结构;了解这一点可以帮助我们规划一个良好的命名结构和定位脚本的路径。
很快,我们就开始编写简单的脚本,其中包含静态内容,但我们看到了使用参数添加灵活性有多么容易。脚本的退出代码可以使用 $?
变量读取,我们可以使用 ||
和 &&
创建命令行列表,这取决于列表中前一个命令的成功或失败。
最后,我们通过查看脚本的调试来结束这一章。当脚本很简单时,实际上并不需要,但在以后增加复杂性时会很有用。
在下一章中,我们将创建交互式脚本,这些脚本在脚本执行期间读取用户的输入。
第二章:创建交互式脚本
在第一章的使用 Bash 脚本的什么和为什么中,我们学习了如何创建脚本以及使用一些基本元素。这些包括我们在执行脚本时可以传递的可选参数。在本章中,我们将通过使用 read shell 内置命令来扩展这一点,以允许交互式脚本。交互式脚本是在脚本执行期间提示信息的脚本。在这样做的过程中,我们将涵盖以下主题:
-
使用带有选项的
echo
-
使用
read
的基本脚本 -
添加注释
-
使用提示增强
read
脚本 -
限制输入字符的数量
-
控制输入文本的可见性
-
简单的脚本来强化我们的学习
使用带有选项的 echo
到目前为止,在本书中,我们已经看到echo
命令非常有用,并且将在我们的许多脚本中使用,如果不是全部。我们还看到这既是一个内置命令,也是一个命令文件。运行echo
命令时,将使用内置命令,除非我们指定文件的完整路径。我们可以使用以下命令进行测试:
$ test -a echo
要获得内置命令的帮助,我们可以使用man bash
并搜索echo
;但是,echo
命令与内部命令相同,因此我建议您在大多数情况下使用man echo
来显示命令选项。
到目前为止,我们已经看到的echo
的基本用法将产生文本输出和一个新行。这通常是期望的响应,所以我们不必担心下一个提示会附加到输出的末尾。新行将脚本输出与下一个 shell 提示分隔开。如果我们不提供任何文本字符串来打印,echo
将只打印新行到STDOUT
。我们可以直接从命令行使用以下命令进行测试。我们不需要从脚本运行echo
或者实际上运行任何其他命令。从命令行运行echo
将简单地输入如下命令:
$ echo
输出将显示我们发出的命令和随后的提示之间的清晰新行。我们可以在下面的截图中看到这一点:
如果我们想要抑制新行,特别是在提示用户时非常有用,我们可以通过以下两种方式使用echo
来实现:
$ echo -n "Which directory do you want to use? "
$ echo -e "Which directory do you want to use? \c"
结果将是抑制换行。在初始示例中,使用-n
选项来抑制换行。第二个示例使用更通用的-e
选项,允许在文本字符串中添加转义序列。为了在同一行上继续,我们使用\c
作为转义序列。
这看起来不太好,作为脚本的最后部分或者从命令行运行时,命令提示符将会跟随。如下截图所示:
使用 read 的基本脚本
当作为提示用户输入的脚本的一部分使用时,抑制换行正是我们想要的。我们将首先将现有的hello2.sh
脚本复制到hello3.sh
,并构建一个交互式脚本。最初,我们将使用echo
作为提示机制,但随着我们逐渐增强脚本,我们将直接从 shell 内置的read
命令生成提示:
$ cp $HOME/bin/hello2.sh $HOME/bin/hello3.sh
$ chmod +x $HOME/bin/hello3.sh
编辑$HOME/bin/hello3.sh
脚本,使其读取如下内容:
#!/bin/bash
echo -n "Hello I $(basename $0) may I ask your name: "
read
echo "Hello $REPLY"
exit 0
当执行脚本时,我们将被问候并提示输入我们自己的名字。这是使用echo
语句中的$REPLY
变量回显出来的。由于我们尚未向read
内置命令提供变量名,因此使用了默认的$REPLY
变量。脚本执行和输出如下截图所示。花些时间在您自己的系统上练习脚本:
这一小步已经让我们走了很长的路,而且像这样的脚本有很多用途,我们都使用过提示选项和目录的安装脚本。我承认这仍然相当琐碎,但随着我们深入本章,我们将更接近一些更有用的脚本。
脚本注释
我们应该在脚本的早期引入注释。脚本注释以#
符号开头。#
符号之后的任何内容都是注释,不会被脚本评估。shebang,#!/bin/bash
,主要是一个注释,因此不会被脚本评估。运行脚本的 shell 读取 shebang,因此知道要将脚本交给哪个命令解释器。注释可以位于行的开头或部分位置。Shell 脚本没有多行注释的概念。
如果您还不熟悉注释,那么它们被添加到脚本中,告诉所有关于谁编写了脚本,脚本是何时编写和最后更新的,以及脚本的功能。这是脚本的元数据。
以下是脚本中注释的示例:
#!/bin/bash
# Welcome script to display a message to users on login
# Author: @theurbanpenguin
# Date: 1/1/1971
注释和添加解释代码正在做什么以及为什么是一个很好的做法。这将帮助您和需要在以后编辑脚本的同事。
使用 read 提示增强脚本
我们已经看到了如何使用内置的 read 来填充一个变量。到目前为止,我们已经使用echo
来生成提示,但是这可以通过-p
选项传递给 read 本身。read
命令将忽略额外的换行符,因此在一定程度上减少了行数和复杂性。
我们可以在命令行本身测试这个。尝试输入以下命令以查看read
的运行情况:
$ read -p "Enter your name: " name
我们使用read
命令和-p
选项。跟在选项后面的参数是出现在提示中的文本。通常,我们会确保文本末尾有一个空格,以确保我们可以清楚地看到我们输入的内容。这里提供的最后一个参数是我们想要填充的变量,我们简单地称之为name
。变量也是区分大小写的。即使我们没有提供最后一个参数,我们仍然可以存储用户的响应,但这次是在REPLY
变量中。
提示
请注意,当我们返回变量的值时,我们使用$
,但在写入变量时不使用。简单来说,当读取变量时,我们引用$VAR
,当设置变量时,我们引用VAR=value
。
以下插图显示了使用-p
选项的read
命令的语法:
我们可以编辑脚本,使其看起来类似于hello3.sh
中的以下片段:
#!/bin/bash
read -p "May I ask your name: " name
echo "Hello $name"
exit 0
read
提示不能评估消息字符串中的命令,就像我们以前使用的那样。
限制输入字符的数量
到目前为止,我们使用的脚本不需要功能,但是我们可能需要要求用户按任意键继续。目前,我们已经设置了这样的方式,即在按下Enter键之前变量不会被填充。用户必须按Enter键继续。如果我们使用-n
选项后跟一个整数,我们可以指定在继续之前要接受的字符,这里我们将设置为1
。看一下以下代码片段:
#!/bin/bash
read -p "May I ask your name: " name
echo "Hello $name"
read -n1 -p "Press any key to exit"
echo
exit 0
现在,脚本将在显示名称后暂停,直到我们按下任意键;实际上,我们可以在继续之前按下任意键,因为我们只接受1
个按键。而在之前,我们需要保留默认行为,因为我们无法知道输入的名称有多长。我们必须等待用户按Enter键。
提示
请注意,我们在这里添加了额外的 echo 以确保脚本结束前发出一个新行。这确保了 shell 提示从新行开始。
控制输入文本的可见性
尽管我们将输入限制为单个字符,但我们确实可以在屏幕上看到文本。同样,如果我们输入名称,我们会在按下Enter之前看到输入的文本。在这种情况下,这只是不整洁,但如果我们输入敏感数据,比如 PIN 码或密码,我们应该隐藏文本。我们可以使用静默选项或-s
来实现这一点。在脚本中进行简单编辑即可实现这一点:
#!/bin/bash
read -p "May I ask your name: " name
echo "Hello $name"
read -sn1 -p "Press any key to exit"
echo
exit 0
现在,当我们使用键继续时,它不会显示在屏幕上。我们可以在下面的截图中看到脚本的行为:
通过简单脚本增强学习
我们的脚本仍然有点琐碎,我们还没有看条件语句,所以我们可以测试正确的输入,但让我们看一些简单的脚本,我们可以用一些功能来构建。
使用脚本进行备份
现在我们已经创建了一些脚本,我们可能希望将它们备份到不同的位置。如果我们创建一个提示我们的脚本,我们可以选择要备份的位置和文件类型。
考虑以下脚本作为您的第一个练习。创建脚本并将其命名为$HOME/backup.sh
:
#!/bin/bash
# Author: @theurbanpenguin
# Web: www.theurbapenguin.com
# Script to prompt to back up files and location
# The files will be search on from the user's home
# directory and can only be backed up to a directory
# within $HOME
# Last Edited: July 4 2015
read -p "Which file types do you want to backup " file_suffix
read -p "Which directory do you want to backup to " dir_name
# The next lines creates the directory if it does not exist
test -d $HOME/$dir_name || mkdir -m 700 $HOME/$dir_name
# The find command will copy files the match the
# search criteria ie .sh . The -path, -prune and -o
# options are to exclude the backdirectory from the
# backup.
find $HOME -path $HOME/$dir_name -prune -o \
-name "*$file_suffix" -exec cp {} $HOME/$dir_name/ \;
exit 0
您会看到文件被注释了;尽管黑白的可读性有点困难。如果您有这本书的电子副本,您应该在下面的截图中看到颜色:
当脚本运行时,您可以选择.sh
文件进行备份,并将backup
作为目录。脚本执行如下截图所示,以及目录的列表:
现在您可以看到,如果我们可以开始创建有意义的脚本,尽管我强烈建议添加错误检查用户输入,如果这个脚本不是用于个人使用。随着我们进入书籍,我们将涵盖这一点。
连接到服务器
让我们看一些实用的脚本,我们可以用来连接服务器。首先,我们将查看 ping,然后在第二个脚本中,我们将查看提示 SSH 凭据。
版本 1 - ping
这是我们所有人都可以做到的,不需要特殊的服务。这将简化控制台用户可能不了解命令细节的ping
命令。这将对服务器进行三次 ping 而不是正常的无限次数。如果服务器存活,则没有输出,但如果服务器失败,则报告服务器死机
。将脚本创建为$HOME/bin/ping_server.sh
:
#!/bin/bash
# Author: @theurbanpenguin
# Web: www.theurbapenguin.com
# Script to ping a server
# Last Edited: July 4 2015
read -p "Which server should be pinged " server_addr
ping -c3 $server_addr 2>&1 > /dev/null || echo "Server dead"
以下截图显示了成功和失败的输出:
版本 2 - SSH
通常在服务器上安装并运行 SSH,因此如果您的系统正在运行 SSH 或者您可以访问 SSH 服务器,您可能可以运行此脚本。在此脚本中,我们提示服务器地址和用户名,并将它们传递给 SSH 客户端。将脚本创建为$HOME/bin/connect_server.sh
:
#!/bin/bash
# Author: @theurbanpenguin
# Web: www.theurbapenguin.com
# Script to prompt fossh connection
# Last Edited: July 4 2015
read -p "Which server do you want to connect to: " server_name
read -p "Which username do you want to use: " user_name
ssh ${user_name}@$server_name
提示
请注意脚本最后一行中使用大括号来将变量与@
符号分隔。
版本 3 - MySQL/MariaDB
在下一个脚本中,我们将提供数据库连接的详细信息以及要执行的 SQL 查询。如果您的系统上有 MariaDB 或 MySQL 数据库服务器,或者您可以连接到一个,您将能够运行此脚本。为演示,我将使用运行 Ubuntu-Mate 15.04 和 MariaDB 版本 10 的 Raspberry Pi;然而,这对于任何 MySQL 服务器或从版本 5 开始的 MariaDB 都应该适用。脚本收集用户和密码信息以及要执行的 SQL 命令。将脚本创建为$HOME/bin/run_mql.sh
:
#!/bin/bash
# Author: @theurbanpenguin
# Web: www.theurbapenguin.com
# Script to prompt for MYSQL user password and command
# Last Edited: July 4 2015
read -p "MySQL User: " user_name
read -sp "MySQL Password: " mysql_pwd
echo
read -p "MySQL Command: " mysql_cmd
read -p "MySQL Database: " mysql_db
mysql -u $user_name -p$mysql_pwd$mysql_db -e"$mysql_cmd"
在脚本中,我们可以看到当我们将 MySQL 密码输入到read
命令中时,我们使用-s
选项来抑制密码的显示。同样,我们直接使用echo
来确保下一个提示从新的一行开始。
脚本输入如下截图所示:
现在,我们可以轻松地看到密码抑制的工作原理,以及向 MySQL 命令添加的便利性。
总结
为自己的 shell 脚本拥有“我会读”的徽章感到自豪。我们已经开发了交互式脚本,并在脚本执行过程中提示用户输入。这些提示可以用来简化用户在命令行上的操作。这样,他们就不需要记住命令行选项,也不会在命令行历史中存储密码。在使用密码时,我们可以使用read -sp
选项来静默存储值。
在下一章中,我们将花时间来研究 bash 中的条件语句。
第三章:附加条件
我想我们现在可以说我们已经进入了脚本的细节部分。这些是使用条件编写到我们的脚本中的细节,用于测试语句是否应该运行。我们现在准备在脚本中添加一些智能,使我们的脚本变得更健壮,更易于使用和更可靠。条件语句可以用简单的命令行列表AND
或OR
命令一起编写,或者更常见的是在传统的if
语句中。
在本章中,我们将涵盖以下主题:
-
使用命令行列表进行简单决策路径
-
使用列表验证用户输入
-
使用测试 shell 内置
-
使用
if
创建条件语句 -
使用
else
扩展if
-
使用
elif
添加更多条件 -
使用
elif
创建backup.sh
脚本 -
使用 case 语句
-
脚本-使用
grep
的前端
使用命令行列表进行简单决策路径
我们在本书的第一章和第二章中的一些脚本中都使用了命令行列表。列表是我们可以创建的最简单的条件语句之一,因此我们认为在完全解释它们之前,在早期的示例中使用它们是合适的。
命令行列表是使用AND
或OR
符号连接的两个或多个语句:
-
&&
:AND
-
||
:OR
两个语句使用AND
符号连接时,只有在第一个命令成功运行时,第二个命令才会运行。而使用OR
符号连接时,只有在第一个命令失败时,第二个命令才会运行。
命令的成功或失败取决于从应用程序读取的退出代码。零表示应用程序成功完成,而非零表示失败。我们可以通过读取系统变量$?
来测试应用程序的成功或失败。下面是一个示例:
$ echo $?
如果我们需要确保脚本是从用户的主目录运行的,我们可以将这个构建到脚本的逻辑中。这可以从命令行测试,不一定要在脚本中。考虑以下命令行示例:
$ test $PWD == $HOME || cd $HOME
双竖线表示OR
列表。这确保了只有在第一个语句不成立时才执行第二个语句。简单来说,如果我们当前不在主目录中,那么在命令行列表结束时我们会在主目录中。我们很快会在测试命令中看到更多内容。
我们可以将这个应用到几乎任何我们想要的命令,而不仅仅是测试。例如,我们可以查询用户是否已登录到系统,如果是,我们可以使用write
命令直接向他们的控制台发送消息。与之前类似,我们可以在脚本之前在命令行中测试这个。下面是一个命令行示例:
$ who | grep pi > /dev/null 2>&1 && write pi < message.txt
如果我们在脚本中使用这个,几乎可以肯定我们会用变量替换用户名。一般来说,如果我们需要多次引用相同的值,那么使用变量是个好主意。在这种情况下,我们正在搜索pi
用户。
当我们分解命令行列表时,我们首先使用who
命令列出已登录的用户。我们将列表传输到grep
以搜索所需的用户名。我们对搜索的输出不感兴趣,只关心成功或失败。考虑到这一点,我们将所有输出重定向到/dev/null
。双和符号表示只有在第一个语句返回 true 时,列表中的第二个语句才运行。如果pi
用户已登录,我们使用write
向用户发送消息。以下截图说明了这个命令和输出。
使用列表验证用户输入
在这个脚本中,我们将确保第一个位置参数已经被提供了一个值。我们可以修改我们在第一章中创建的hello2.sh
脚本,使用 Bash 进行脚本编写的什么和为什么,在显示hello
文本之前检查用户输入。
您可以将hello2.sh
脚本复制到hello4.sh
,或者从头开始创建一个新的脚本。输入的内容不会很多,脚本将被创建为$HOME/bin/hello4.sh
,如下所示:
我们可以使用以下命令确保脚本是可执行的:
$ chmod +x $HOME/bin/hello4.sh
然后我们可以带参数或不带参数运行脚本。test
语句正在寻找$1
变量是否为零字节。如果是,那么我们将看不到hello
语句;否则它将打印Hello消息。简单来说,如果我们提供一个名字,我们将看到hello
消息。
以下屏幕截图显示了当您没有向脚本提供参数时会看到的输出,然后是提供的参数:
使用测试 shell 内置
现在可能是时候我们停下来,看一看这个test
命令。这既是一个 shell 内置命令,也是一个独立的可执行文件。当然,除非我们指定文件的完整路径,否则我们将首先使用内置命令。
当运行测试命令而没有任何表达式要评估时,测试将返回 false。因此,如果我们运行如下命令所示的测试:
$ test
退出状态将是1
,即使没有显示错误输出。test
命令将始终返回True
或False
或0
或1
。test
的基本语法是:
test EXPRESSION
或者,我们可以使用以下命令来反转test
命令:
test ! EXPRESSION
如果我们需要包含多个表达式,这些表达式可以使用-a
和-o
选项分别进行AND
或OR
连接:
test EXPRESSION -a EXPRESSION
test EXPRESSION -o EXPRESSION
我们还可以以简写版本编写,用方括号替换测试以包围表达式,如下例所示:
[ EXPRESION ]
测试字符串
我们可以测试两个字符串的相等或不相等。例如,测试 root 用户的一种方法是使用以下命令:
test $USER = root
我们也可以使用方括号表示法来编写这个:
[ $USER = root ]
同样,我们可以使用以下两种方法测试非 root 帐户:
test ! $USER = root
[ ! $USER = root ]
我们还可以测试字符串的零值和非零值。我们在本章的早些时候的一个示例中看到了这一点。
要测试字符串是否有值,我们可以使用-n
选项。我们可以通过检查用户环境中变量的存在来检查当前连接是否是通过 SSH 进行的。我们在以下两个示例中使用test
和方括号来展示这一点:
test -n $SSH_TTY
[ -n $SSH_TTY ]
如果这是真的,那么连接是通过 SSH 建立的;如果是假的,那么连接不是通过 SSH。
正如我们之前看到的,当决定一个变量是否设置时,测试零字符串值是有用的:
test -z $1
或者,更简单地,我们可以使用:
[ -z $1 ]
对于这个查询的真实结果意味着没有输入参数被提供给脚本。
测试整数
此外,bash 脚本的测试字符串值可以测试整数值和整数。测试脚本的另一种方法是计算位置参数的数量,并测试该数字是否大于0
:
test $# -gt 0
或者使用括号,如下所示:
[ $# -gt 0 ]
在关系中,顶部位置参数变量$#
表示传递给脚本的参数数量。要测试整数值的相等性,使用-eq
选项,而不是=
符号。
测试文件类型
在测试值时,我们可以测试文件的存在或文件类型。例如,我们可能只想在文件是符号链接时才删除文件。我在编译内核时使用这个功能。/usr/src/linux
目录应该是最新内核源代码的符号链接。如果我在编译新内核之前下载了更新版本,我需要删除现有的链接并创建新的链接。以防万一有人创建了/usr/src/linux
目录,我们可以在删除之前测试它是否是一个链接:
# [ -h /usr/src/linux ] &&rm /usr/src/linux
-h
选项测试文件是否有链接。其他选项包括:
-
-d
:这显示它是一个目录 -
-e
:这显示文件以任何形式存在 -
-x
:这显示文件是可执行的 -
-f
:这显示文件是一个普通文件 -
-r
:这显示文件是可读的 -
-p
:这显示文件是命名管道 -
-b
:这显示文件是块设备 -
-c
:这显示文件是字符设备
还有更多选项存在,因此根据需要深入主页。我们将在整本书中使用不同的选项;因此,为您提供实用和有用的示例。
使用 if 创建条件语句
正如我们迄今所见,可以使用命令行列表构建简单的条件。这些条件可以使用测试和不使用测试来编写。随着任务复杂性的增加,使用if
创建语句将更容易。这肯定会提高脚本的可读性和逻辑布局。在某种程度上,它也符合我们的思维和语言表达方式,if
在我们的口语中和 bash 脚本中都是语义的一部分。
即使在脚本中占用多行,使用if
语句也可以实现更多功能并使脚本更易读。说了这些,让我们来看看如何创建if
条件。以下是使用if
语句的脚本示例:
#!/bin/bash
# Welcome script to display a message to users
# Author: @theurbanpenguin
# Date: 1/1/1971
if [ $# -lt 1 ] ; then
echo "Usage: $0 <name>"
exit 1
fi
echo "Hello $1"
exit 0
if
语句内的代码仅在条件评估为真时运行,if
块的结尾用fi
表示-if
反过来。在vim
中的颜色编码可以帮助提高可读性,您可以在以下截图中看到:
在脚本中,我们可以轻松添加多个语句以在条件为真时运行。在我们的情况下,这包括使用错误指示退出脚本,以及使用usage
语句来帮助用户。这确保我们只在提供要欢迎的名称时才显示Hello消息。
我们可以在以下截图中查看带有参数和不带参数的脚本执行:
为了帮助我们理解if
条件语句的布局,以下插图演示了使用伪代码的语法:
缩进代码并非必需,但有助于可读性,强烈建议这样做。将then
语句添加到与if
相同的行上,同样有助于代码的可读性,并且分号是必需的,用于将if
与then
分隔开来。
使用 else 扩展 if
当脚本需要继续执行而不管if
条件的结果时,通常需要处理评估的两种条件。当条件为真时该怎么办,以及当条件评估为假时该怎么办。这就是我们可以使用else
关键字的地方。这允许在条件为真时执行一块代码,在条件为假时执行另一块代码。下图显示了这种情况的伪代码:
如果我们考虑扩展之前创建的hello5.sh
脚本,可以轻松地实现无论参数是否存在都能正确执行。我们可以将其重新创建为hello6.sh
,如下所示:
#!/bin/bash
# Welcome script to display a message to users
# Author: @theurbanpenguin
# Date: 1/1/1971
if [ $# -lt 1 ] ; then
read -p "Enter a name: "
name=$REPLY
else
name=$1
fi
echo "Hello $name"
exit 0
脚本现在设置了一个命名变量,这有助于可读性,我们可以从输入参数或read
提示中为$name
分配正确的值,无论哪种方式,脚本都能正常工作并开始成形。
更多的 elif 条件
当我们需要更高程度的控制时,我们可以使用elif
关键字。与else
不同,elif
需要为每个elif
测试额外的条件。通过这种方式,我们可以应对不同的情况。我们可以添加尽可能多的elif
条件。以下是伪代码示例:
if condition; then
statement
elif condition; then
statement
else
statement
fi
exit 0
脚本可以通过提供更简化的选择来为操作员提供更复杂的代码。尽管脚本逐渐变得更加复杂以满足要求,但对于操作员来说,执行变得大大简化了。我们的工作是使用户能够轻松地从命令行运行更复杂的操作。通常,这将需要向我们的脚本添加更多的复杂性;然而,我们将获得脚本化应用的可靠性。
使用 elif 创建 backup2.sh
我们可以重新查看我们创建的用于运行之前备份的脚本。这个脚本$HOME/bin/backup.sh
提示用户选择文件类型和存储备份的目录。备份使用的工具是find
和cp
。
有了这些新的知识,我们现在可以允许脚本使用tar
命令和操作员选择的压缩级别运行备份。无需选择文件类型,因为完整的主目录将被备份,不包括备份目录本身。
操作员可以根据三个字母H
、M
和L
选择压缩。选择将影响传递给tar
命令的选项和创建的备份文件。选择高将使用bzip2
压缩,中使用gzip
压缩,低创建一个未压缩的tar
存档。这个逻辑存在于后续的扩展if
语句中:
if [ $file_compression = "L" ] ; then
tar_opt=$tar_l
elif [ $file_compression = "M" ]; then
tar_opt=$tar_m
else
tar_opt=$tar_h
fi
根据用户的选择,我们可以为tar
命令配置正确的选项。由于我们有三个条件需要评估,因此适合使用if
、elif
和else
语句。要查看变量是如何配置的,我们可以查看脚本中的以下摘录:
tar_l="-cvf $backup_dir/b.tar --exclude $backup_dir $HOME"
tar_m="-czvf $backup_dir/b.tar.gz --exclude $backup_dir $HOME"
tar_h="-cjvf $backup_dir/b.tar.bzip2 --exclude $backup_dir $HOME"
完整的脚本可以创建为$HOME/bin/backup2.sh
,应该读取如下代码:
#!/bin/bash
# Author: @theurbanpenguin
# Web: www.theurbapenguin.com
read -p "Choose H, M or L compression " file_compression
read -p "Which directory do you want to backup to " dir_name
# The next lines creates the directory if it does not exist
test -d $HOME/$dir_name || mkdir -m 700 $HOME/$dir_name
backup_dir=$HOME/$dir_name
tar_l="-cvf $backup_dir/b.tar --exclude $backup_dir $HOME"
tar_m="-czvf $backup_dir/b.tar.gz --exclude $backup_dir $HOME"
tar_h="-cjvf $backup_dir/b.tar.bzip2 --exclude $backup_dir $HOME"
if [ $file_compression = "L" ] ; then
tar_opt=$tar_l
elif [ $file_compression = "M" ]; then
tar_opt=$tar_m
else
tar_opt=$tar_h
fi
tar $tar_opt
exit 0
当我们执行脚本时,需要以大写字母选择H
、M
或L
,因为这是脚本内部进行选择的方式。以下截图显示了初始脚本执行,选择了M
:
使用 case 语句
与使用多个elif
语句不同,当对单个表达式进行评估时,case
语句可能提供了更简单的机制。
使用伪代码列出了case
语句的基本布局:
case expression in
case1)
statement1
statement2
;;
case2)
statement1
statement2
;;
*)
statement1
;;
esac
我们看到的语句布局与其他语言中存在的switch
语句并没有太大不同。在 bash 中,我们可以使用case
语句测试简单的值,比如字符串或整数。Case 语句可以适用于各种字母,比如[a-f]
或a
到f
,但它们不能轻松处理整数范围,比如[1-20]
。
case
语句首先会展开表达式,然后尝试依次与每个项目进行匹配。当找到匹配时,所有语句都会执行直到;;
。这表示该匹配的代码结束。如果没有匹配,将匹配*
表示的else
语句。这需要是列表中的最后一项。
考虑以下脚本grade.sh
,用于评估成绩:
#!/bin/bash
# Script to evaluate grades
# Usage: grade.sh student grade
# Author: @theurbanpenguin
# Date: 1/1/1971
if [ ! $# -eq2 ] ; then
echo "You must provide <student><grade>
exit 2
fi
case $2 in
[A-C]|[a-c]) echo "$1 is a star pupil"
;;
[Dd]) echo "$1 needs to try a little harder!"
;;
[E-F]|[e-f]) echo "$1 could do a lot better next year"
;;
*) echo "Grade could not be evaluated for $1"
esac
脚本首先使用if
语句检查脚本是否提供了确切的两个参数。如果没有提供,脚本将以错误状态退出:
if [ ! $# -eq2 ] ; then
echo "You must provide <student><grade>
exit 2
fi
然后case
语句扩展表达式,这是在这个例子中的$2
变量的值。这代表我们提供的等级。然后我们尝试首先匹配大写和小写的字母A
到C
。[A-C]
用于匹配A
或B
或C
。竖线然后添加了一个额外的OR
来与a
、b
或c
进行比较:
[A-C]|[a-c]) echo "$1 is a star pupil"
;;
我们对其他提供的等级A
到F
进行了类似的测试。
以下屏幕截图显示了不同等级的脚本执行:
脚本-使用 grep 构建前端
作为本章的结束,我们可以将我们学到的一些功能组合在一起,构建一个脚本,提示操作员输入文件名、搜索字符串和要使用grep
命令执行的操作。我们可以将脚本创建为$HOME/bin/search.sh
,不要忘记将其设置为可执行文件:
#!/bin/bash
#Author: @theurbanpenguin
usage="Usage: search.sh file string operation"
if [ ! $# -eq3 ] ; then
echo "$usage"
exit 2
fi
[ ! -f $1 ]&& exit 3
case $3 in
[cC])
mesg="Counting the matches in $1 of $2"
opt="-c"
;;
[pP])
mesg="Print the matches of $2 in $1"
opt=""
;;
[dD])
mesg="Printing all lines but those matching $3 from $1"
opt="-v"
;;
*) echo "Could not evaluate $1 $2 $3";;
esac
echo $mesg
grep $opt $2 $1
我们首先通过以下代码检查是否有三个输入参数:
if [ ! $# -eq3 ] ; then
echo "$usage"
exit 2
fi
下一个检查使用命令行列表来退出脚本,如果文件参数不是常规文件,则使用test -f
:
[ ! -f $1 ]&& exit 3
case
语句允许三种操作:
-
计算匹配的行数
-
打印匹配的行
-
打印除匹配行之外的所有行
以下屏幕截图显示了在/etc/ntp.conf
文件中搜索以字符串 server 开头的行。在这个例子中,我们选择了计数选项:
摘要
在脚本编写中最重要且耗时的任务之一是构建所有我们需要使脚本可用和健壮的条件语句。经常提到 80-20 法则。这是指你花费 20%的时间编写主要脚本,80%的时间用于确保脚本中正确处理所有可能的情况。这就是我所说的脚本的程序完整性,我们试图仔细和准确地涵盖每种情况。
我们首先查看了一个简单的命令行列表测试。如果需要的操作很简单,那么这些功能提供了很好的功能,并且很容易添加。如果需要更复杂的功能,我们将添加if
语句。
使用if
语句,我们可以根据需要扩展它们,使用else
和elif
关键字。不要忘记elif
关键字需要它们自己的条件来评估。
最后,我们看到了如何在需要评估单个表达式的情况下使用case
。
在下一章中,我们将了解从已准备好的代码片段中读取的重要性。我们将创建一个样本if
语句,可以保存为代码片段,在编辑时读入脚本。
第四章:创建代码片段
如果您喜欢使用命令行,但也喜欢使用图形集成开发环境(IDE)的一些功能,那么本章可能会为您揭示一些新的想法。我们可以使用命令行中的vi
或vim
文本编辑器为常用的脚本元素创建快捷方式。
在本章中,我们将涵盖以下主题:
-
在
.vimrc
中创建缩写 -
使用
vim
文本编辑器阅读片段 -
在终端中使用颜色
缩写
我们已经短暂地进入了~/.vimrc
文件,现在我们将重新访问这个文件,看看缩写或abbr
控制。这个文件充当了vim
文本编辑器的运行控制机制,很可能已经安装在您的 Linux 发行版上。旧的发行版或 Unix 变种可能会有原始的vi
文本编辑器,并且会使用~/.exrc
文件。如果您不确定您的vi
版本的身份和要使用的正确运行控制文件,只需输入vi
命令。如果打开了一个空白页面,那么确实是vi
。但是,如果打开了带有vim
启动屏幕的新空白文档,那么您正在使用改进的vim
或Vi
。
缩写允许在较长的字符串的位置使用快捷字符串。这些缩写可以在vim
会话中从最后一行模式设置,但通常在控制文件中设置。shebang 可以很容易地表示为一个缩写,如下所示:
abbr _sh #!/bin/bash
缩写的基本语法如下命令所示:
abbr <shortcut><string>
使用这个缩写,我们只需要在编辑模式下输入_sh
。在输入快捷代码后按下ENTER键,shebang 的完整文本就会打印出来。实际上,不仅仅是ENTER键,按下abbr
代码后的任意键都会展开快捷方式。像这样的简单元素可以大大增加使用vim
作为我们的文本编辑器的体验。下面的截图显示了更新后的~/.vimrc
文件:
我们不限于单个缩写代码,可以添加更多的abbr
条目。例如,为了支持 Perl 脚本的 shebang,可以在行上添加:
abbr _pl #!/usr/bin/perl
下划线的使用并不是必需的,但目的是保持快捷代码的唯一性,避免输入错误。我们也不限于单行;尽管如此,缩写通常用于单行。考虑以下if
语句的缩写:
abbr _if if [-z $1];then<CR>echo "> $0 <name><CR>exit 2<CR>fi
尽管这样做是有效的,但if
语句的格式化不会完美,多行缩写远非理想。这就是我们可以考虑使用预先准备的代码片段的地方。
使用代码片段
我们所说的代码片段的含义只是准备好的代码,我们可以读入我们当前的脚本。这对于vim
能够在编辑过程中读取其他文本文件的内容来说特别容易。
ESC
:r <path-and-filename>
例如,如果我们需要读取位于$HOME/snippets
中的名为if
的文件的内容,我们将在vim
中使用以下键序列:
ESC
:r $HOME/snippets/if
该文件的内容被读入当前文档的当前光标位置下方。通过这种方式,我们可以使代码片段尽可能复杂,并保持正确的缩进以帮助可读性和一致性。
因此,我们将把创建一个片段目录放在我们的主目录中作为我们的职责:
$ mkdir -m 700 $HOME/snippets
不需要共享目录,因此在创建时将模式设置为700
或私有用户是一个好习惯。
在创建代码片段时,您可以选择使用伪代码或真实示例。我更喜欢使用真实示例,这些示例经过编辑以反映接收脚本的要求。一个简单的if
片段的内容将是:
if [ -z $1 ] ; then
echo "Usage: $0 <name>"
exit 2
fi
这为我们提供了创建带有实际示例的if
语句的布局。在这种情况下,我们检查$1
是否未设置,并在退出脚本之前向用户发送错误。关键在于保持片段简短,以限制需要进行的更改,但易于理解和扩展,根据需要。
给终端带来色彩
如果我们要向用户和执行脚本的操作员显示文本消息,我们可以提供颜色以帮助解释消息。使用红色作为错误的同义词,绿色表示成功,可以更轻松地为我们的脚本添加功能。并非所有,但肯定是绝大多数的 Linux 终端都支持颜色。内置命令echo
在与-e
选项一起使用时可以向用户显示颜色。
要以红色显示文本,我们可以使用echo
命令,如下所示:
$ echo -e "\03331mError\033[0m"
以下截图显示了代码和输出:
给终端带来色彩红色文本将立即引起注意,可能导致脚本执行失败。以这种方式使用颜色符合基本的应用设计原则。如果您觉得代码复杂,那么只需使用友好的变量来表示颜色和重置代码。在前面的代码中,我们使用了红色和最终的重置代码来将文本设置回 shell 默认值。我们可以轻松地为这些颜色代码和其他颜色创建变量:
RED="\03331m"
GREEN="\033[32m"
BLUE="\033[34m"
RESET="\033[0m"
提示
\033
值是ESCAPE字符,[31m
是红色的颜色代码。在使用变量时,我们需要小心,以确保它们与文本正确分隔。
修改前面的示例,我们可以看到如何轻松实现这一点:
$ echo -e ${RED}Error$RESET"
提示
我们使用大括号确保RED
变量被识别并与Error
单词分隔开。
将变量定义保存到$HOME/snippets/color
文件中将允许它们在其他脚本中使用。有趣的是,我们不需要编辑这个脚本;我们可以使用source
命令在运行时将这些变量定义读入脚本。在接收脚本中,我们需要添加以下行:
source $HOME/snippets/color
使用 shell 内置的source
命令将颜色变量读入脚本执行时。以下截图显示了hello5.sh
脚本的修改版本,现在我们称之为hello7.sh
,它使用了这些颜色:
![给终端带来色彩当我们执行脚本时,我们可以看到这种效果。在下面的截图中,您将看到执行和输出,无论是否提供了参数:
我们可以通过颜色编码的输出轻松识别脚本的成功和失败;绿色的Hello fred是我们提供参数的地方,红色的Usage
语句是我们没有提供所需名称的地方。
摘要
对于任何管理员脚本重用始终是效率追求中的首要问题。在命令行使用vim
可以快速有效地编辑脚本,并且可以节省缩写的输入。最好在用户的个人.vimrc
文件中设置这些缩写,并使用abbr
控制进行定义。除了缩写,我们可以看到使用代码片段的意义。这些是预先准备好的代码块,可以读入当前脚本。
最后,我们看了一下在命令行中使用颜色的价值,脚本将提供反馈。乍一看,这些颜色代码并不友好,但我们可以通过使用变量来简化这个过程。这些变量可以在脚本内在运行时设置,并通过source
命令将它们的值读入当前环境。
在下一章中,我们将看看其他机制,我们可以使用它们来编写测试表达式,简化整数和变量的使用。
第五章:替代语法
在脚本编程的旅程中,我们已经看到我们可以使用test
命令来确定条件状态。我们进一步发现,我们还可以使用单方括号。在这里,我们将回顾test
命令,并更详细地查看单方括号。在更多了解方括号之后,我们将进入更高级的变量或参数管理;因此,提供默认值并理解引用问题。
最后,我们将看到在像 bash、korn 和 zsh 这样的高级 shell 中,我们可以使用双括号!利用双圆括号和双方括号可以简化整体语法,并允许使用数学符号的标准化。
在本章中,我们将涵盖以下主题:
-
测试条件
-
提供参数默认值
-
当有疑问时-引用!
-
使用
[[
进行高级测试 -
使用
((
进行高级测试
回顾测试
到目前为止,我们已经使用内置的test
命令来驱动我们的条件语句。使用test
的其他选项,我们可以查看返回的值来确定文件系统中文件的状态。运行没有任何选项的测试将返回一个错误的输出:
$ test
测试文件
通常,我们可以使用test
来检查围绕文件的条件。例如,要测试文件是否存在,我们可以使用-e
选项。以下命令将测试/etc/hosts
文件的存在:
test -e /etc/hosts
我们可以再次运行此测试,但这次要检查文件不仅存在,而且是一个常规文件,而不是具有某些特殊目的。特定的文件类型可以是目录、管道、链接等。常规文件的选项是-f
。
$ test -f /etc/hosts
添加逻辑
如果我们需要在脚本内部打开一个文件,我们将测试该文件既是常规文件,又具有读取权限。为了使用test
实现这一点,我们还可以包括-a
选项来将多个条件连接在一起。在以下示例代码中,我们将使用-r
条件来检查文件是否可读:
$ test -f /etc/hosts -a -r /etc/hosts
同样,支持使用-o
来OR
表达式中的两个条件。
以前未见过的方括号
作为test
命令的替代,我们可以使用单方括号来实现相同的条件测试。重复之前的条件测试并省略命令本身。我们将在以下代码中重写这一点:
$ [ -f /etc/hosts -a -r /etc/hosts ]
许多时候,即使作为经验丰富的管理员,我们也习惯于语言元素,并接受它们。我觉得许多 Linux 管理员会惊讶地发现``既是一个 shell 内置命令,又是一个独立的文件。使用type
命令,我们可以验证这一点:
$ type -a [
我们可以在以下截图中看到此命令的输出,确认其存在:
![以前未见过的方括号
我们可以看到,在我使用的 Raspbian 发行版中,有内置的[
命令和/usr/bin/[
命令。正如我们所见,这两个命令都模仿了test
命令,但需要一个闭括号。
现在我们对在 bash 和早期的 Bourne shell 中找到的[
命令有了更多了解,我们现在可以继续添加一些命令行列表语法。除了命令行列表,我们还可以在以下代码示例中看到所需的功能正在工作:
$ FILE=/etc/hosts
$ [ -f $FILE -a -r $FILE ] && cat $FILE
设置了参数FILE
变量后,我们可以测试它既是常规文件,又可被用户读取,然后再尝试列出文件内容。这样,脚本就变得更加健壮,而无需复杂的脚本逻辑。我们可以在以下截图中看到代码的使用:
这种缩写方式非常常见,很容易识别。如果缩写不增加可读性,我们应该谨慎使用。我们在编写脚本时的目标应该是编写清晰易懂的代码,避免不必要的快捷方式。
提供参数默认值
在 bash 参数中,有命名空间在内存中允许我们访问存储的值。参数有两种类型:
-
变量
-
特殊参数
特殊参数是只读的,并且由 shell 预设。变量由我们自己以及 bash 维护。一般来说,在谈论语法时,bash 会用参数的家族名称来指代变量。
变量
变量是一种参数类型。这些可以由系统或我们自己设置。例如,$USER
是一个由系统设置但可以被我们编写的变量参数。因此,它不是特殊参数的只读要求。
特殊参数
特殊参数是第二种参数类型,由 shell 本身管理,并且呈现为只读。我们之前在参数中遇到过这些,比如$0
,但让我们再看看另一个$-
。我们可以扩展这些参数以了解它们的用途,使用echo
命令:
$ echo "My shell is $0 and the shell options are: $-"
从我添加的注释文本中,我们可以理解$-
选项代表配置的 shell 选项。这些可以使用set -o
命令显示,但也可以使用$-
在程序中读取。
我们可以在以下截图中看到这一点:
这里设置的选项如下:
-
h
: 这是 hashall,允许使用PATH
参数找到程序 -
i
: 这显示这是一个交互式 shell -
m
: 这是 monitor 的缩写,允许使用bg
和fg
命令将命令放入后台或从后台调出 -
B
: 这允许大括号扩展或mkdirdir{1,2}
,我们创建dir1
和dir2
-
H
: 这允许历史扩展或运行命令,比如!501
来重复历史中的命令
设置默认值
使用test
命令或括号,我们可以为变量提供默认值,包括命令行参数。拿我们之前使用过的hello4.sh
脚本来说,如果name
参数是零字节,我们可以修改它并设置它:
#!/bin/bash
name=$1
[ -z $name ] && name="Anonymous"
echo "Hello $name"
exit 0
这段代码是功能性的,但我们可以选择如何编写默认值。我们也可以直接为参数分配默认值。考虑以下代码,直接进行默认赋值:
name=${1-"Anonymous"}
在 bash 中,这被称为参数替换,可以用以下伪代码表示:
${parameter-default}
无论何处,如果一个变量(参数)没有被声明并且具有空值,将使用默认值。如果参数已经被显式声明为空值,我们将使用:-
语法,如下例所示:
parameter=
${parameter:-default}
通过现在编辑脚本,我们可以创建hello8.sh
来利用 bash 参数替换提供默认值:
#!/bin/bash
#Use parameter substitution to provide default value
name=${1-"Anonymous"}
echo "Hello $name"
exit 0
这个脚本及其输出,无论是否提供了值,都显示在以下的截图中:
hello8.sh
脚本提供了我们需要的功能,逻辑直接内置到参数赋值中。现在逻辑和赋值是脚本中的一行代码,这是保持脚本简单和可读性的重要一步。
当怀疑时 - 引用!
既然已经确定变量是一种参数,我们应该始终记住这一点,特别是在阅读手册和 HOWTO 时。文档经常提到参数,并在这样做时包括变量,以及 bash 特殊参数,如$1
等。为此,我们将看看为什么在命令行或脚本中使用参数时最好引用这些参数。现在学习这一点可以在以后节省我们很多痛苦和心痛,特别是当我们开始研究循环时。
首先,我们应该用于读取变量值的正确术语是参数扩展。对你和我来说,这是读取一个变量,但对 bash 来说这太简单了。正确的命名,比如参数扩展,减少了任何对其含义的歧义,但同时增加了复杂性。在下面的例子中,代码的第一行将fred
的值分配给name
参数。代码的第二行使用参数扩展来打印存储在内存中的值。$
符号用于允许参数的扩展:
$ name=fred
$ echo "The value is: $name"
在这个例子中,我们使用了双引号来允许echo
打印单个字符串,因为我们使用了空格。如果不使用引号,echo 可能会将其视为多个参数。空格是大多数 shell(包括 bash)中的默认字段分隔符。通常,当我们没有考虑使用引号时,我们看不到直接的空格。考虑我们之前使用的命令行代码的以下摘录:
$ FILE=/etc/hosts
$ [ -f $FILE -a -r $FILE ] && cat $FILE
尽管这样可以工作,我们可能有点幸运,特别是如果我们正在从我们自己没有创建的文件列表中填充FILE
参数。一个文件可能在其名称中包含空格是很有可能的。现在让我们使用不同的文件重新播放这段代码。考虑以下命令:
$ FILE="my file"
$ [ -f $FILE -a -r $FILE ] && cat $FILE
尽管在结构上代码没有改变,但现在失败了。这是因为我们向``命令提供了太多的参数。即使我们使用test
命令,失败的结果也是一样的。
尽管我们已经正确引用了文件名分配给参数FILE
,但在参数扩展时我们没有
保护空格。我们可以看到代码失败,如下面的截图所示:
![当你犹豫时-引用!
我们可以看到,这对我们的脚本来说还没有准备好。唉,我们曾经认为坚固的东西现在已经支离破碎,就像泰坦尼克号一样,我们的代码已经沉没了。
然而,一个简单的解决方案是恢复引用参数扩展,除非特别不需要。通过对代码进行简单的编辑,我们可以使这艘船不会沉没:
$ FILE="my file"
$ [ -f "$FILE" -a -r "$FILE" ] && cat "$FILE"
现在我们可以自豪地站在白星航运公司的码头上,因为我们看到泰坦尼克号 II 在以下代码示例中被推出,这在下面的截图中被捕捉到:
这些微小的引号可以产生真正令人惊讶,有时甚至有点难以置信的影响。当扩展变量时,我们绝不能忽视引号。为了确保我们强调这一点,我们可以在另一个更简单的例子中突出这种现象。让我们考虑现在只想删除文件的情况。在第一个例子中,我们不使用引号:
$ rm $FILE
这段代码将导致失败,因为参数扩展将导致以下感知命令:
$ rm my file
这段代码将失败,因为它无法找到my
文件或file
文件。更糟糕的是,可能我们会意外地删除错误的文件,如果其中任何一个名称被错误地解析。
而引用参数扩展将拯救一切,就像我们在第二个例子中看到的那样:
$ rm "$FILE"
这被正确地扩展为我们在以下代码示例中说明的期望命令:
$ rm "my file"
我确实希望这些例子能够说明在扩展参数时需要小心,并且你意识到了其中的陷阱。
使用[[进行高级测试
使用双括号[[条件]]
允许我们进行更高级的条件测试,但与 Bourne Shell 不兼容。双括号首次作为 korn shell 中的定义关键字引入,并且也可用于 bash 和 zsh。与单括号不同,这不是一个命令而是一个关键字。使用 type 命令可以确认这一点:
$ type [[
空格
[[
不是一个命令在空格方面是重要的。作为关键字,[[
在 bash 扩展它们之前解析其参数。因此,单个参数将始终表示为单个参数。即使违反最佳实践,[[
可以减轻参数值中空格相关的一些问题。重新考虑我们之前测试的条件,当使用[[
时,我们可以省略引号,如下例所示:
$ echo "The File Contents">"my file"
$ FILE="my file"
$ [[ -f $FILE && -r $FILE ]] && cat "$FILE"
当使用cat
时,我们仍然需要引用参数,如您所见,我们可以在双括号中使用引号,但它们变得可选。请注意,我们还可以使用更传统的&&
和||
来分别表示-a
和-o
。
其他高级功能
一些额外功能可以包括在双括号中。即使在使用它们时失去了可移植性,也有一些很好的功能可以克服这一损失。请记住,如果我们只使用 bash,那么我们可以使用双括号,但不能在 Bourne Shell 中运行我们的脚本。我们在下面的部分中获得的高级功能包括模式匹配和正则表达式。
模式匹配
使用双括号,我们不仅可以匹配字符串,还可以使用模式匹配。例如,我们可能需要专门处理以.pl
结尾的 Perl 脚本文件。我们可以在条件中轻松实现这一点,包括模式作为匹配,如下例所示:
$ [[ $FILE = *.pl ]] &&cp"$FILE" scripts/
正则表达式
我们不仅可以使用=~
运算符进行简单的模式匹配,还可以匹配正则表达式。我们可以使用正则表达式重写上一个示例:
$ [[ $FILE =~ \.pl$ ]] &&cp "$FILE" scripts/
提示
由于单个点或句号在正则表达式中具有特殊含义,因此我们需要用\
进行转义。
以下截图显示了正则表达式匹配与名为my.pl
和my.apl
的文件一起工作。匹配正确显示了以.pl
结尾的文件:
正则表达式脚本
不能忽视正则表达式的威力。使用正则表达式进行条件测试的另一个简单演示是公开颜色的美式和英式拼写:color 和 colour。我们可以提示用户是否要为脚本选择彩色或单色输出,同时考虑两种拼写。在脚本中执行此操作的行如下:
if [[ $REPLY =~ colou?r ]] ; then
正则表达式通过使 u 可选来满足 color 的两种拼写:u?。此外,我们可以通过设置 shell 选项来禁用大小写敏感性,从而允许COLOR和 color 的匹配:
shopt -s nocasematch
此选项可以在脚本末尾使用以下命令再次禁用:
shopt -s nocasematch
当我们使用我们命名的变量参数$GREEN
和$RESET
时,我们会影响输出的颜色。只有在我们引用颜色定义文件时,绿色才会显示。当我们选择单色显示时,选择单色将确保变量参数为空且无效。
完整的脚本显示在以下截图中:
使用(( ))进行算术运算
在使用 bash 和其他高级 shell 时,我们可以使用(( ))
符号来简化脚本中的数学运算。
简单的数学
在 bash 中,双括号结构允许进行算术展开。在最简单的格式中,我们可以轻松进行整数运算。这成为了let
内置的替代品。以下示例展示了使用let
命令和双括号来实现相同的结果:
$ a=(( 2 + 3 ))
$ let a=2+3
在这两种情况下,a
参数都被填充为2 + 3
的和。
参数操作
也许,在脚本编写中对我们更有用的是使用双括号的 C 风格参数操作。我们经常可以使用这个来在循环中递增计数器,并限制循环迭代的次数。考虑以下代码:
$ COUNT=1
$ (( COUNT++ ))
echo $COUNT
在这个例子中,我们首先将COUNT
设置为1
,然后使用++
运算符对其进行递增。当在最后一行中输出时,参数将具有值2
。我们可以在以下截图中看到结果:
我们可以通过以下语法来以长格式实现相同的结果:
$ COUNT=1
$ (( COUNT=COUNT+1 ))
echo $COUNT
当然,这允许对COUNT
参数进行任何增量,而不仅仅是单个单位的增加。同样地,我们可以使用--
运算符进行倒数,如下例所示:
$ COUNT=10
$ (( COUNT-- ))
echo $COUNT
我们从10
开始,然后在双括号中将值减少1
。
提示
请注意,我们不使用$
来扩展括号内的参数。它们用于参数操作,因此我们不需要显式地扩展参数。
标准算术测试
另一个我们可以从这些双括号中获得的优势是在测试中。我们可以简单地使用>
而不是使用-gt
来表示大于。我们可以在以下代码中演示这一点:
$(( COUNT > 1 )) && echo "Count is greater than 1"
以下截图为您演示了这一点:
正是这种标准化,无论是在 C 风格的操作还是测试中,使双括号对我们如此有用。这种用法既适用于命令行,也适用于脚本。当我们研究循环结构时,我们将广泛使用这个特性。
总结
在本章中,我真诚地希望我们为您介绍了许多新颖有趣的选择。这是一个范围广泛的领域,我们从回顾测试的使用开始,发现[
是一个命令而不是语法结构。它作为一个命令的主要影响在于空格,我们还讨论了引用变量的必要性。
即使我们通常称变量为变量。我们也看到它们的正确名称,特别是在文档中是参数。读取变量是参数展开。理解参数展开可以帮助我们理解关键字[[
的用法。双方括号不是命令,也不展开参数。这意味着即使变量包含空格,我们也不需要引用变量。此外,我们可以使用双方括号进行高级测试,如模式匹配或正则表达式。
最后,我们看了双括号符号的算术展开和参数操作。它最大的特点是可以轻松地递增和递减计数器。
在下一章中,我们将进入 bash 中的循环结构,并利用本章中学到的一些新技能。
第六章:使用循环迭代
记住,脚本是给懒人用的。我们是世界上有更重要事情要做的人,而不是重复一项任务 100 次或更多次;循环是我们的朋友。
循环结构是脚本的生命线。这些循环是可以可靠和一致地重复多次执行相同任务的工作引擎。想象一下,有 10 万行文本在 CSV 文件中,必须检查是否有错误条目。一旦开发完成,脚本可以轻松而准确地完成这项任务,但在人类的情况下,可靠性和准确性将很快失败。
所以让我们看看如何通过在本章中涵盖以下主题来节省时间和理智:
-
for 循环
-
循环控制
-
while 和 until
-
从文件中读取
-
操作菜单
for 循环
我们所有的循环控制都可以很简单,我们将从for
循环开始。for
是 bash 中的关键字,在工作中类似于if
。我们可以使用命令类型来验证这一点,如下例所示:
$ type for
for is a shell keyword
作为保留的 shell 关键字,我们可以在脚本中和直接在命令行中使用for
循环。这样,我们可以在脚本内外利用循环,优化命令行的使用。一个简单的for
循环如下例所示:
# for u in bob joe ; do
useradd $u
echo '$u:Password1' | chpasswd
passwd -e $u
done
在for
循环中,我们从右侧的列表中读取以填充左侧的变量参数,这种情况下我们将从包含bob
和joe
的列表中读取,并插入到参数变量u
中。列表中的每个项目都会逐个插入到变量中。这样,只要列表中有要处理的项目,循环就会执行,直到列表耗尽。
实际上,对我们来说,执行此循环意味着我们将:
-
创建用户
bob
-
为
bob
设置密码 -
让用户
bob
的密码过期,这样在第一次登录时就需要重置
然后我们循环回去,重复为用户joe
执行相同的过程。
我们可以在以下截图中查看前面的示例;在通过sudo -i
获得 root 访问权限后,我们继续运行循环并创建用户:
在for
循环中读取的列表可以动态或静态生成,如最后一个例子所示。要创建动态列表,我们可以使用各种通配技术来填充列表。例如,要处理目录中的所有文件,我们可以使用*
,如下例所示:
for f in * ; do
stat "$f"
done
注意
当生成列表时,比如使用文件通配符,我们应该引用变量参数的扩展。如果没有引号,可能会包含一个空格,导致命令失败。这就是我们在stat
命令中看到的情况。
在以下示例中,我们隔离以ba*
开头的文件名。然后我们使用stat
命令打印 inode 元数据。代码和输出如下截图所示:
这个列表也可以从另一个命令的输出或一系列命令的输出中生成。例如,如果我们需要打印所有已登录用户的当前工作目录,我们可以尝试类似以下的操作:
$ for user in $(who | cut -f1 -d"") ; do
lsof -u $user -a -c bash | grep cwd
done
在前面的例子中,我们可以看到参数名称的选择取决于您;我们不限于单个字符,我们可以在此示例中使用$user
name。使用小写我们不会覆盖系统变量$USER
。以下截图演示了循环和随后的输出:
lsof
命令将列出打开的文件,我们可以依次搜索每个用户打开的文件,并使用bash
命令作为当前工作目录。
使用我们迄今为止创建的脚本,我们可以创建一个名为hello9.sh
的新脚本。如果我们将$HOME/bin/hello2.sh
脚本复制到新脚本中,我们可以编辑它以使用for
循环:
#!/bin/bash
echo "You are using $(basename $0)"
for n in $*
do
echo "Hello $n"
done
exit 0
该循环用于遍历提供的每个命令行参数并分别向每个用户打招呼。当我们执行脚本时,我们可以看到我们现在可以为每个用户显示 hello 消息。这在下面的截图中显示:
尽管我们在这里看到的仍然相对琐碎,但我们现在应该意识到脚本和循环可以做些什么。此脚本的参数可以是我们已经使用过的用户名或其他任何内容。如果我们坚持使用用户名,那么创建用户帐户并设置密码将非常容易,就像我们之前看到的那样。
控制循环
进入循环后,我们可能需要提前退出循环,或者可能需要排除某些项目不进行处理。如果我们只想在列表中处理目录,而不是任何类型的文件,那么为了实现这一点,我们有循环控制关键字,如break
和continue
。
break
关键字用于退出循环,不再处理条目,而continue
关键字用于停止处理当前条目并恢复处理下一个条目。
假设我们只想处理目录,我们可以在循环中实现一个测试,并确定文件类型:
$ for f in * ; do
[ -d "$f" ] || continue
chmod 3777 "$f"
done
在循环中,我们想要设置包括 SGID 和粘性位的权限,但仅适用于目录。*
搜索将返回所有文件,循环内的第一条语句将确保我们只处理目录。如果测试是针对当前循环进行的,目标未通过测试并不是一个目录;continue
关键字将检索下一个循环列表项。如果测试返回 true 并且我们正在处理一个目录,那么我们将处理后续语句并执行chmod
命令。
如果我们需要运行循环直到找到一个目录,然后退出循环,我们可以调整代码,以便可以遍历每个文件。如果文件是一个目录,那么我们使用break
关键字退出循环:
$ for f in * ; do
[ -d "$f" ] &&break
done
echo "We have found a directory $f"
在下面的截图中,我们可以看到我刚刚编写的代码在运行中的情况:
通过使用以下代码,我们可以打印列表中找到的每个目录:
for f in * ; do
[ -d "$f" ] || continue
dir_name="$dir_name $f"
done
echo "$dir_name"
我们可以通过仅在循环中处理目录项目来实现结果。我们可以使用if
测试仅处理常规文件。在这个例子中,我们将目录名附加到dir_name
变量。一旦退出循环,我们打印完整的目录列表。我们可以在下面的截图中看到这一点:
使用这些示例和您自己的想法,您现在应该能够看到如何使用continue
和break
关键字控制循环。
While 循环和 until 循环
使用for
循环时,我们遍历列表,无论是我们创建的列表还是动态生成的列表。使用while
或until
循环时,我们根据条件变为真或假来循环。
while
循环在条件为真时循环,相反until
循环在条件为假时循环。以下命令将从 10 倒数到零。循环的每次迭代都打印变量,然后将值减 1:
$ COUNT=10
$ while (( COUNT >= 0 )) ; do
echo -e "$COUNT \c"
(( COUNT-- ))
done ; echo
我们可以在下面的截图中看到这个命令的输出;从而确认倒计时到零:
注意
在这里使用的\c
转义序列允许抑制echo
通常使用的换行符。这样,我们可以保持单行输出的倒计时。我想您会同意这是一个不错的效果。
使用until
循环可以获得此循环的功能;只需要快速重新考虑逻辑,因为我们希望循环直到条件变为真。通常,关于使用哪种循环是个人选择,以及逻辑对您来说哪种循环效果最好。以下示例显示了使用until
循环编写的循环:
$ COUNT=10
$ until (( COUNT < 0 )) ; do
echo -e "$COUNT \c"
(( COUNT-- ))
done ; echo
从文件中读取输入
现在,看起来这些循环可以做的不仅仅是倒数。我们可能希望从文本文件中读取数据并处理每一行。我们在本书中早些时候看到的 shell 内置read
命令可以用于逐行读取文件。这样,我们可以使用循环处理文件的每一行。
为了演示其中一些功能,我们将使用一个包含服务器地址的文件。这些可以是主机名或 IP 地址。在下面的示例中,我们将使用 Google DNS 服务器的 IP 地址。以下命令显示了servers.txt
文件的内容:
$cat servers.txt
8.8.8.8
8.8.4.4
使用while
循环的条件中使用read
命令,我们可以循环读取文件中的行。我们在done
关键字后直接指定输入文件。对于从文件中读取的每一行,我们可以使用ping
命令测试服务器是否正常运行,如果服务器响应,我们将其添加到可用服务器列表中。循环结束后,将打印此列表。在下面的示例中,我们可以看到我们开始添加书中涵盖的脚本元素:
$ while read server ; do
ping -c1 $server && servers_up="$servers_up $server"
done < servers.txt
echo "The following servers are up: $servers_up"
我们可以在以下截图中验证操作:
使用这种循环,我们可以开始构建非常实用的脚本,以处理从命令行或脚本中提供的信息。很容易用$1
代表传递到脚本中的位置参数来替换我们读取的文件名。让我们返回到ping_server.sh
脚本,并调整它以接受输入参数。我们可以将脚本复制到新的$HOME/bin/ping_server_from_file.sh
文件中。在脚本中,我们首先测试输入参数是否为文件。然后,我们创建一个包含日期的标题的输出文件。当我们进入循环时,我们将可用服务器追加到此文件,并在脚本结束时列出文件:
#!/bin/bash
# Author: @theurbanpenguin
# Web: www.theurbapenguin.com
# Script to ping servers from file
# Last Edited: August 2015
if [ ! -f"$1 ] ; then
echo "The input to $0 should be a filename"
exit 1
fi
echo "The following servers are up on $(date +%x)"> server.out
done
while read server
do
ping -c1 "$server"&& echo "Server up: $server">> server.out
done
cat server.out
现在我们可以以以下方式执行脚本:
$ ping_server_from_file.sh servers.txt
脚本执行的输出应该类似于以下截图:
创建操作员菜单
我们可以为需要从 shell 获取有限功能并且不想学习命令行使用细节的 Linux 操作员提供菜单。我们可以使用他们的登录脚本为他们启动菜单。此菜单将提供要选择的命令选项列表。菜单将循环,直到用户选择退出菜单。我们可以创建一个新的$HOME/bin/menu.sh
脚本,菜单循环的基础如下:
while true
do
……
done
我们在这里创建的循环是无限的。true
命令将始终返回 true 并持续循环;但是,我们可以提供循环控制机制,以允许用户离开菜单。要开始构建菜单的结构,我们需要在循环中输出一些文本,询问用户选择的命令。每次加载菜单之前,我们将清除屏幕,并在所需命令执行后出现额外的读取提示。
这允许用户在清除屏幕并重新加载菜单之前读取命令的输出。此时脚本将如下所示:
#!/bin/bash
# Author: @theurbanpenguin
# Web: www.theurbapenguin.com
# Sample menu
# Last Edited: August 2015
while true
do
clear
echo "Choose an item: a,b or c"
echo "a: Backup"
echo "b: Display Calendar"
echo "c: Exit"
read -sn1
read -n1 -p "Press any key to continue"
done
如果在此阶段执行脚本,将没有机制可以离开脚本。我们还没有添加任何代码到菜单选择;但是,您可以使用Ctrl + c键测试功能并退出。
此时,菜单应该类似于以下截图中显示的输出:
为了构建菜单选择背后的代码,我们将实现一个case
语句。这将被添加在两个read
命令之间,如下所示:
read -sn1
case "$REPLY" in
a) tar -czvf $HOME/backup.tgz ${HOME}/bin;;
b) cal;;
c) exit 0;;
esac
read -n1 -p "Press any key to continue"
我们可以看到我们已经添加到case
语句中的三个选项,a
,b
和c
:
-
选项 a:这将运行
tar
命令来备份脚本 -
选项 b:这将运行
cal
命令来显示当前月份 -
选项 c:这将退出脚本
为了确保用户在退出其登录脚本时注销,我们将运行:
exec menu.sh
exec
命令用于确保在menu.sh
文件完成后保留 shell。这样,用户永远不需要体验 Linux shell。完整的脚本显示在以下截图中:
摘要
在本章中,我们已经开始取得进展。我们已经能够将许多我们以前使用的元素结合成连贯和功能性的脚本。尽管本章的重点是循环,但我们已经使用了命令行列表,if
语句,case
语句和算术计算。
我们在本章开头描述循环为我们脚本的工作马,并且我们已经能够用for
,while
和until
循环来证明这一点。for
循环用于遍历列表的元素。列表可以是静态的或动态的,重点是动态列表,我们展示了如何通过文件通配符或命令扩展简单地创建这些列表。
while
和until
循环受条件控制。while
循环在提供的条件为真时循环。until
循环将在提供的条件返回真或返回假时循环。continue
和break
关键字是特定于循环的,以及exit
,我们可以控制循环流程。
在下一章中,我们将学习使用函数将脚本模块化。
第七章:使用函数创建构建块
在本章中,我们将深入了解函数的奇妙世界。我们可以将这些视为创建强大和适应性脚本的模块化构建块。通过创建函数,我们将代码添加到一个单独的构建块中,与脚本的其余部分隔离开来。专注于改进单个函数要比尝试改进整个脚本容易得多。没有函数,很难专注于问题区域,代码经常重复,这意味着需要在许多位置进行更新。函数被命名为代码块或脚本中的脚本,并且它们可以克服与更复杂代码相关的许多问题。
随着我们在本章中的学习,我们将涵盖以下主题:
-
函数
-
向函数传递参数
-
返回值
-
使用函数的菜单
介绍函数
函数是作为命名元素存在于内存中的代码块。这些元素可以在 shell 环境中创建,也可以在脚本执行中创建。当在命令行上发出命令时,首先检查别名,然后检查匹配的函数名称。要显示驻留在您的 shell 环境中的函数,可以使用以下代码:
$ declare -F
输出将根据您使用的发行版和创建的函数数量而变化。在我的 Raspbian OS 上,部分输出显示在以下截图中:
使用-f
选项,您可以显示函数及其相关定义。但是,如果我们只想看到单个函数定义,我们可以使用type
命令:
$ type quote
前面的代码示例将显示quote
函数的代码块,如果它存在于您的 shell 中。我们可以在以下截图中看到此命令的输出:
在 bash 中,quote
函数会在提供的输入参数周围插入单引号。例如,我们可以展开USER
变量并将值显示为字符串文字;这在以下截图中显示。截图捕获了命令和输出:
大多数代码都可以用伪代码表示,显示一个示例布局。函数也不例外,创建函数的代码列在以下示例中:
function <function-name> {
<code to execute>
}
该函数创建时没有do
和done
块,就像我们在之前的循环中使用的那样。大括号的目的是定义代码块的边界。
以下是一个简单的函数,用于显示聚合系统信息的代码。这可以在命令行中创建,并将驻留在您的 shell 中。这将不会保留登录信息,并且在关闭 shell 或取消函数设置时将丢失。要使函数持久存在,我们需要将其添加到用户帐户的登录脚本中。示例代码如下:
$ function show_system {
echo "The uptime is:"
uptime
echo
echo "CPU Detail"
lscpu
echo
echo "User list"
who
}
我们可以使用type
命令打印函数的详细信息,类似于之前的示例;这在以下截图中显示:
要执行函数,我们只需输入show_system
,我们将看到静态文本和来自uptime
、lscpu
和who
三个命令的输出。当然,这是一个非常简单的函数,但我们可以通过允许在运行时传递参数来开始添加更多功能。
向函数传递参数
在本章的前面,我们将函数称为脚本中的脚本,我们仍将保持这种类比。类似于脚本可以有输入参数,我们可以创建接受参数的函数,使它们的操作不那么静态。在我们开始编写脚本之前,我们可以看一下命令行中一个有用的函数。
提示
我最讨厌过度注释的配置文件,尤其是存在文档详细说明可用选项的情况。
GNU Linux 命令sed
可以轻松地编辑文件并删除注释行和空行。我们在这里引入了流编辑器sed
,但我们将在下一章节中更详细地讨论它。
进行原地编辑的sed
命令行将是:
$ sed -i.bak '/^\s*#/d;/^$/d' <filename>
我们可以通过逐个元素地分解命令行来进行取证。让我们深入研究一下:
-
sed -i.bak
:这会编辑文件并创建一个带有扩展名.bak
的备份。原始文件将以<filename>.bak
的形式可访问。 -
/^
:以...开头的行,也就是行的第一个字符。 -
\s*
:这意味着任意数量的空白,包括没有空格或制表符。 -
#/
:后跟注释。整体上/^\s*#/
表示我们正在寻找以注释或空格和注释开头的行。 -
d
:删除匹配行的操作。 -
; /^$/d
:分号用于分隔表达式,第二个表达式与第一个类似,但这次我们准备删除空行或以行结束标记$
开头的行。
将此移入函数中,我们只需要考虑一个好名字。我喜欢在函数名中加入动词;这有助于确保唯一性并确定函数的目的。我们将创建clean_file
函数如下:
$ function clean_file {
sed -i.bak '/^\s*#/d;/^$/d' "$1"
}
与脚本中一样,我们使用位置参数来接受命令行参数。我们可以在函数中用$1
替换之前使用的硬编码文件名。我们将引用这个变量以防止文件名中有空格。为了测试clean_file
函数,我们将复制一个系统文件并使用副本进行操作。这样,我们可以确保不会对任何系统文件造成伤害。我们向所有读者保证,在编写本书的过程中没有损坏任何系统文件。以下是我们需要遵循的详细步骤,以对新函数进行测试:
-
按照描述创建
clean_file
函数。 -
使用
cd
命令而不带参数切换到你的主目录。 -
将时间配置文件复制到你的主目录:
cp /etc/ntp.conf $HOME
。 -
使用以下命令计算文件中的行数:
wc -l $HOME/ntp.conf
。 -
现在,使用以下命令删除注释和空行:
clean_file $HOME/ntp.conf
。 -
现在,使用
wc -l $HOME/ntp.conf
重新计算行数。 -
从我们创建的原始备份中:
wc -l $HOME/ntp.conf.bak
。
命令序列如下截图所示:
我们可以使用执行函数时提供的参数将函数的注意力引向所需的文件。如果我们需要保留此函数,那么我们应该将其添加到登录脚本中。但是,如果我们想要在 shell 脚本中测试这个函数,我们可以创建以下文件来做这个练习,并练习我们学到的其他一些元素。我们需要注意,函数应该始终在脚本的开头创建,因为它们需要在被调用时存储在内存中。只需想象你的函数需要在你扣动扳机之前被解锁和加载。
我们将创建一个新的 shell 脚本$HOME/bin/clean.sh
,并且像往常一样,需要设置执行权限。脚本的代码如下:
#!/bin/bash
# Script will prompt for filename
# then remove commented and blank lines
function is_file {
if [ ! -f "$1" ] ; then
echo "$1 does not seem to be a file"
exit 2
fi
}
function clean_file {
is_file "$1"
BEFORE=$(wc -l "$1")
echo "The file $1 starts with $BEFORE"
sed -i.bak '/^\s*#/d;/^$/d' "$1"
AFTER=$(wc -l "$1")
echo "The file $1 is now $AFTER"
}
read -p "Enter a file to clean: "
clean_file "$REPLY"
exit 1
我们在脚本中提供了两个函数。第一个is_file
只是测试以确保我们输入的文件名是一个普通文件。然后我们声明clean_file
函数并添加了一些额外的功能,显示操作前后文件的行数。我们还可以看到函数可以被嵌套,并且我们用clean_file
调用is_file
函数。
没有函数定义,我们在文件末尾只有三行代码,可以在之前的代码块中看到,并保存为$HOME/bin/clean.sh
。我们首先提示输入文件名,然后运行clean_file
函数,该函数又调用is_file
函数。这里主要是主要代码的简单性。复杂性在函数中,因为每个函数都可以作为一个独立的单元进行处理。
我们现在可以测试脚本的操作,首先使用一个错误的文件名,如下面的截图所示:
现在我们已经看到了对一个不正确的文件的操作,我们可以再试一次,使用一个实际的文件!我们可以使用之前操作过的同一个系统文件。我们需要首先将文件恢复到它们的原始状态:
$ cd $HOME
$ rm $HOME/ntp.conf
$ mv ntp.conf.bak ntp.conf
文件现在准备好了,我们可以在$HOME
目录中执行脚本,如下面的截图所示:
从函数返回值
每当我们在函数内打印在屏幕上的语句时,我们可以看到它们的结果。然而,很多时候我们希望函数在脚本中填充一个变量而不显示任何内容。在这种情况下,我们在函数中使用return
。当我们从用户那里获得输入时,这一点尤为重要。我们可能更喜欢将输入转换为已知的情况,以使条件测试更容易。将代码嵌入函数中允许它在脚本中被多次使用。下面的代码显示了我们如何通过创建to_lower
函数来实现这一点:
function to_lower ()
{
input="$1"
output=$(tr [A-Z] [a-z] <<<"$input")
return $output
}
通过逐步分析代码,我们可以开始理解这个函数的操作:
-
input="$1"
:这更多是为了方便,我们将第一个输入参数分配给一个命名变量输入。 -
output=$(tr [A-Z] [a-z] <<< "$input")
:这是函数的主要引擎,其中发生从大写到小写的转换。使用 here string 操作符<<<
允许我们扩展变量以读取到tr
程序的内容。这是一种输入重定向形式。 -
return$output
:这是我们创建返回值的方法。
这个函数的一个用途将在一个读取用户输入并简化测试以查看他们是否选择了Q
或q
的脚本中。这可以在以下代码片段中看到:
function to_lower ()
{
input="$1"
output=$(tr [A-Z] [a-z] <<< "$input")
return $output
}
while true
do
read -p "Enter c to continue or q to exit: "
$REPLY=$(to_lower "$REPLY")
if [ $REPLY = "q" ] ; then
break
fi
done
echo "Finished"
在菜单中使用函数
在上一章,第六章,使用循环迭代,我们创建了menu.sh
文件。菜单是使用函数的很好的目标,因为case
语句非常简单地维护单行条目,而复杂性仍然可以存储在每个函数中。我们应该考虑为每个菜单项创建一个函数。如果我们将之前的$HOME/bin/menu.sh
复制到$HOME/bin/menu2.sh
,我们可以改进功能。新菜单应该如下代码所示:
#!/bin/bash
# Author: @theurbanpenguin
# Web: www.theurbapenguin.com
# Sample menu with functions
# Last Edited: Sept 2015
function to_lower {
input="$1"
output=$(tr [A-Z] [a-z] <<< "$input")
return $output
}
function do_backup {
tar -czvf $HOME/backup.tgz ${HOME}/bin
}
function show_cal {
if [ -x /usr/bin/ncal ] ; then
command="/usr/bin/ncal -w"
else
command="/usr/bin/cal"
fi
$command
}
while true
do
clear
echo "Choose an item: a, b or c"
echo "a: Backup"
echo "b: Display Calendar"
echo "c: Exit"
read -sn1
REPLY=$(to_lower "$REPLY")
case "$REPLY" in
a) do_backup;;
b) show_cal;;
c) exit 0;;
esac
read -n1 -p "Press any key to continue"
done
正如我们所看到的,我们仍然保持case
语句的简单性;然而,我们可以通过函数来增加脚本的复杂性。例如,当选择日历的选项 b 时,我们现在检查ncal
命令是否可用。如果可用,我们使用ncal
并使用-w
选项来打印周数。我们可以在下面的截图中看到这一点,我们选择显示日历并安装ncal
。
我们也不必担心大写锁定键,因为to_lower
函数将我们的选择转换为小写。随着时间的推移,很容易向函数中添加额外的元素,因为我们知道只会影响到单个函数。
总结
我们在脚本编写方面仍在飞速进步。我希望这些想法能留在你心中,并且你会发现代码示例很有用。函数对于脚本的易维护性和最终功能非常重要。脚本越容易维护,你就越有可能随着时间的推移添加改进。我们可以在命令行或脚本中定义函数,但在使用之前,它们需要被包含在脚本中。
函数本身在脚本运行时加载到内存中,但只要脚本被分叉而不是被源化,它们将在脚本完成后从内存中释放。在本章中,我们已经稍微涉及了sed
,在下一章中我们将更多地学习如何使用流编辑器(sed
)。sed
命令非常强大,我们可以在脚本中充分利用它。
第八章:介绍 sed
在上一章中,我们看到我们可以利用sed
在脚本中编辑文件。sed
命令是流编辑器,逐行打开文件以搜索或编辑文件内容。从历史上看,这追溯到 Unix,那时系统可能没有足够的 RAM 来打开非常大的文件。使用sed
绝对是必不可少的。即使在今天,我们仍然会使用sed
来对包含数百或数千条记录的文件进行更改和显示数据。这比人类尝试做同样的事情更简单、更容易、更可靠。最重要的是,正如我们所见,我们可以在脚本中使用sed
自动编辑文件,无需人工干预。
我们将首先查看grep
并搜索文件中的文本。grep
命令中的re
是正则表达式的缩写。在我们查看sed
之前,这介绍了 POSIX 兼容正则表达式的强大功能。即使在本章中我们不涉及脚本编写,我们也将介绍一些非常重要的工具,可以在脚本中使用。在下一章中,我们将看到sed
在脚本中的实际应用。
目前,我们已经排队了足够的内容,我们将在本章中涵盖以下主题:
-
使用
grep
显示文本 -
使用正则表达式
-
理解
sed
的基础知识
使用 grep 显示文本
欢迎回来,欢迎来到在命令行中使用正则表达式的强大之处。我们将通过查看grep
命令来开始这个旅程。这将使我们能够掌握一些简单的搜索文本的概念,然后再转向更复杂的正则表达式和使用sed
编辑文件。
全局正则表达式打印(grep),或者我们更常用的称为grep
命令,是一个用于全局搜索(跨文件中的所有行)并将结果打印到STDOUT
的命令行工具。搜索字符串是一个正则表达式。
grep
命令是如此常见的工具,它有许多简单的示例和许多我们每天都可以使用它的场合。在接下来的部分中,我们将包含一些简单而有用的示例,并进行解释。
在接口上显示接收到的数据
在这个示例中,我们将仅打印eth0
接口接收到的数据。
注意
这是我在本课程中使用的树莓派的主要网络连接接口。如果您不确定您的接口名称,可以使用ifconfig -a
命令显示所有接口,并在您的系统上选择正确的接口名称。如果找不到ifconfig
,请尝试输入完整路径/sbin/ifconfig
。
仅使用ifconfig eth0
命令,就可以将大量数据打印到屏幕上。为了仅显示接收到的数据包,我们可以隔离包含RX packets
(RX
表示接收)的行。这就是grep
发挥作用的地方:
$ ifconfig eth0 | grep "RX packets"
使用管道或竖线,我们可以将ifconfig
命令的输出发送到grep
命令的输入。在这种情况下,grep
正在搜索一个非常简单的正则表达式,即"RX packet"。搜索字符串是区分大小写的,因此我们需要正确地获取这个或者使用grep
的-i
选项以不区分大小写地运行搜索,如下例所示:
$ ifconfig eth0 | grep -i "rx packets"
注意
在搜索配置文件选项时,不区分大小写的搜索特别有用,因为配置文件通常是混合大小写的。
我们可以在以下截图中看到初始命令的结果,确认我们已经能够隔离出单行输出,如下所示:
显示用户帐户数据
在 Linux 中,本地用户帐户数据库是/etc/passwd
文件,所有用户帐户都可以读取。如果我们想要搜索包含我们自己数据的行,我们可以在搜索中使用我们自己的登录名,或者使用参数扩展和$USER
变量。我们可以在以下命令示例中看到这一点:
$ grep "$USER" /etc/passwd
在这个例子中,grep
的输入来自/etc/passwd
文件,并且我们搜索$USER
变量的值。同样,在这种情况下,它是一个简单的文本,但仍然是正则表达式,只是没有任何操作符。
为了完整起见,我们在下面的屏幕截图中包含了输出:
我们可以使用这种类型的查询作为脚本中的条件来扩展一下。我们可以使用这个来检查用户帐户是否存在,然后再尝试创建一个新帐户。为了尽可能简化脚本,并确保不需要管理员权限,创建帐户将仅显示提示和条件测试,如下面的命令行示例所示:
$ bash
$ read -p "Enter a user name: "
$ if (grep "$REPLY" /etc/passwd > /dev/null) ; then
> echo "The user $REPLY exists"
> exit 1
>fi
grep
搜索现在使用由read
填充的$REPLY
变量。如果我输入名称pi
,将显示一条消息,然后退出,因为我的用户帐户也叫pi
。没有必要显示grep
的结果,我们只是在寻找一个返回代码,要么是true
要么是false
。为了确保如果用户在文件中,我们不会看到任何不必要的输出,我们将grep
的输出重定向到特殊设备文件/dev/null
。
如果要从命令行运行此命令,应首先启动一个新的 bash shell。您只需键入bash
即可。这样,当exit
命令运行时,它不会将您注销,而是关闭新打开的 shell。我们可以看到这种情况发生以及在以下图形中指定现有用户时的结果:
列出系统中的 CPU 数量
另一个非常有用的功能是grep
可以计算匹配的行数并且不显示它们。我们可以使用这个来计算系统上的 CPU 或 CPU 核心的数量。每个核心或 CPU 在/proc/cpuinfo
文件中都有一个名称。然后我们搜索文本name
并计算输出;使用的-c
选项如下例所示:
$ grep -c name /proc/cpuinfo
我正在使用 Raspberry Pi 2,它有四个核心,如下面的输出所示:
如果我们在具有单个核心的 Raspberry Pi Model B 上使用相同的代码,我们将看到以下输出:
我们可以再次在脚本中使用这个来验证在运行 CPU 密集任务之前是否有足够的核心可用。要从命令行测试这一点,我们可以在只有单个核心的 Raspberry Pi 上执行以下代码:
$ bash
$ CPU_CORES=$(grep -c name /proc/cpuinfo)
$ if (( CPU_CORES < 4 )) ; then
> echo "A minimum of 4 cores are required"
> exit 1
> fi
我们只在开始时运行 bash,以确保我们不会因为退出命令而退出系统。如果这是在脚本中,这将是不需要的,因为我们将退出脚本而不是我们的 shell 会话。
通过在 Model B 上运行此命令,我们可以看到脚本的结果,还可以看到我们没有所需数量的核心:
如果您需要在多个脚本中运行此检查,则可以在共享脚本中创建一个函数,并在需要进行检查的脚本中引用包含共享函数的脚本:
function check_cores {
[ -z $1 ] && REQ_CORES=2
CPU_CORES=$(grep -c name /proc/cpuinfo)
if (( CPU_CORES < REQ_CORES )) ; then
echo "A minimum of $REQ_CORES cores are required"
exit 1
fi
}
如果向函数传递了参数,则将其用作所需的核心数;否则,我们将默认值设置为2
。如果我们在 Model B Raspberry Pi 的 shell 中定义这个作为函数,并使用type
命令显示详细信息,我们应该会看到如下所示的情况:
如果我们在单核系统上运行,并且指定了只有单核的要求,我们会看到当我们满足要求时没有输出。如果我们没有指定要求,那么默认为2
个核心,我们将无法满足要求并退出 shell。
我们可以看到在使用参数1
运行函数时的输出,然后在没有参数的情况下运行的输出,如下面的屏幕截图所示:
我们可以看到即使是grep
的基础知识在脚本中也可以非常有用,以及我们可以利用所学知识开始创建可用的模块以添加到我们的脚本中。
解析 CSV 文件
我们现在将看一下创建一个解析或格式化 CSV 文件的脚本。文件的格式化将添加新行、制表符和颜色到输出中,以使其更可读。然后我们可以使用grep
来显示 CSV 文件中的单个项目。这里的实际应用是基于 CSV 文件的目录系统。
CSV 文件
CSV 文件或逗号分隔值列表将来自我们当前目录中的名为 tools 的文件。这是我们销售产品的目录。文件内容显示如下输出:
drill,99,5
hammer,10,50
brush,5,100
lamp,25,30
screwdriver,5,23
table-saw,1099,3
这只是一个简单的演示,所以我们不希望有太多数据,但目录中的每个项目都包括以下内容:
-
名称
-
价格
-
库存单位
我们可以看到我们有一把钻头,售价为 99 美元,我们有五个单位库存。如果我们使用cat
列出文件,它并不友好;但是我们可以编写一个脚本以更吸引人的方式显示数据。我们可以创建一个名为$HOME/bin/parsecsv.sh
的新脚本:
#!/bin/bash
OLDIFS="$IFS"
IFS=","
while read product price quantity
do
echo -e "\0331;33m$product \
========================\033[0m\n\
Price : \t $price \n\
Quantity : \t $quantity \n"
done <"$1"
IFS=$OLDIFS
让我们逐步进行这个文件,并查看相关的步骤:
元素 | 含义 |
---|---|
OLDIFS="$IFS" |
IFS 变量存储文件分隔符,通常是空格。我们可以存储旧的IFS ,以便在脚本结束时恢复它。确保一旦脚本完成,无论脚本如何运行,都能返回相同的环境。 |
我们将分隔符设置为逗号,以匹配 CSV 文件的需要。 | |
while read product price quantity |
我们进入一个while 循环以填充我们需要的三个变量:产品、价格和数量。while 循环将逐行读取输入文件,并填充每个变量。 |
echo … |
echo 命令以蓝色显示产品名称,并在其下方显示双下划线。其他变量将打印在新行上并进行制表。 |
done <"$1" |
这是我们读取输入文件的地方,我们将其作为脚本的参数传递。 |
该脚本显示在以下截图中:
我们可以使用以下命令在当前目录中执行工具目录文件的脚本:
$ parsecsv.sh tools
为了查看这将如何显示,我们可以查看以下截图的部分输出:
我们现在开始意识到我们在命令行上有很大的能力以更可读的方式格式化文件,而纯文本文件不需要是单调的。
隔离目录条目
如果我们需要搜索一个条目,那么我们需要不止一行。该条目占据了三行。因此,如果我们搜索锤子,我们需要转到锤子行和其后的两行。我们可以使用grep
的-A
选项来做到这一点。我们需要显示匹配的行和之后的两行。这将由以下代码表示:
$ parsecsv.sh tool | grep -A2 hammer
这在以下截图中显示:
使用正则表达式
到目前为止,我们一直将正则表达式(RE)用于简单的文本,但当然还有很多东西可以从中学到。尽管人们经常认为正则表达式看起来像是在蝙蝠侠打斗中可能看到的漫画书亵渎语,但它们确实有强大的含义。
使用替代拼写
首先,让我们看一下拼写上的一些异常。单词"color"可能会根据我们使用的是英式英语还是美式英语而拼写为"colour"或"color"。这可能会导致搜索"color"这个词时出现问题,因为它可能以两种方式拼写。实施以下命令将仅返回包含单词"color"的第一行,而不是第二行:
$ echo -e "color\ncolour" | grep color
如果我们需要返回两种拼写,那么我们可以使用一个RE
运算符。我们将使用?
运算符。您应该知道,在RE
中,?
运算符与 shell 中的不同。在RE
中,?
运算符表示前一个字符是可选的。当运行带有额外运算符的RE
时,我们可能需要运行grep -E
或egrep
以获得增强的 RE 引擎:
$ echo -e "color\ncolour" | grep -E 'colou?r'
我们可以通过快速查看以下截图来看到这一点:
有多少单词有四个连续的元音字母?
这位女士们先生们,这就是为什么 RE 如此重要,值得坚持。我们还可以想一些有趣的游戏或填字游戏求解器。我们对 RE 玩得越开心,使用起来就越容易。许多 Linux 系统包括一个位于/usr/share/dict/words
的字典文件,如果您的系统上存在这个文件,我们将使用它。
你能想到有四个连续元音字母的单词有多少?不确定的话,那就让我们用grep
和 RE 来搜索文件:
$ grep -E '[aeiou]{5}' /usr/share/dict/words
首先,您可以看到我们使用了方括号。这与 shell 中的含义相同,并且OR
分组字符,作为列表。结果搜索是字母a
或e
或i
或o
或u
。在括号末尾添加大括号启用了乘法器。在大括号中只有数字4
表示我们正在寻找四个连续的元音字母。
我们可以在以下截图中看到这一点:
这有多酷?现在我们永远不会有未完成的填字游戏了,也没有借口在 Scrabble 上输了。
RE 锚点
当使用clean_file
函数删除注释行和空行时,我们已经使用了 RE 锚点。^
或插入符号代表行的开头,$
代表行的结尾。如果我们想列出从字典文件开始的以ante
开头的单词,我们将编写以下查询:
$ grep '^ante' /usr/share/dict/words
结果应该显示 anteater,antelope,antenna 等。如果我们想查询以cord
结尾的单词,我们将使用:
$ grep 'cord$' /usr/share/dict/words
这将打印少量内容,并在我的系统上列出单词 accord,concord,cord,discord 和 record。
因此,即使这只是介绍了正则表达式的一小部分,我们也应该欣赏到我们可以从仅知道这么一点点中获得的东西。
理解 sed 的基础知识
在建立了一点基础之后,我们现在可以开始查看sed
的一些操作。这些命令将在大多数 Linux 系统中提供,并且是核心命令。
我们将直接深入一些简单的例子:
$ sed 'p' /etc/passwd
p
运算符将打印匹配的模式。在这种情况下,我们没有指定模式,所以我们将匹配所有内容。在不抑制STDOUT
的情况下打印匹配的行将重复行。这个操作的结果是将passwd
文件中的所有行都打印两次。要抑制STDOUT
,我们使用-n
选项:
$ sed -n 'p' /etc/passwd
太棒了!我们刚刚重新发明了cat
命令。现在我们可以专门处理一系列行:
$ sed -n '1,3 p ' /etc/passwd
现在我们已经重新发明了head
命令,但我们也可以在 RE 中指定范围来重新创建grep
命令:
$ sed -n '/^root/ p' /etc/passwd
我们可以在以下截图中看到这一点:
替换命令
我们已经看到了用于打印模式空间的p
命令。现在我们将看一下替换命令或s
。通过这个命令,我们可以用另一个字符串替换一个字符串。同样,默认情况下,我们将输出发送到STDOUT
,并且不编辑文件。
要替换用户pi
的默认 shell,我们可以使用以下命令:
sed -n ' /^pi/ s/bash/sh/p ' /etc/passwd
我们继续使用p
命令来打印匹配的模式,并使用-n
选项来抑制STDOUT
。我们搜索以pi
开头的行。这代表用户名。然后我们使用s
命令来替换这些匹配的行中的文本。这需要两个参数,第一个是要搜索的文本,第二个代表用于替换原始文本的文本。在这种情况下,我们搜索bash
并将其替换为sh
。这很简单,确实有效,但从长远来看可能不太可靠。我们可以在下面的截图中看到输出:
我们必须强调,目前我们并没有编辑文件,只是将其显示在屏幕上。原始的passwd
文件保持不变,我们可以以标准用户身份运行这个命令。我在前面的例子中提到,搜索可能不太可靠,因为我们要搜索的字符串是bash
。这个字符串非常短,也许它可以在匹配的行中的其他地方被包含。可能某人的姓氏是"Tabash",其中包含字符串bash
。我们可以扩展搜索以查找/bin/bash
并将其替换为/bin/sh
。然而,这引入了另一个问题,即默认分隔符是斜杠,所以我们必须转义我们在搜索和替换字符串中使用的每个斜杠,即:
sed -n ' /^pi/ s/\/bin\/bash/\/usr\/bin\/sh/p ' /etc/passwd
这是一个选择,但不是一个整洁的选择。更好的解决方案是知道我们使用的第一个分隔符定义了分隔符。换句话说,您可以使用任何字符作为分隔符。在这种情况下,使用@
符号可能是一个好主意,因为它既不出现在搜索字符串中,也不出现在替换字符串中:
sed -n ' /^pi/ s@/bin/bash@/usr/bin/sh@p ' /etc/passwd
现在我们有了一个更可靠的搜索和可读的命令行,这总是一件好事。我们只替换每行的第一个出现的/bin/bash
为/bin/sh
。如果我们需要替换不止第一个出现,我们在最后加上g
命令以进行全局替换:
sed -n ' /^pi/ s@bash@sh@pg ' /etc/passwd
在我们的情况下,这并不是必需的,但了解这一点是很好的。
编辑文件
如果我们想要编辑文件,我们可以使用-i
选项。我们需要有权限来处理文件,但我们可以复制文件以便处理,这样就不会损害任何系统文件或需要额外的访问权限。
我们可以将passwd
文件复制到本地:
$ cp /etc/passwd "$HOME"
$ cd
我们用cd
命令结束,以确保我们在家目录和本地passwd
文件中工作。
-i
选项用于进行原地更新。在编辑文件时,我们将不需要-n
选项或p
命令。因此,命令就像下面的例子一样简单:
$ sed -i ' /^pi/ s@/bin/bash@/bin/sh/ ' $HOME/passwd
命令不会有任何输出,但文件现在将反映出更改。下面的截图显示了命令的使用:
在进行更改之前,我们应该备份文件,直接在-i
选项后附加一个字符串,不加任何空格。这在下面的例子中显示:
$ sed -i.bak ' /^pi/ s@/bin/bash@/bin/sh/ ' $HOME/passwd
如果我们想要查看这个,我们可以反转搜索和替换字符串:
$ sed -i.bak ' /^pi/ s@/bin/sh@/bin/bash/ ' $HOME/passwd
这将使本地的passwd
文件与之前一样,并且我们将有一个passwd.bak
,其中包含之前的一系列更改。这样如果需要,我们就有了一个回滚选项,可以确保安全。
总结
这是另一个你牢牢掌握的伟大章节,我希望这对你真的很有用。虽然我们想集中使用sed
,但我们从grep
的强大之处开始,无论是在脚本内部还是外部。这使我们在查看sed
的可能性之前先了解了正则表达式。虽然我们只是初步接触了sed
,但我们将在下一章中开始扩展这一点,我们将扩展我们所学到的知识。这将以从当前配置中提取注释数据开始,取消注释并将其写入模板的形式进行。然后我们可以使用模板来创建新的虚拟主机。所有这些操作的工作马是sed
和sed
脚本。
第九章:自动化 Apache 虚拟主机
现在我们已经了解了一些流编辑器sed
,我们可以将这些知识付诸实践。在第八章中,介绍 sed,我们已经习惯了sed
的一些功能;然而,这只是编辑器中所包含的一小部分功能。在本章中,我们将更多地使用sed
,并且在使用我们的 bash 脚本时,暴露自己于工具的一些实际用途。
在这个过程中,我们将使用sed
来帮助我们自动创建基于名称的 Apache 虚拟主机。Apache 主机是我们演示的sed
的实际用户,但更重要的是,我们将使用sed
来搜索主配置中的选定行。然后我们将取消注释这些行并将它们保存为模板。创建了模板后,我们将从中创建新的配置。我们在 Apache 中演示的概念可以应用于许多不同的情况。
我们将发现,在我们的 shell 脚本中使用sed
将允许我们轻松地从主配置中提取模板数据,并根据虚拟主机的需要进行调整。通过这种方式,我们将能够扩展对sed
和 shell 脚本的知识。在本章中,我们将涵盖以下主题:
-
Apache HTTPD 虚拟主机
-
提取模板信息
-
自动创建主机
-
在主机创建过程中提示
基于名称的 Apache 虚拟主机
为了演示,我们将使用从 CentOS 6.6 主机中获取的 Apache 2.2 HTTPD 服务器的httpd.conf
文件。坦率地说,我们对配置文件更感兴趣,因为 Red Hat 或 CentOS 提供它,而不是我们将进行的实际配置更改。我们的目的是学习如何从系统提供的文件中提取数据并创建模板。我们可以将此应用于 Apache 配置文件或任何其他文本数据文件。这是方法论,我们不关注实际结果。
为了对我们要做的事情有一些了解,我们必须首先查看随 Enterprise Linux 6 一起提供的/etc/httpd/conf/httpd.conf
文件,即 CentOS、Red Hat Enterprise Linux 或 Scientific Linux。以下截图显示了我们感兴趣的文件的虚拟主机部分。
看着这些行,我们可以看到它们被注释了,这都是一个单一的httpd.conf
的一部分。在创建虚拟主机时,我们通常更喜欢为每个潜在的虚拟主机单独配置。我们需要能够从主文件中提取这些数据,并同时取消注释。然后我们可以将这些取消注释的数据保存为模板。
使用这个模板,我们将创建新的配置文件,代表我们需要在一个 Apache 实例上运行的不同命名的hosts
。这使我们能够在单个服务器上托管sales.example.com
和marketing.example.com
。销售和营销将拥有各自独立的配置和网站。此外,使用我们创建的模板也很容易添加我们需要的其他站点。主要的 Web 服务器的任务是读取传入的 HTTP 头请求,并根据使用的域名将其定向到正确的站点。
我们的第一个任务是提取在开放和关闭VirtualHost
标签之间的数据,取消注释并保存到模板中。这只需要做一次,不会成为创建虚拟主机的主要脚本的一部分。
创建虚拟主机模板
由于我们不打算测试我们创建的虚拟主机,我们将复制httpd.conf
文件并在本地家目录中使用。在开发脚本时,这是一个很好的做法,以免影响工作配置。我正在使用的httpd.conf
文件应该能够从发布者引用的其他脚本资源中下载。或者,您可以从安装了 Apache 的企业 Linux 6 主机上复制它。确保将httpd.conf
文件复制到您的家目录,并且您正在家目录中工作。
第一步
创建模板的第一步是隔离我们需要的行。在我们的情况下,这将是在之前的屏幕截图中看到的示例虚拟主机定义中包括的行。这包括VirtualHost
的开放和关闭标签以及中间的所有内容。我们可以使用行号来实现这一点;但是,这可能不太可靠,因为我们需要假设文件中的内容没有发生变化,行号才能保持一致。为了完整起见,我们将在转向更可靠的机制之前展示这一点。
首先,我们将回顾一下如何使用sed
打印整个文件。这很重要,因为在下一步中,我们将过滤显示并仅显示我们想要的行:
$ sed -n ' p ' httpd.conf
使用-n
选项来抑制标准输出,引号内的sed
命令是p
,用于显示模式匹配。由于我们在这里没有过滤任何内容,匹配的模式就是整个文件。如果我们要使用行号进行过滤,可以使用sed
轻松添加行号,如下命令所示:
$ sed = httpd.conf
从以下屏幕截图中,我们可以看到在这个系统中,我们需要处理的行是从1003
到1009
;但是,我再次强调,这些数字可能会因文件而异:
隔离行
要显示这些带有标签的行,我们可以在sed
中添加一个数字范围。通过将这些数字添加到sed
中,可以轻松实现这一点,如下命令所示:
$ sed -n '1003,1009 p ' httpd.conf
通过指定行范围,我们已经成功地隔离了我们需要的行,现在显示的只有虚拟主机定义的行。我们可以在以下屏幕截图中看到这一点,其中显示了命令和输出:
在硬编码行号时面临的问题是我们失去了灵活性。这些行号与这个文件相关,可能只与这个文件相关。我们将始终需要检查与我们正在处理的文件相关的文件中的正确行号。如果行不方便地位于文件的末尾,我们将不得不向后滚动以尝试找到正确的行号。为了克服这些问题,我们可以实现对开放和关闭标签的直接搜索,而不是使用行号。
$ sed -n '/^#<VirtualHost/,/^#<\/VirtualHost/p' httpd.conf
我们不再使用起始号码和结束号码,而是更可靠的起始正则表达式和结束正则表达式。开头的正则表达式寻找以#<VirtualHost
开头的行。结束的正则表达式正在寻找关闭标签。但是,我们需要用转义字符保护/VirtualHost
。通过查看结束的正则表达式,我们看到它转换为以#\/VirtualHost
开头的行,带有转义的斜杠。
注意
如果您还记得第八章中的内容,介绍 sed,我们可以使用插入符(^
)指定以指定字符开头的行。
通过查看以下屏幕截图,我们现在可以更可靠地隔离所需的行,而无需知道行号。这在编辑过的文件中更可取,这些文件的行号会有所不同:
sed 脚本文件
隔离行只是第一步!我们仍然需要取消注释这些行,然后将结果保存为模板。虽然我们可以将其写成一个单独的sed
命令字符串,但我们已经看到它会非常冗长,难以阅读和编辑。幸运的是,sed
命令确实有从输入文件(通常称为脚本)读取命令的选项。我们使用-f
选项与sed
一起指定要读取的文件作为我们的控制。有关sed
的所有选项的更多详细信息,请参阅主页。
我们已经看到我们可以正确地从文件中隔离出正确的行。因此,脚本的第一行配置了我们要处理的行。我们使用大括号{}
来定义所选行后面的代码块。代码块是我们想要在给定选择上运行的一个或多个命令。
在我们的情况下,第一个命令将是删除注释,第二个命令将是将模式空间写入新文件。sed
脚本应该如下例所示:
/^#<VirtualHost/,/^#<\/VirtualHost/ {
s/^#//
wtemplate.txt
}
我们可以将此文件保存为$HOME/vh.sed
。
在第一行,我们选择要处理的行,就像我们之前看到的那样,然后用左大括号打开代码块。在第 2 行,我们使用替换命令s
。这将查找以注释或#
开头的行。我们用空字符串替换注释。中间和结束的斜杠之间没有字符或空格。用英语来说,我们是在取消注释该行,但对于代码来说,这是用空字符串替换#
。代码的最后一行使用write
命令w
将其保存到template.txt
。为了帮助您看到这一点,我们已经包含了vh.sed
文件的以下截图:
现在我们可以看到我们所有的努力都得到了成果,只要确保我们在执行以下命令的httpd.conf
和vh.sed
文件所在的同一目录中:
$ sed -nf vh.sed httpd.conf
我们现在已经在我们的工作目录中创建了template.txt
文件。这是从httpd.conf
文件中隔离出的取消注释文本。简单来说,我们从数千行文本中提取了七行正确的文本,删除了注释,并将结果保存为新文件。template.txt
文件显示在以下截图中:
现在,我们有一个模板文件,可以开始使用它来创建虚拟主机定义。即使我们一直在看 Apache,取消注释文本或删除所选行的第一个字符的相同想法也可以适用于许多情况,因此将其视为sed
可以做什么的一个想法。
自动创建虚拟主机
创建了模板之后,我们现在可以使用它来创建虚拟主机配置。简单来说,我们需要将dummy-host.example.com
URL 替换为sales.example.com
或marketing.example.com
URL。当然,我们还需要创建DocumentRoot
目录,这是网页所在的目录,并添加一些基本内容。当我们使用脚本运行整个过程时,不会遗漏任何内容,每次编辑都将准确无误。脚本的基本内容如下:
#!/bin/bash
WEBDIR=/www/docs
CONFDIR=/etc/httpd/conf.d
TEMPLATE=$HOME/template.txt
[ -d $CONFDIR ] || mkdir -p $CONFDIR
sed s/dummy-host.example.com/$1/ $TEMPLATE > $CONFDIR/$1.conf
mkdir -p $WEBDIR/$1
echo "New site for $1" > $WEBDIR/$1/index.html
如果我们忽略第一行的 shebang,我们现在应该知道了。我们可以从脚本的第 2 行开始解释:
行 | 意思 |
---|---|
WEBDIR=/www/docs/ |
我们初始化WEDIR 变量,将其存储在将容纳不同网站的目录的路径中。 |
CONFDIR=/etc/httpd/conf.d |
我们初始化CONFDIR 变量,用于存储新创建的虚拟主机配置文件。 |
TEMPLATE=$HOME/template.txt |
我们初始化将用于模板的变量。这应该指向您的模板路径。 |
[ -d $CONFDIR ] || mkdir -p "$CONFDIR" |
在一个工作的 EL6 主机上,这个目录将存在并包含在主配置中。如果我们将其作为纯测试运行,那么我们可以创建一个目录来证明我们可以在目标目录中创建正确的配置。 |
sed s/dummy-host.example.com/$1/ $TEMPLATE >$CONFDIR/$1.conf |
sed 命令作为脚本中运行搜索和替换操作的引擎。使用sed 中的替换命令,我们搜索虚拟文本并用传递给脚本的参数替换它。 |
mkdir -p $WEBDIR/$1 |
在这里,我们创建正确的子目录来存放新虚拟主机的网站。 |
echo "New site for $1" > $WEBDIR/$1/index.html |
在最后一步中,我们为网站创建一个基本的临时页面。 |
我们可以将此脚本创建为$HOME/bin/vhost.sh
。如下截图所示。不要忘记添加执行权限:
要创建销售虚拟主机和网页,我们可以按照以下示例运行脚本。我们将直接以 root 用户身份运行脚本。或者,您也可以选择在脚本中使用sudo
命令:
# vhost.sh sales.example.com
现在我们可以看到,使用精心制作的脚本可以轻松创建虚拟主机。虚拟主机的配置文件将在/etc/httpd/conf.d/
目录中创建,并命名为sales.example.com.conf
。该文件将类似于以下截图:
网站内容必须已经创建在/www/docs/sales.example.com
目录中。这将是一个简单的临时页面,证明我们可以从脚本中做到这一点。使用以下命令,我们可以列出用于存放每个站点的内容或基本目录:
$ ls -R /www/docs
-R
选项允许递归列出。我们纯粹使用/www/docs
目录,因为这是我们提取的原始虚拟主机定义中设置的。如果在实际环境中工作,您可能更喜欢使用/var/www
或类似的内容,而不是在文件系统根目录创建新目录。编辑我们创建的模板将是一件简单的事情,也可以在模板创建时使用sed
完成。
在站点创建过程中提示数据
现在我们可以使用脚本来创建虚拟主机和内容,但除了虚拟主机名称之外,我们还没有允许任何定制。当然,这很重要。毕竟,正是这个虚拟主机名称在配置本身以及设置网站目录和配置文件名中使用。
我们可以允许在虚拟主机创建过程中指定附加选项。我们将使用sed
根据需要插入数据。sed
命令i
用于在选择之前插入数据,a
用于在选择之后追加数据。
在我们的示例中,我们将添加主机限制,只允许本地网络访问网站。我们更感兴趣的是将数据插入文件中,而不是我们在特定 HTTP 配置文件中所做的事情。在脚本中,我们将添加read
提示,并在配置中插入Directory
块。
为了尝试解释我们要做的事情,当执行脚本时,我们应该看到类似于以下内容。您可以从我们为营销站点创建的文本中看到这一点,并添加对谁可以访问站点的限制:
正如您所看到的,我们可以提出两个问题,但如果需要,可以添加更多问题以支持定制;其想法是,额外的定制应该像脚本创建一样准确可靠。您还可以选择用示例答案详细说明问题,以便用户知道网络地址应该如何格式化。
为了帮助脚本创建,我们将原始的vhost.sh
复制到vhost2.sh
。我们可以整理脚本中的一些项目,以便更容易扩展,然后添加额外的提示。新脚本将类似于以下代码:
#!/bin/bash
WEBDIR=/www/docs/$1
CONFDIR=/etc/httpd/conf.d
CONFFILE=$CONFDIR/$1.conf
TEMPLATE=$HOME/template.txt
[ -d $CONFDIR ] || mkdir -p $CONFDIR
sed s/dummy-host.example.com/$1/ $TEMPLATE > $CONFFILE
mkdir -p $WEBDIR
echo "New site for $1" > $WEBDIR/index.html
read -p "Do you want to restrict access to this site? y/n "
[ $REPLY = 'n' ] && exit 0
read -p "Which network should we restrict access to: " NETWORK
sed -i "/<\/VirtualHost>/i <Directory $WEBDIR >\
\n Order allow,deny\
\n Allow from 127.0.0.1\
\n Allow from $NETWORK\
\n</Directory>" $CONFFILE
注意
请注意,我们在脚本中没有运行太多检查。这是为了让我们专注于添加的元素而不是一个健壮的脚本。在您自己的环境中,一旦脚本按您希望的方式工作,您可能需要实施更多的检查以确保脚本的可靠性。
正如您所看到的,我们有更多的行。WEBDIR
变量已经调整为包含目录的完整路径,类似地,我们添加了一个新变量CONFFILE
,以便我们可以直接引用文件。如果对第一个提示的答案是n
,并且用户不需要额外的定制,脚本将退出。如果他们对“否”回答任何其他答案,脚本将继续并提示网络授予访问权限。然后我们可以使用sed
来编辑现有配置并插入新的目录块。这将默认拒绝访问,但允许localhost
和NETWORK
变量。我们在代码中将localhost
称为127.0.0.1
。
为了简化代码以便更好地理解,伪代码将如下例所示:
$ sed -i "/SearchText/i NewText <filename>
其中SearchText
代表我们要在其前插入文本的文件中的行。此外,NewText
代表将在SearchText
之前添加的新行或多行。直接跟在SearchText
后面的i
命令表示我们正在插入文本。使用a
命令进行追加意味着我们添加的文本将在SearchText
之后添加。
我们可以看到marketing.example.com
的结果配置文件,因为我们已经创建了它,并在以下截图中添加了额外的Directory块:
我们可以看到我们已经在关闭的VirtualHost
标签上方添加了新块。在脚本中,这是我们使用的SearchText
。我们添加的Directory块替换了伪代码中的NewText
。当我们看它时,它看起来更复杂,因为我们使用\n
嵌入了新行,并使用行继续字符\
格式化文件以便更容易阅读。再次强调,一旦脚本创建完成,这种编辑是容易和准确的。
为了完整起见,我们在以下截图中包括了脚本vhost2.sh
的截图:
总结
在本章中,我们已经看到了如何将sed
扩展到一些非常酷的脚本中,这些脚本使我们能够从文件中提取数据,取消注释选定的行并编写新的配置。我们还看到了如何使用sed
与脚本,将新行插入现有文件中。我认为sed
很快就会成为您的朋友,我们已经创建了一些强大的脚本来支持学习体验。
你可能已经知道这一点,但sed
有一个大哥awk
。在下一章中,我们将看到如何使用awk
从文件中提取数据。
第十章:awk 基础知识
流编辑器并不孤单,它还有一个大哥 awk。在本章中,我们将介绍 awk 的基础知识,并看到 awk 编程语言的强大之处。我们将了解为什么我们需要和喜爱 awk,以及在开始在接下来的两章中实际使用 awk 之前,我们如何利用一些基本功能。在这个过程中,我们将涵盖以下主题:
-
从文件中过滤内容
-
格式化输出
-
显示
/etc/passwd
中的非系统用户 -
使用
awk
控制文件
awk 背后的历史
awk
命令是 Unix 和 Linux 命令套件中的主要组成部分。Unix 命令awk
最早是在 20 世纪 70 年代由贝尔实验室开发的,它的名字取自主要作者的姓氏:Alfred Aho,Peter Weinberger 和 Brian Kernighan。awk
命令允许访问 awk 编程语言,该语言旨在处理文本流中的数据。
为了演示awk
提供的编程语言,我们应该创建一个hello world
程序。我们知道这对于所有语言来说都是强制性的:
$ awk 'BEGIN { print "Hello World!" }'
我们不仅可以看到这段代码将打印无处不在的 hello 消息,还可以使用BEGIN
块生成头信息。稍后,我们将看到我们可以通过END
代码块创建摘要信息,从而允许主代码块。
我们可以在以下截图中看到这个基本命令的输出:
显示和过滤文件中的内容
当然,我们都希望能够打印比Hello World更多的内容。awk
命令可以用来过滤文件中的内容,如果需要的话,还可以处理非常大的文件。我们应该先打印完整的文件,然后再进行过滤。这样,我们就可以感受到命令的语法。稍后,我们将看到如何将此控制信息添加到awk
文件中,以便简化命令行。使用以下命令,我们将打印/etc/passwd
文件中的所有行:
$ awk ' { print } ' /etc/passwd
这相当于使用print
语句的$0
变量:
$ awk ' { print $0 }' /etc/passwd
$0
变量指的是完整的行。如果print
命令没有提供参数,我们假设要打印整行。如果我们只想打印/etc/passwd
文件中的第一个字段,我们可以使用$1
变量。但是,我们需要指定在该文件中使用的字段分隔符是冒号。awk
的默认分隔符是空格或任意数量的空格或制表符和换行符。有两种方法可以指定输入分隔符;这些方法在以下示例中显示。
第一个示例很容易且简单易用。-F
选项特别适用,特别是在我们不需要任何额外的头信息时:
$ awk -F":" '{ print $1 }' /etc/passwd
我们也可以在BEGIN
块中执行此操作;当我们想要使用BEGIN
块显示头信息时,这是很有用的:
$ awk ' BEGIN { FS=":" } { print $1 } ' /etc/passwd
我们可以在前面的示例中清楚地看到这一点,我们将其命名为BEGIN
块,其中的所有代码都被大括号括起来。主块没有名称,并且被大括号括起来。
在看到BEGIN
块和主代码块之后,我们现在将看一下END
代码块。这通常用于显示摘要数据。例如,如果我们想要打印passwd
文件中的总行数,我们可以利用END
块。具有BEGIN
和END
块的代码只处理一次,而主块则对每一行进行处理。以下示例将添加到我们迄今为止编写的代码中,以包括总行数:
$ awk ' BEGIN { FS=":" } { print $1 } END { print NR } ' /etc/passwd
awk
内部变量NR
维护了处理的行数。如果需要,我们可以为此添加一些附加文本。这可以用于注释摘要数据。我们还可以利用 awk 语言中使用的单引号;它们允许我们将代码跨多行展开。一旦我们打开了单引号,我们就可以在命令行中添加新行,直到我们关闭引号。这在下一个示例中得到了展示,我们扩展了摘要信息:
$ awk ' BEGIN { FS=":" }
> { print $1 }
> END { print "Total:",NR } ' /etc/passwd
如果我们不想在这里结束我们的 awk 体验,我们可以轻松地显示每行的运行行数以及最终总数。这在下面的例子中得到了展示:
$ awk ' BEGIN { FS=":" }
> { print NR,$1 }
> END { print "Total:",NR } ' /etc/passwd
以下截图捕获了这个命令和部分输出:
在第一个使用BEGIN
的示例中,我们看到没有理由不能单独使用END
代码块而不使用主代码块。如果我们需要模拟wc -l
命令,我们可以使用以下awk
语句:
$ awk ' END { print NR }' /etc/passwd
输出将是文件的行数。以下截图显示了awk
命令和wc
命令用于计算/etc/passwd
文件中的行数:
值得注意的是,我们可以看到输出确实符合 28 行,我们的代码也起作用了。
我们还可以练习的另一个功能是仅处理选定的行。例如,如果我们只想打印前五行,我们将使用以下语句:
$ awk ' NR < 6 ' /etc/passwd
如果我们想打印第8
到12
行,我们可以使用以下代码:
$ awk ' NR==8,NR==12 ' /etc/passwd
我们还可以使用正则表达式来匹配行中的文本。看看下面的例子,我们查看以单词 bash 结尾的行:
$ awk ' /bash$/ ' /etc/passwd
示例和输出如下截图所示:
格式化输出
到目前为止,我们一直忠于print
命令,因为我们对输出的要求有限。如果我们想要打印出用户名、UID 和默认 shell,我们需要开始对输出进行一些格式化。在这种情况下,我们可以将输出组织成形状良好的列。没有格式化的话,我们使用的命令会类似于以下示例,其中我们使用逗号来分隔要打印的字段:
$ awk ' BEGIN { FS=":" } { print $1,$3,$7 } ' /etc/passwd
我们在这里使用BEGIN
块,因为我们可以利用它稍后打印列标题。
为了更好地理解问题,我们可以看一下下面的截图,它说明了不均匀的列宽:
我们输出中的问题是列不对齐,因为用户名的长度不一致。为了改进这一点,我们可以使用printf
函数,其中我们可以指定列宽。awk
语句的语法将类似于以下示例:
$ awk ' BEGIN { FS=":" }
> { printf "%10s %4d %17s\n",$1,$3,$7 } ' /etc/passwd
printf
格式化包含在双引号内。我们还需要用\n
包括换行符。printf
函数不会自动添加新行,而print
函数会。我们打印三个字段;第一个接受字符串值,并设置为10
个字符宽。中间字段最多接受 4 个数字,最后是默认 shell 字段,我们允许最多17
个字符串字符。
以下截图显示了如何改进输出:
我们可以通过添加标题信息进一步增强这一点。尽管在这个阶段代码开始看起来凌乱,但我们稍后将看到如何使用 awk 控制文件解决这个问题。下面的例子显示了标题信息被添加到Begin
块中。分号用于分隔BEGIN
块中的两个语句:
$ awk 'BEGIN {FS=":" ; printf "%10s %4s %17s\n",""Name","UID","Shell"}
> { printf "%10s %4d %17s\n",$1,$3,$7 } ' /etc/passwd
在下面的截图中,我们可以看到这如何进一步改进了输出:
在上一章中,我们看到如何在 shell 中使用颜色来增强输出。我们也可以通过在 awk 中添加自己的函数来使用颜色。在下面的代码示例中,您将看到 awk 允许我们定义自己的函数来促进更复杂的操作并隔离代码。我们现在将修改以前的代码以在标题中包含绿色输出:
$ awk 'function green(s) {
> printf "\0331;32m" s "\033[0m\n"
> }
> BEGIN {FS=":" ; green(" Name: UID: Shell:"}
> { printf "%10s %4d %17s\n",$1,$3,$7 } ' /etc/passwd
在awk
中创建函数允许我们在需要的地方添加颜色,这种情况下是绿色文本。很容易创建函数来定义其他颜色。代码和输出包含在以下截图中:
![格式化输出
进一步过滤以显示 UID 用户
我们已经能够逐步建立我们的 awk 技能,我们学到的东西都很有用。我们可以将这些小步骤添加起来,开始创建一些更有用的东西。也许,我们想要只打印标准用户;这些通常是高于 500 或 1000 的用户,具体取决于您的特定发行版。
在我为本书使用的 Raspbian 发行版中,标准用户的 UID 从 1000 开始。UID 是第三个字段。这实际上只是简单地使用第三个字段的值作为范围运算符。我们可以在以下示例中看到这一点:
$ awk -F":" '$3 > 999 ' /etc/passwd
我们可以使用以下命令显示 UID 为 101 的用户:
$ awk -F":" '$3 < 101 ' /etc/passwd
这只是让您了解 awk 的一些可能性。事实上,我们可以整天玩我们的算术比较运算符。
我们还看到,有些示例中,awk
语句变得有点长。这就是我们可以实现awk
控制文件的地方。在我们陷入语法混乱之前,让我们立即看看这些。
Awk 控制文件
就像sed
一样,我们可以通过创建和包含控制文件来简化命令行。这也使得以后编辑命令更容易实现。控制文件包含我们希望awk
执行的所有语句。我们在使用sed
、awk
和 shell 脚本时必须考虑的主要问题是模块化;创建可重用的元素,以隔离和重用代码。这样可以节省我们的时间和工作,并且我们有更多时间用于我们喜欢的任务。
要查看awk
控制文件的示例,我们应该重新访问passwd
文件的格式。创建以下文件将封装awk
语句:
function green(s) {
printf "\033[1;32m" s "\033[0m\n"
}
BEGIN {
FS=":"
green(" Name: UID: Shell:")
}
{
printf "%10s %4d %17s\n",$1,$3,$7
}
我们可以将此文件保存为passwd.awk
。
能够将所有的awk
语句都包含在一个文件中非常方便,执行变得干净整洁:
$ awk -f passwd.awk /etc/passwd
这肯定鼓励更复杂的awk
语句,并允许您为代码扩展更多功能。
总结
我希望您对可以使用 awk 工具有更好和更清晰的理解。这是一个数据处理工具,逐行运行文本文件并处理您添加的代码。如果已添加,主要块将针对符合行条件的每一行运行。而BEGIN
和END
块代码只执行一次。
在接下来的两章中,我们将继续使用 awk,并举一些 awk 在现实生活中的实际示例。
第十一章:使用 Awk 总结日志
awk 真正擅长的任务之一是从日志文件中过滤数据。这些日志文件可能有很多行,可能有 250,000 行或更多。我曾处理过超过一百万行的数据。Awk 可以快速有效地处理这些行。例如,我们将使用包含 30,000 行的 Web 服务器访问日志文件,以展示 awk 代码的有效性和良好编写。在本章中,我们将涵盖以下主题:
-
HTTPD 日志文件格式
-
显示来自 Web 服务器日志的数据
-
总结 HTTP 访问代码
-
显示排名最高的客户端 IP 地址
-
列出浏览器数据
-
处理电子邮件日志
HTTPD 日志文件格式
在处理任何文件时,第一项任务是熟悉文件模式。简单来说,我们需要知道每个字段代表什么,以及用于分隔字段的内容。我们将使用 Apache HTTPD Web 服务器的访问日志文件。日志文件的位置可以从httpd.conf
文件中控制。基于 Debian 的系统上,默认的日志文件位置是/var/log/apache2/access.log
;其他系统可能使用apache2
目录代替httpd
。
为了演示文件的布局,我在 Ubuntu 15.10 系统上安装了一个全新的 Apache2 实例。安装完 Web 服务器后,我们从本地主机的 Firefox 浏览器进行了一次访问。
使用tail
命令可以显示日志文件的内容。尽管公平地说,使用cat
也可以,因为它只有几行:
# tail /var/log/apache2/access.log
命令的输出和文件的内容如下截图所示:
命令的输出会有一些换行,但我们可以感受到日志的布局。我们还可以看到,尽管我们认为只访问了一个网页,但实际上我们访问了两个项目:index.html
和ubuntu-logo.png
。我们还未能访问favicon.ico
文件。我们可以看到该文件是以空格分隔的。每个字段的含义在以下表格中列出:
字段 | 目的 |
---|---|
1 | 客户端 IP 地址。 |
2 | RFC 1413 和identd 客户端定义的客户端身份。除非启用IdentityCheck ,否则不会读取此内容。如果未读取,该值将带有连字符。 |
3 | 如果启用了用户身份验证,则为用户身份验证的用户 ID。如果未启用身份验证,则该值将为连字符。 |
4 | 请求的日期和时间格式为day/month/year:hour:minute:second offset 。 |
5 | 实际请求和方法。 |
6 | 返回状态代码,如 200 或 404。 |
7 | 文件大小(以字节为单位)。 |
即使这些字段是由 Apache 定义的,我们也必须小心。时间、日期和时区是一个字段,并且在方括号内定义;然而,在该数据和时区之间的字段内有额外的空格。为了确保在需要时打印完整的时间字段,我们需要同时打印$4
和$5
。这在以下命令示例中显示:
# awk ' { print $4,$5 } ' /var/log/apache2/access.log
我们可以在以下截图中查看命令和其产生的输出:
显示来自 Web 日志的数据
我们已经预览了如何使用 awk 查看 Apache Web 服务器的日志文件;但是,现在我们将转向我们的演示文件,其中包含更丰富和更多样化的内容。
按日期选择条目
看到我们如何显示日期后,也许我们应该看看如何仅打印一天的条目。为此,我们可以在awk
中使用匹配运算符。如果您愿意,这由波浪线表示。由于我们只需要日期元素,因此我们不需要同时使用日期和时区字段。以下命令显示了如何打印 2014 年 9 月 10 日的条目:
$ awk ' ( $4 ~ /10\/Sep\/2014/ ) ' access.log
为了完整起见,以下是该命令和部分输出的截图:
圆括号或括号包含我们正在寻找的行范围,我们已经省略了主块,这确保我们打印出范围内的完整匹配行。没有什么能阻止我们进一步过滤匹配行中要打印的字段。例如,如果我们只想打印正在用于访问 Web 服务器的客户端 IP 地址,我们可以打印字段1
。这在以下命令示例中显示。
$ awk ' ( $4 ~ /10\/Sep\/2014/ ) { print $1 } ' access.log
如果我们想要能够打印给定日期的总访问次数,我们可以将条目通过管道传递到wc
命令。这在以下示例中演示:
$ awk ' ( $4 ~ /10\/Sep\/2014/ ) { print $1 } ' access.log | wc -l
然而,如果我们想要使用awk
来为我们做这个,这将比启动一个新进程更有效,并且我们可以计算条目。如果我们使用内置变量NR
,我们可以打印文件中的整行而不仅仅是范围内的行。最好在主块中递增我们自己的变量,而不是为每行匹配范围。END
块可以被实现以打印我们使用的count
变量。以下命令行充当示例:
$ awk ' ( $4 ~ /10\/Sep\/2014/ ) { print $1; COUNT++ } END { print COUNT }' access.log
从wc
和内部计数器的计数输出将使我们从演示文件中得到16205
的结果。如果我们想要计数而不做其他操作,我们应该在主块中使用变量增量。
$ awk ' ( $4 ~ /10\/Sep\/2014/ ) { COUNT++ } END { print COUNT }' access.log
我们可以在以下输出中看到这一点:
总结 404 错误
请求页面的状态代码显示在日志的字段9
中。404
状态将表示服务器上找不到页面的错误,我相信我们都在某个阶段在我们的浏览器中看到过这个。这可能表明您网站上的链接配置错误,或者只是由浏览器搜索要在选项卡式浏览器中显示的图标图像而产生的。您还可以通过寻找标准页面的请求来识别对您网站的潜在威胁,这些页面可能会提供对 PHP 驱动站点的其他信息的访问,例如 WordPress。
首先,我们可以仅打印请求的状态:
$ awk '{ print $9 } ' access.log
现在我们可以稍微扩展代码,也可以扩展自己,只打印404
错误:
$ awk ' ( $9 ~ /404/ ) { print $9 } ' access.log
这在以下代码中显示:
我们可以进一步扩展,通过打印状态代码和正在访问的页面来打印。这将需要我们打印字段9
和字段7
。简而言之,这将如下所示:
$ awk ' ( $9 ~ /404/ ) { print $9, $7 } ' access.log
这些失败的访问页面中许多将是重复的。为了总结这些记录,我们可以使用sort
和uniq
命令的命令管道来实现这一点:
$ awk ' ( $9 ~ /404/ ) { print $9, $7 } ' access.log | sort | uniq
要使用uniq
命令,数据必须经过预排序;因此,我们使用sort
命令来准备数据。
总结 HTTP 访问代码
现在是时候离开纯命令行并开始使用 awk 控制文件了。与以往一样,当所需结果集的复杂性增加时,我们看到awk
代码的复杂性也在增加。我们将在当前目录中创建一个status.awk
文件。该文件应该类似于以下文件:
{ record[$9]++ }
END {
for (r in record)
print r, " has occurred ", record[r], " times." }
首先,我们将简化主代码块,这非常简单和稀疏。这是一种简单的方法来计算每个状态代码的唯一发生次数。我们不使用简单的变量,而是将其输入到数组中。这种情况下的数组称为记录。数组是一个多值变量,数组中的槽称为键。因此,我们将在数组中存储一组变量。例如,我们期望看到record[200]
和record[404]
的条目。我们用它们的发生次数填充每个键。每次我们找到404
代码时,我们增加存储在相关键中的计数:
{ record[$9]++ }
在END
块中,我们使用for
循环创建摘要信息,以打印数组中的每个键和值:
END {
for (r in record)
print r, " has occurred ", record[r], " times." }
要运行这个,相关的命令行将类似于以下内容:
$ awk -f status.awk access.log
要查看命令和输出,我们已经包含了以下截图:
我们可以进一步关注404
错误。当然,你可以选择任何状态代码。从结果中我们可以看到有4382
个404
状态代码。为了总结这些404
代码,我们将status.awk
复制到一个名为404.awk
的新文件中。我们可以编辑404.awk
,添加一个if
语句,只处理404
代码。文件应该类似于以下代码:
{ if ( $9 == "404" )
record[$9,$7]++ }
END {
for (r in record)
print r, " has occurred ", record[r], " times." }
如果我们用以下命令执行代码:
$ awk -f 404.awk access.log
输出将类似于以下截图:
显示最高排名的 IP 地址
现在你应该意识到awk
的一些功能,以及语言结构本身的强大之处。我们能够从这个 3 万行的文件中产生的数据是非常强大且容易提取的。我们只需要用$1
替换之前使用过的字段。这个字段代表客户端 IP 地址。如果我们使用以下代码,我们将能够打印每个 IP 地址以及它被用来访问网页服务器的次数:
{ ip[$1]++ }
END {
for (i in ip)
print i, " has accessed the server ", ip[i], " times." }
我们希望能够扩展这个功能,只显示 IP 地址中排名最高的,即访问网站最频繁的地址。工作主要在END
块中进行,将利用与当前最高排名地址的比较。可以创建以下文件并保存为ip.awk
:
{ ip[$1]++ }
END {
for (i in ip)
if ( max < ip[i] ) {
max = ip[i]
maxnumber = i }
print i, " has accessed ", ip[i], " times." }
我们可以在以下截图中看到命令的输出。客户端 IP 地址的部分已被隐藏,因为它来自我的公共网页服务器:
代码的功能来自END
块内部。进入END
块时,我们进入一个for
循环。我们遍历ip
数组中的每个条目。我们使用条件if
语句来查看我们正在遍历的当前值是否高于当前最大值。如果是,这将成为新的最高条目。当循环
结束时,我们打印具有最高条目的 IP 地址。
显示浏览器数据
用于访问网站的浏览器包含在字段12
的日志文件中。显示用于访问您网站的浏览器列表可能会很有趣。以下代码将帮助您显示报告的浏览器的访问列表:
{ browser[$12]++ }
END {
for ( b in browser )
print b, " has accessed ", browser[b], " times."
}
你可以看到我们如何可以创建awk
的小插件,并调整字段和数组名称以适应你自己的喜好。输出如下截图所示:
有趣的是,我们看到 Mozilla 4 和 5 占据了大部分请求客户端。我们看到 Mozilla 4 在这里列出了1713次。这里的 Mozilla/5.0 条目格式不正确,多了一个双引号。它稍后出现了 27K 次。
处理电子邮件日志
我们已经使用了来自 Apache HTTP Web 服务器的日志。事实是我们可以将相同的理念和方法应用到任何日志文件上。我们将查看 Postfix 邮件日志。邮件日志保存了来自 SMTP 服务器的所有活动,然后我们可以看到谁向谁发送了电子邮件。日志文件通常位于/var/log/mail.log
。我将在我的 Ubuntu 15.10 服务器上访问这个文件,该服务器具有本地电子邮件传递功能。这意味着 STMP 服务器只监听127.0.0.1
的本地接口。
日志格式将根据消息类型的不同而略有变化。例如,$7
将包含出站消息的from
日志,而入站消息将包含to
。
如果我们想列出所有发送到 SMTP 服务器的入站消息,我们可以使用以下命令:
# awk ' ( $7 ~ /^to/ ) ' /var/log/mail.log
由于字符串to
非常短,我们可以通过确保字段以^
开头来为其添加标识。命令和输出如下截图所示:
将扩展to
或from
搜索以包括用户名称将会很容易。我们可以看到交付或接收邮件的格式。使用与 Apache 日志相同的模板,我们可以轻松显示最高的收件人或发件人。
总结
现在我们在文本处理中有了一些重要的武器,我们可以开始理解awk
有多么强大。使用真实数据在评估我们搜索的性能和准确性方面特别有用。在新安装的 Ubuntu 15.10 Apache Web 服务器上开始使用简单的 Apache 条目后,我们很快就迁移到了来自实时 Web 服务器的更大的样本数据。有 30,000 行,这个文件给了我们一些真实的数据来处理,我们很快就能够生成可信的报告。我们结束了返回 Ubuntu 15.10 服务器来分析 Postfix SMTP 日志。我们可以看到我们可以非常轻松地将之前使用过的技术拖放到新的日志文件中。
接下来,我们继续使用awk
,看看如何报告 lastlog 数据和平面 XML 文件。
第十二章:使用 Awk 改进 lastlog
我们已经在第十一章中看到了如何从纯文本文件中挖掘大量数据并创建复杂报告。同样,我们可以使用标准命令行工具的输出来创建广泛的报告,比如lastlog
工具。lastlog
本身可以报告所有用户的最后登录时间。然而,我们可能希望过滤lastlog
的输出。也许您需要排除从未用于登录系统的用户帐户。也可能不相关报告root
,因为该帐户可能主要用于sudo
,而不用于记录标准登录。
在本章中,我们将同时使用lastlog
和 XML 数据格式化。由于这是我们调查 awk 的最后一章,我们将配置记录分隔符。我们已经看到了 awk 中字段分隔符的使用,但我们可以将默认记录分隔符从换行符更改为更符合我们需求的内容。具体来说,在本章中我们将涵盖:
-
使用 awk 范围来排除数据
-
基于行中字段数量的条件
-
操作 awk 记录分隔符以报告 XML 数据
使用 awk 范围来排除数据
到目前为止,在本书中,我们主要关注包括sed
或awk
的范围内的数据。使用这两个工具,我们可以否定范围,以便排除指定的行。这符合我们一直使用lastlog
输出的需求。这将打印出所有用户的登录数据,包括从未登录的帐户。这些从未登录的帐户可能是服务帐户或尚未登录系统的新用户。
lastlog 命令
如果我们查看lastlog
的输出,当它没有任何选项时,我们可以开始理解问题。从命令行,我们以标准用户身份执行命令。没有必要以 root 帐户运行它。命令如下示例所示:
$ lastlog
部分输出如下截图所示:
即使从这有限的输出中,我们可以看到由于从未登录的帐户创建的虚拟噪音而产生的混乱输出。使用lastlog
选项可能在一定程度上缓解这一问题,但可能并不能完全解决问题。为了证明这一点,我们可以向lastlog
添加一个选项,只包括通常由标准帐户使用的用户帐户。这可能因系统而异,但在我使用的样本 CentOS 6 主机上,第一个用户将是 UID 500。
如果我们使用lastlog -u 500-5000
命令,我们将只打印 UID 在此范围内的用户的数据。在简单的演示系统中,我们只有三个用户帐户的输出是可以接受的。然而,我们可以理解到我们可能仍然有一些混乱,因为这些帐户尚未被使用。如下截图所示:
除了从从未登录帐户打印出的多余数据之外,我们可能只对用户名和最新字段感兴趣。这是支持使用 awk 作为数据过滤器的另一个原因。通过这种方式,我们可以提供水平和垂直数据过滤,行和列。
使用 awk 进行水平过滤行
为了使用 awk 提供这种过滤,我们将把数据从lastlog
直接传输到awk
。我们将首先使用一个简单的控制文件来提供水平过滤或减少我们看到的行。首先,命令管道将如下命令示例一样简单:
$ lastlog | awk -f lastlog.awk
当然,复杂性是从命令行中抽象出来的,并隐藏在我们使用的控制文件中。最初,控制文件保持简单,读起来如下:
!(/Never logged in/ || /^Username/ || /^root/) {
print $0;
}
范围设置与我们之前看到的一样,并在主代码块之前。在括号前使用感叹号可以否定或颠倒所选范围。双竖线作为逻辑OR
。我们不包括包含Never logged in
的行,也不包括以Username
开头的行。这将移除lastlog
打印的标题行。最后,我们排除 root 账户的显示。这初始化了我们要处理的行,主代码块将打印这些行。
匹配行的计数
我们可能还想计算过滤返回的行数。例如,使用内部的NR
变量将显示所有行而不仅仅是匹配的行;为了能够报告已登录用户的数量,我们必须使用我们自己的变量。以下代码将在我们命名为cnt
的变量中维护计数。我们使用 C 风格的++
来增加主代码块的每次迭代。我们可以使用END
代码块来显示这个变量的最终值:
!(/Never logged in/ || /^Username/ || /^root/) {
cnt++
print $0;
}
END {
print "========================"
print "Total Number of Users Processed: ", cnt
}
我们可以从以下代码和输出中看到这在我的系统上是如何显示的:
从显示输出中,我们现在可以看到我们只显示已登录的用户,这种情况下只有一个用户。然而,我们可能还决定要进一步抽象数据,并且只显示匹配行中的某些字段。这应该是一个简单的任务,但它很复杂,因为字段的数量将取决于登录的方式。
基于字段数量的条件
如果用户直接登录到服务器的物理控制台,而不是通过远程或图形伪终端登录,那么lastlog
输出将不会显示主机字段。为了证明这一点,我直接登录到我的 CentOS 主机的tty1
控制台,并避免了图形界面。之前 awk 控制文件的输出显示我们现在有用户tux和bob;然而bob缺少主机字段,因为他连接到控制台。
虽然这本身不是问题,但如果我们想要过滤字段,两行的字段编号将有所不同,因为某些行中省略了字段。对于lastlog
,大多数连接将有9
个字段,而直接连接到服务器控制台的连接只有8
个字段。应用程序的要求是打印用户名和日期,但不打印最后登录的时间。我们还将在BEGIN
块中打印我们自己的标题。为了确保我们使用正确的位置,我们需要使用NF
内部变量来计算每行的字段数。
对于有8
个字段的行,我们想要打印字段1
、4
、5
和8
;对于有额外主机信息的较长行,我们将使用字段1
、5
、6
和9
。我们还将使用printf
来正确对齐列数据。控制文件应该被编辑,如下例所示:
BEGIN {
printf "%8s %11s\n","Username","Login date"
print "===================="
}
!(/Never logged in/ || /^Username/ || /^root/) {
cnt++
if ( NF == 8 )
printf "%8s %2s %3s %4s\n", $1,$5,$4,$8
else
printf "%8s %2s %3s %4s\n", $1,$6,$5,$9
}
END {
print "===================="
print "Total Number of Users Processed: ", cnt
}
我们可以在以下截图中看到命令和它产生的输出。我们可以看到如何基于我们想要关注的信息创建更合适的显示:
如果我们看一下输出,我选择在月份之前显示日期,这样我们就不按数字顺序显示字段。当然,这是个人选择,可以根据你认为数据应该如何显示进行自定义。
我们可以使用lastlog
控制文件中所见原则的输出来过滤任何命令的输出,并且你应该练习使用你想要过滤数据的命令。
操纵 awk 记录分隔符以报告 XML 数据
到目前为止,虽然我们一直在使用 awk,但我们只限于处理单独的行,每一行代表一个新记录。虽然这通常是我们想要的,当我们处理带有标记数据的情况时,比如 XML,其中一个单独的记录可能跨越多行。在这种情况下,我们可能需要设置RS
或record
分隔符内部变量。
Apache 虚拟主机
在第九章中,自动化 Apache 虚拟主机,我们使用了Apache 虚拟主机。这使用了定义每个虚拟主机的开始和结束的标记数据。即使我们更喜欢将每个虚拟主机存储在自己的文件中,它们也可以合并到单个文件中。考虑以下文件,它存储了可能的虚拟主机定义,可以存储为virtualhost.conf
文件,如下所示:
<VirtualHost *:80>
DocumentRoot /www/example
ServerName www.example.org
# Other directives here
</VirtualHost>
<VirtualHost *:80>
DocumentRoot /www/theurbanpenguin
ServerName www.theurbanpenguin.com
# Other directives here
</VirtualHost>
<VirtualHost *:80>
DocumentRoot /www/packt
ServerName www.packtpub.com
# Other directives here
</VirtualHost>
我们在单个文件中有三个虚拟主机。每个记录由一个空行分隔,这意味着我们有两个逻辑上分隔每个条目的新行字符。我们通过设置RS
变量来告诉 awk 这一点:RS="\n\n"
。有了这个设置,我们就可以打印所需的虚拟主机记录。这将在控制文件的BEGIN
代码块中设置。
我们还需要动态搜索命令行以获取所需的主机配置。我们将这构建到控制文件中。控制文件应该类似于以下代码:
BEGIN { RS="\n\n" ; }
$0 ~ search { print }
BEGIN
块设置变量,然后我们进入范围。范围设置为记录($0
)匹配(~
)search
变量。我们必须在执行awk
时设置变量。以下命令演示了命令行执行,控制文件和配置文件位于我们的工作目录中:
$ awk -f vh.awk search=packt virtualhost.conf
通过查看以下屏幕截图中生成的命令和输出,我们可以更清楚地看到这一点:
XML 目录
我们可以进一步扩展到 XML 文件,其中我们可能不想显示完整的记录,而只是某些字段。如果我们考虑以下产品目录
:
<product>
<name>drill</name>
<price>99</price>
<stock>5</stock>
</product>
<product>
<name>hammer</name>
<price>10</price>
<stock>50</stock>
</product>
<product>
<name>screwdriver</name>
<price>5</price>
<stock>51</stock>
</product>
<product>
<name>table saw</name>
<price>1099.99</price>
<stock>5</stock>
</product>
逻辑上,每个记录都与之前的空行分隔。每个字段都更详细,我们需要使用分隔符FS="[><]"
。我们将开头或结尾的尖括号定义为字段分隔符。
为了帮助分析这一点,我们可以打印单个记录如下:
<product><name>top</name><price>9</price><stock>5</stock></product>
每个尖括号都是一个字段分隔符,这意味着我们将有一些空字段。我们可以将这行重写为 CSV 文件:
,product,,name,top,/name,,price,9,/price,,stock,5,/stock,,/product,
我们只需用逗号替换每个尖括号,这样我们更容易阅读。我们可以看到字段5
的内容是top
值。
当然,我们不会编辑 XML 文件,我们会保留它的 XML 格式。这里的转换只是为了突出字段分隔符的读取方式。
我们用于从 XML 文件中提取数据的控制文件在以下代码示例中说明:
BEGIN { FS="[><]"; RS="\n\n" ; OFS=""; }
$0 ~ search { print $4 ": " $5, $8 ": " $9, $12 ": " $13 }
在BEGIN
代码块中,我们设置了FS
和RS
变量,正如我们讨论过的。我们还将OFS
或输出字段分隔符设置为一个空格。这样,当我们打印字段时,我们用空格分隔值,而不是保留尖括号。这个范围使用了与我们之前查看虚拟主机时使用的相同匹配。
如果我们需要在目录
中搜索产品drill
,我们可以使用以下示例中列出的命令:
$ awk -f catalog.awk search=drill catalog.xml
以下屏幕截图详细显示了输出:
我们现在已经能够从一个相当混乱的 XML 文件中创建可读的报告。awk 的强大再次得到了突出,并且对我们来说,这是本书中的最后一次。到目前为止,我希望你也能开始经常使用它。
总结
我们已经有了三个章节,在这些章节中我们使用了 awk。从第十章开始,Awk 基础,我们变得更加熟悉。在第十一章中,使用 Awk 总结日志以及这一章,我们开始构建我们定制的应用程序。
具体来说,在这一章中,我们看到了如何从标准命令的输出中创建报告,比如lastlog
。我们看到我们可以否定范围,并且另外利用OR
语句。然后我们构建了一个允许我们查询 XML 数据的应用程序。
在接下来的两章中,我们将远离 shell 脚本,转而使用 perl 和 Python 编写脚本,这样我们可以比较脚本语言并做出适当的选择。
第十三章:使用 Perl 作为 Bash 脚本的替代方案
使用 bash 进行脚本编写可以帮助您自动化任务,并且通过掌握 bash 脚本编写,您可以取得很大成就。然而,您的旅程不应该以 bash 结束。虽然我们已经看到了在 bash 脚本中可用的功能,但我们受到可以运行的命令和它们的选项的限制。Bash 脚本允许我们访问命令;而如果我们使用 Perl 脚本,我们就可以访问系统的编程接口或 API。通过这种方式,我们通常可以用更少的资源实现更多的功能。
在本章中,我们将介绍 Perl 脚本和一些其他基本脚本,我们可以用来学习 Perl;我们将涵盖以下主题:
-
什么是 Perl?
-
Hello World
-
Perl 中的数组
-
Perl 中的条件测试
-
函数
什么是 Perl?
Perl 是一种脚本语言,由 Larry Wall 在 1980 年代开发,用于扩展sed
和awk
的功能。它是Practical Extraction and Reporting Language的首字母缩写,但已经远远超出了最初的目的,今天它可以在 Unix、Linux、OS X 和 Windows 操作系统上使用。
尽管它是一种脚本语言,但它不是 shell 脚本;因此没有 Perl shell。这意味着代码必须通过 Perl 脚本执行,而不是直接从命令行执行。唯一的例外是perl
命令的-e
选项,它可以允许您执行一个perl
语句。例如,我们可以使用以下命令行来打印无处不在的Hello World
:
$ perl -e ' print("Hello World\n");'
您会发现 Perl 默认安装在大多数 Linux 和 Unix 系统上,因为许多程序将在它们的代码中使用 Perl。要检查您系统上安装的 Perl 版本,可以使用perl
命令,如下所示:
$ perl -v
这个命令的输出显示在我树莓派上的以下截图中:
注意
在本章中,大写的 Perl 将指的是语言,小写的perl
将指的是命令。
如果我们创建一个 Perl 脚本,就像 bash 一样,它将是一种解释性语言,第一行将是 shebang,以便系统知道要使用哪个命令来读取脚本。/usr/bin/perl
命令通常用于定位perl
。要验证这一点,可以使用:
$ which perl
与 bash 不同,当perl
命令读取脚本时,它将在运行时优化脚本;这将使我们能够在脚本末尾定义函数,而不是在使用之前。当我们在本章中详细查看 Perl 脚本时,我们将看到这一点。
Hello World
要创建一个简单的 Perl 脚本,我们可以使用所选的文本编辑器。对于短脚本,vi
或vim
效果很好,如果要在 GUI 中工作,gedit
也可以。对于较大的项目,IDE 可能会有所帮助。通常,IDE 将允许您轻松地在整个脚本中更改对象名称并提供对象名称的扩展。在本章中,我们将继续使用vi
。
我们将创建一个$HOME/bin/hello.pl
文件来产生我们想要的输出:
#!/usr/bin/perl
print("Hello World\n");
文件仍然需要在我们的PATH
变量中的目录中;因此,我们创建$HOME/bin
。如果它不在PATH
变量中,那么我们将需要指定文件的完整路径或相对路径,就像 bash 一样。
文件需要设置执行权限。我们可以使用以下命令来实现:
$ chmod u+x $HOME/bin/hello.pl
我们可以使用以下命令运行脚本:
$ hello.pl
我们可以看到我们添加的代码与我们之前运行的perl -e
命令相同。唯一的区别是 shebang。这也与 bash 非常相似。我们现在使用 print 函数而不是使用echo
命令。Bash 脚本运行一系列命令,而 Perl 脚本运行函数。print 函数不会自动添加新行,因此我们使用\n
字符自己添加。我们还可以看到 Perl 使用分号来终止一行代码。shebang 不是一行代码,而 print 行以分号终止。
如果我们使用的是 Perl 5.10 或更高版本,在 Pi 上我们已经看到它是 5.14,我们还可以使用一个名为say
的函数。类似于print
命令,它用于显示输出,但它还包括换行符。我们必须启用此功能,由use
关键字管理。以下任一脚本都将使用say
函数打印Hello World
:
#!/usr/bin/perl
use v5.10;
say("Hello World");
#!/usr/bin/perl
use 5.10.0;
say("Hello World");
say
函数还简化了文件和列表的打印。
Perl 数组
在 Perl 中我们可以利用的一点是数组。这些数组是从列表创建的变量;简单地说,它们基本上是多值变量。如果我们要使用容器类比来描述一个变量,它将是一个杯子或一个值的占位符。数组将类比为一个板条箱。我们可以用一个单一的名称描述板条箱,但是我们必须包括额外的命名元素来访问板条箱内的每个槽。一个板条箱可以容纳多个项目,就像一个数组一样。
我们看到通过使用 bash 脚本,我们可以在脚本中传递命令行参数。参数使用它们自己的变量名,$1
,$2
等。这也与程序的名称有一定的冲突,因为它是$0
。即使它们看起来可能相似,但$0
和$1
之间没有逻辑关系。$0
变量是脚本的名称,$1
是第一个参数。当我们在 Perl 中看到这一点时,我们可以开始看到一些主要的区别。
程序名称?
在 Perl 中,程序名称仍然可以使用$0
变量访问。我们可以在以下脚本中看到这一点:
#!/usr/bin/perl
print("You are using $0\n");
print("Hello World\n");
现在,即使我们认为$0
使用起来相当简单,因为我们之前在 bash 中访问过它,但如果我们以全新的眼光来看待它,它并不那么明显。Perl 有一个名为English
的模块,其中定义了许多其他在 Perl 中使用的变量的更友好的名称。如果我们看一下以下脚本,我们可以看到它的用法:
#!/usr/bin/perl
use English;
print("You are using $PROGRAM_NAME\n");
print("Hello World\n");
use English
;这一行将导入重新定义$0
的模块,以便可以将其引用为$PROGRAM_NAME
。尽管这需要更多的输入,但它也作为一个更好的名称来记录其目的。
参数数组
不再使用$1
,$2
等参数;Perl 现在使用存储在单个数组变量中的参数列表。数组名称是@ARGV
,我们可以通过索引号或槽号访问由此提供的每个参数。计算机从0
开始计数,所以第一个参数将是$ARGV[0]
,第二个将是$ARGV[1]
,依此类推。
注意
使用@
符号命名索引数组。数组的每个元素仍然是单个或标量变量,就像在 bash 中一样,它们使用$
符号读取。
当我们查看以下脚本$HOME/bin/args.pl
时,我们可以看到如何通过接受参数使 Hello 脚本更具可移植性:
#!/usr/bin/perl
use English;
print("You are using $PROGRAM_NAME\n");
print("Hello $ARGV[0]\n");
我们可以通过运行脚本来看到这一点,如下面的屏幕截图所示:
计算数组中的元素
我们可以看到命令行参数存储在@ARGV
数组中。我们可以使用以下代码计算参数的数量,或者实际上是任何数组中的元素:
scalar @<array-name>;
因此,我们将使用以下代码来计算提供的参数,而不是使用$#
:
scalar @ARGV;
如果我们将这个添加到我们的脚本中,它将会被看到,如下面的代码块所示:
#!/usr/bin/perl
use English;
print("You are using $PROGRAM_NAME\n");
print("You have supplied: " . scalar @ARGV . " arguments\n");
print("Hello $ARGV[0]\n");
注意
我们还可以从前面的代码块中注意到,我们可以使用句点字符将命令的输出与测试连接起来。
循环遍历数组
在 bash 中,我们有一个简单的机制,使用$*
来引用提供给脚本的参数列表。在 Perl 中,这与必须循环遍历列表略有不同。然而,foreach
关键字是为此而建立的:
#!/usr/bin/perl
use English;
print("You are using $PROGRAM_NAME\n");
print("You have supplied " . scalar @ARGV . " arguments\n");
foreach $arg (@ARGV) {
print("Hello $arg\n");
}
我们可以看到,代码是在循环内定义的,并使用大括号括起来。如果您还记得,bash 并没有专门的foreach
关键字,而是使用do
和done
来限制代码。
如果我们在$HOME/bin/forargs.pl
文件中实现此代码,我们可以执行类似以下屏幕截图的代码:
创建数组
到目前为止,我们一直依赖于@ARGV
系统数组,这已被证明是学习如何访问数组的好方法。现在我们需要看看如何创建我们自己设计的数组。
数组是可以存储混合数据类型的值的列表;因此,我们可以有一个既存储字符串又存储数字的数组是毫无问题的。提供给数组的项目的顺序将设置它们的索引位置。换句话说,列表中的第一项将是数组中的第一个索引或索引0
。考虑以下代码:$HOME/bin/array.pl
:
#!/usr/bin/perl
use English;
print("You are using $PROGRAM_NAME\n");
@user = ("Fred","Bloggs",24);
print("$user[0] $user[1] is @user[2]\n");
我们应该注意的第一件事是,当我们设置任何类型的变量时,包括数组时,我们将使用变量类型的指示符。我们在这里看到,使用@user = …
,将使用先前提到的@
符号来表示变量是一个数组变量。如果我们设置一个类似于我们在 bash 中使用的标量变量,我们将设置$user
。在 bash 中,设置变量时不使用指示符,并且我们不能在赋值运算符=
周围有空格。Perl 将允许空格,并通过额外的空格提高可读性。
接下来,我们应该注意到列表包含字符串和整数。这是完全可以接受的,数组可以容纳不同的数据类型。数组的单个名称是有意义的,因为我们现在可以将相关数据存储到一个对象中。
在提供的代码中需要注意的最后一点是,我们可以轻松地使用 Perl 将字符串值与整数值连接起来。无需提供任何形式的数据转换。在单个字符串中,我们打印用户的名字、姓氏和年龄。
在脚本执行时,我们应该收到一个输出,如下面的屏幕截图所示:
Perl 中的条件语句
与 Perl 语言的其余部分类似,我们将与 bash 脚本编写有相似之处,也有一些完全实现条件的新方法。这通常对我们有利,因此使代码更易读。
替换命令行列表
首先,我们没有命令行列表逻辑,我们在 bash 中使用的逻辑,也不使用&&
和||
。在 Perl 中,单个语句的条件逻辑是以以下方式编写的,而不是这些看起来相当奇怪的符号:
exit(2) if scalar @ARGV < 1;
print("Hello $ARGV[0]\n") unless scalar @ARGV == 0;
在第一个例子中,如果我们提供的命令行参数少于一个,我们将以错误代码2
退出。这在 bash 中的等效操作将是:
[ $# -lt 1 ] && exit 2
在第二个例子中,只有在我们提供了参数时,我们才会打印hello
语句。这将在 bash 中编写,如下例所示:
[ $# -eq 0 ] || echo "Hello $1"
就个人而言,我喜欢 Perl;至少它使用单词的方式,这样我们即使以前没有遇到过这些符号,也可以理解发生了什么。
If 和 unless
在 Perl 中,我们已经在之前的例子中看到,我们可以使用unless
来使用负逻辑。我们既有传统的if
关键字,现在又有了unless
。我们可以在我们已经看到的短代码中使用这些,也可以在完整的代码块中使用。
我们可以编辑现有的 args.pl
来创建一个新文件:$HOME/bin/ifargs.pl
。文件应该类似于以下代码:
#!/usr/bin/perl
use English;
print("You are using $PROGRAM_NAME\n");
my $count = scalar @ARGV;
if ($count > 0) {
print("You have supplied $count arguments\n");
print("Hello $ARGV[0]\n");
}
现在代码有了一个额外的参数,我们已经声明并设置了这一行 my $count = scalar @ARGV;
。我们使用这个值作为 if
语句的条件。在大括号中限定的代码块只有在条件为真时才会执行。
我们演示了在下面的截图中使用和不使用参数运行此程序:
我们可以使用 unless
来编写类似的代码:
print("You are using $PROGRAM_NAME\n");
my $count = scalar @ARGV;
unless ($count == 0) {
print("You have supplied $count arguments\n");
print("Hello $ARGV[0]\n");
}
括号中的代码现在只有在条件为假时才运行。在这种情况下,如果我们没有提供参数,代码将不运行。
在 Perl 中使用函数
与所有语言一样,将代码封装在函数中可以使代码更易读,并最终导致更易管理的代码,代码行数也更少。与 bash 不同,Perl 中的函数可以在代码中引用后定义,我们通常选择在脚本末尾定义函数。
提示用户输入
我们已经看到了在 Perl 中使用命令行参数;现在,让我们来看看如何提示用户输入。这成为了一种封装执行代码和存储提示的好方法。首先,我们将看一个简单的脚本,提示用户名,然后我们将修改它以包含函数。我们将创建 $HOME/bin/prompt.pl
文件来读取,如下面的代码示例所示:
#!/usr/bin/perl
my $name;
print("Enter your name: ");
chomp( $name = <STDIN> );
print("Hello $name\n");
在第 2 行,我们使用 my
声明了变量。关键字 my
定义了具有局部作用域的变量。换句话说,它仅在创建它的代码块中可用。由于这是在脚本的主体中创建的,变量对整个脚本都是可用的。这一行声明了变量,但我们此时没有设置值。Perl 不强制您声明变量,但这是一个好主意和一个很好的实践。事实上,我们可以告诉 Perl 使用 use strict;
行来强制执行这一点。我们可以实现这一点,如下面的代码块所示:
#!/usr/bin/perl
use strict;
my $name;
print("Enter your name: ");
chomp( $name = <STDIN> );
print("Hello $name\n");
有了这个,我们被迫声明变量,如果没有声明,代码将失败。这背后的想法是通过在代码后期识别拼写错误的变量来帮助故障排除。尝试删除以 my
开头的行并重新执行代码;它将失败。同样,我们可以使用 use warnings;
行,如果我们只使用了一次变量,它会警告我们。
我们提示用户输入用户名,这里不使用换行符。我们希望提示与用户输入数据的行在同一行上。chomp
函数很棒,不是吗?这个函数将删除或截断我们提交的输入中的换行符。我们需要使用 Enter 键提交数据,chomp
会为我们删除换行符。
创建函数
目前我们只提示用户输入用户名,所以我们只需要一个提示,但我们也可以很容易地要求名字和姓氏。我们可以创建一个函数,而不是每次都写提示的代码。这些是使用关键字 sub
定义的,如下面的代码所示:
#!/usr/bin/perl
use strict;
my $name = prompt_user("Enter a name: ");
print("Hello $name\n");
sub prompt_user () {
my $n;
print($_[0]);
chomp( $n = <STDIN> );
return($n);
}
prompt_user
函数接受一个参数,这个参数将成为显示提示的消息。对于参数的引用,我们使用系统数组 @_
和索引 0
。这写作 $_[0]
。如果我们记得,数组是多值的,数组中的每个条目都是一个标量变量。在函数内部,我们使用函数返回将用户设置的值发送回调用代码。我们可以看到主代码块现在更简单了,因为提示的代码被抽象成了一个函数。当我们看到这个时,可能会觉得这需要很多工作,但是当我们为名字和姓氏添加提示时,现在就简单多了。
使用函数是一个好习惯,希望下面的代码能帮助你看到这一点:
#!/usr/bin/perl
use strict;
my $fname = prompt_user("Enter a first name: ");
my $lname = prompt_user("Enter a last name: ");
print("Hello $fname $lname\n");
sub prompt_user () {
my $n;
print($_[0]);
chomp( $n = <STDIN> );
return($n);
}
总结
这就结束了我们的风风火火的旅程和对 Perl 的介绍。我们已经看到了它与 bash 的相似之处,以及新的特性和区别。从中可以得出的主要观点是,一旦你精通一种语言,学习其他编程语言就会变得更容易。
为了保持学习新语言的兴致,我们接下来将在下一章快速了解 Python。
第十四章:使用 Python 作为 Bash 脚本替代品
Python 是另一种脚本语言,也是我们迄今为止看过的最新的脚本语言。与 bash 和 Perl 类似,Python 是一种解释性语言,并使用 shebang。尽管它没有 shell 界面,但我们可以访问一个名为 REPL 的控制台,在那里我们可以输入 Python 代码与系统进行交互。在本章中,我们将涵盖以下主题:
-
什么是 Python?
-
以 Python 方式说 Hello
-
Pythonic 参数
-
重要的空白
-
读取用户输入
-
使用 Python 写入文件
什么是 Python?
Python 是一种面向对象的解释性语言,旨在易于使用并有助于快速应用程序开发。这是通过在语言中使用简化的语义来实现的。
Python 诞生于 20 世纪 80 年代末,即 1989 年 12 月底,由荷兰开发者 Guido van Rossum 创建。该语言的设计大部分目的在于清晰和简单,而Python 之禅的主要规则之一是:
"应该有一种,最好只有一种明显的方法来做到这一点。"
通常系统会安装 Python 2 和 Python 3,但是所有更新的发行版都在转向 Python 3。我们将使用 Python 3,因为它是树莓派上安装的最新版本。
尽管没有 shell,我们可以使用 REPL 与 Python 进行交互:读取、评估、打印和循环。我们可以通过在命令行中输入python3
来访问这个。您应该看到类似以下屏幕截图的内容:
我们可以看到我们被呈现出>>>提示,这被称为 REPL 控制台。我们应该强调这是一种脚本语言,就像 bash 和 Perl 一样,我们通常会通过创建的文本文件来执行代码。这些文本文件通常希望其名称具有.py
后缀。
在使用 REPL 时,我们可以通过导入模块来独立打印版本。在 Perl 中,我们将使用关键字,在 bash 中我们将使用命令源,在 Python 中我们使用import
:
>>>import sys
加载模块后,我们现在可以通过打印版本来研究 Python 的面向对象的特性:
>>> sys.version
我们将导航到我们命名空间中的 sys 对象,并从该对象调用 version 方法。
将这两个命令组合在一起,我们应该能够看到以下输出:
要结束关于描述 Python 的这一部分,我们应该看一下Python 之禅。从 REPL,我们可以输入import this
,如下面的屏幕截图所示:
这远不仅仅是 Python 之禅;它实际上构成了所有编程语言的一个很好的规则,也是开发人员的指南。
最后,要关闭 REPL,我们将在 Linux 中使用Ctrl + d,在 Windows 中使用Ctrl + z。
以 Python 方式说 Hello World
我们在 Python 中编写的代码应该清晰而简洁,稀疏比密集更好。我们需要在第一行上使用 shebang,然后使用print
语句。print
函数包括换行符,我们不需要在行尾使用分号。我们可以在以下示例中看到编辑后的$HOME/bin/hello.py
版本:
#!/usr/bin/python3
print("Hello World")
我们仍然需要添加执行权限,但是我们可以像之前一样使用chmod
来运行代码。这在以下命令中显示,但我们现在应该对此有点习惯了:
$ chmod u+x $HOME/bin/hello.py
最后,我们现在可以执行代码来看到我们的问候语。
再次,至少了解一种语言会使适应其他语言变得更容易,而且这并没有太多新功能。
Pythonic 参数
到目前为止,我们应该知道我们希望能够向 Python 传递命令行参数,我们可以使用argv
数组来实现这一点,类似于 Perl。但是,与 bash 更像,使用 Python 时,我们将程序名称与其他参数组合到数组中。Python 还使用小写而不是大写的对象名称。
-
argv
数组是sys
对象的一部分 -
sys.argv[0]
是脚本名称 -
sys.argv[1]
是提供给脚本的第一个参数 -
sys.argv[2]
是第二个提供的参数,依此类推 -
参数计数始终至少为 1,因此,在检查提供的参数时请记住这一点
提供参数
如果我们创建$HOME/bin/args.py
文件,我们可以看到它的运行情况。文件应该创建如下并且可执行:
#!/usr/bin/python3
import sys
print("Hello " + sys.argv[1])
如果我们使用提供的参数运行脚本,我们应该能够看到类似以下截图的内容:
我们的代码仍然非常清晰简单;但是,我们可能已经注意到我们不能将print
语句中的带引号的文本与参数组合在一起。我们使用+
符号将两个字符串连接在一起。由于没有特定的符号来表示变量或任何其他类型的对象,它们不能出现在引号内作为静态文本。
计算参数
如前所述,脚本名称是数组中索引为0
的第一个参数。因此,如果我们尝试计算参数,那么计数应该始终至少为 1。换句话说,如果我们没有提供参数,参数计数将为 1。要计算数组中的项目数,我们可以使用len()
函数。如果我们编辑脚本以包含一个新行,我们将看到它的工作情况,如下所示:
#!/usr/bin/python3
import sys
print("Hello " + sys.argv[1])
print( len(sys.argv) )
执行代码,我们可以看到我们提供了两个参数。脚本名称,然后是字符串fred
:
如果我们尝试使用单个print
语句来打印输出和参数的数量,我们会发现 Python 不喜欢混合数据类型。长度值是整数,这不能在没有转换的情况下与字符串混合。以下代码将失败:
#!/usr/bin/python3
import sys
print("Hello " + sys.argv[1] + " " + len(sys.argv))
但是,这并不是一项艰巨的任务,只需要明确的转换。来自 Python 之禅:
“明确胜于隐晦。”
修改后,代码将正常工作,如下所示:
#!/usr/bin/python3
import sys
print("Hello " + sys.argv[1] + " " + str(len(sys.argv)))
如果我们尝试运行脚本并省略提供参数,那么当我们引用索引1
时,数组中将会有一个空值。这将导致错误,如下截图所示:
当然,我们需要处理这个问题以防止错误,现在我们可以进入重要的空白部分。
重要的空白
Python 与大多数其他语言之间的一个主要区别是额外的空白可能意味着某些东西。代码的缩进级别定义了它所属的代码块。到目前为止,我们还没有将创建的代码缩进到行的开头之后。这意味着所有的代码都在相同的缩进级别,并且属于相同的代码块。我们使用缩进而不是使用大括号或do
和done
关键字来定义代码块。如果我们使用四个空格进行缩进,那么我们必须坚持使用这四个空格。当我们返回到先前的缩进级别时,我们返回到先前的代码块。
这似乎很复杂,但实际上非常简单,可以保持代码的清晰和简洁。如果我们编辑arg.py
文件以防止不受欢迎的错误,如果没有提供参数,我们可以看到它的运行情况:
#!/usr/bin/python3
import sys
count = len(sys.argv)
if ( count > 1 ):
print("Arguments supplied: " + str(count))
print("Hello " + sys.argv[1])
print("Exiting " + sys.argv[0])
if
语句检查参数计数是否大于1
。现在,为了方便起见,我们将参数计数存储为自己的变量,称为count
。代码块以冒号开始,然后所有缩进到四个空格的后续代码都是将在条件返回为真时执行的代码的一部分。
当我们返回到先前的缩进级别时,我们返回到主代码块并执行代码,无论条件的状态如何。
我们可以在以下截图中看到这一点,我们可以执行带有参数和不带参数的脚本:
读取用户输入
如果我们希望欢迎消息无论是否提供脚本参数都能以我们的名字来欢迎我们,我们可以在脚本运行时添加提示来捕获数据。Python 使这变得简单且易于实现。我们可以从以下截图中显示的编辑文件中看到如何实现这一点:
我们在脚本中使用了一个新变量,最初在主块中设置为空字符串。在这里设置它,以便该变量对整个脚本和所有代码块都可用。
在 Python 3 中可以使用input
函数,或者在 Python 2 中可以使用raw_input
来获取用户输入。我们将输入存储在name
变量中。如果我们提供了参数,我们将在else
块中的代码中获取它,并将name
变量设置为第一个提供的参数。这将在主块中的print
语句中使用。
使用 Python 写入文件
为了给本章增加一些变化,我们现在将看看如何将这些数据打印到文件中。再次使用 Python,这是一个相当简单和容易上手的方法。我们将首先复制我们现有的args.py
。我们将其复制到$HOME/bin/file.py
。新的file.py
应该类似于以下截图,并且设置执行权限:
您会注意到我们刚刚修改了最后几行,而不是打印,我们现在打开了一个文件。我们还看到了 Python 的面向对象生活更多的一面,它动态地将write()
和close()
方法分配给了 log 对象,因为它被视为文件的一个实例。当我们打开文件时,我们是为了追加目的而打开它,这意味着如果已经有内容,我们不会覆盖现有内容。如果文件不存在,我们将创建一个新文件。如果我们使用w
,我们将打开文件进行写入,这可能会导致覆盖,所以要小心。
你可以看到这是一个简单的任务,这就是为什么 Python 被用在许多应用程序中并且在学校广泛教授的原因。
总结
这就结束了我们对 Python 的介绍,这确实是一个简短的旅程。我们再次强调你将在许多语言中看到的相似之处,以及学习任何编程语言的重要性。你在一种语言中学到的东西将有助于你在大多数其他语言中的学习。
我们从 Python 之禅中学到的东西将帮助我们设计和开发出优秀的代码。我们可以使用以下 Python 代码打印 Python 之禅:
>>>import this
我们可以在 REPL 提示符上输入代码。保持代码整洁和间隔良好将有助于代码的可读性,最终这将有助于代码的维护。
我们还看到 Python 喜欢你在代码中明确表达,并且不会隐式转换数据类型。
我们也到了书的结尾,但希望这是你脚本编写生涯的开端。祝你好运,谢谢阅读。