Linux-Shell-编程秘籍-全-

Linux Shell 编程秘籍(全)

原文:zh.annas-archive.org/md5/ABA4B56CB4F69896DB2E9CFE0817AFEF

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

GNU/Linux 是一个非常出色的操作系统,具有完整的开发环境,稳定、可靠且功能强大。作为与操作系统通信的本地接口,shell 能够控制整个操作系统。了解 shell 脚本有助于更好地了解操作系统,并帮助您用几行脚本自动化大部分手动任务,节省大量时间。shell 脚本可以与许多外部命令行实用程序一起工作,用于查询信息、简单文本处理、调度任务运行时间、准备报告、发送邮件等。GNU/Linux shell 上有许多命令,虽然有文档记录,但很难理解。本书是一本包含详细描述和实际应用的基本命令行脚本食谱集。它涵盖了 Linux 中大多数重要的命令,并伴有大量用例。本书帮助您使用少量命令组合执行涉及文本处理、文件管理、备份等复杂数据操作。

你想成为一名命令行巫师吗?能够用一行代码执行任何复杂的文本处理任务?你想为了乐趣或严肃的系统管理编写 shell 脚本和报告工具吗?这本食谱书就是为你准备的。开始阅读吧!

本书涵盖内容

第一章 ,命令行基础,收集了一系列食谱,涵盖了基本任务,如在终端中打印、执行数学运算、数组、运算符、函数、别名、文件重定向等,都是通过 Bash 脚本实现的。本章是一个介绍性章节,用于理解 Bash 中的基本概念和特性。

第二章 ,掌握好命令,展示了 GNU/Linux 中可用的各种命令,这些命令在不同情况下都具有实际用途。它介绍了各种基本命令,如 cat、md5sum、find、tr、sort、uniq、split、rename、look 等。本章通过不同的实际用例,向用户展示了他们可能遇到的情况以及可以利用的命令。

第三章 ,文件输入,文件输出,包含了与文件和文件系统相关的任务食谱。本章解释了如何生成大型文件、在文件上安装文件系统和挂载文件、查找和删除重复文件、计算文件中的行数、创建 ISO 镜像、收集文件的详细信息、符号链接操作、文件权限和文件属性等。

第四章 ,文本处理与驱动,收集了大量命令行文本处理工具在 GNU/Linux 下的使用示例。它还包括了正则表达式和 sed、awk 等命令的详细概述。本章通过各种示例解决了大多数常用的文本处理任务。

第五章 ,网络迷局?一点也不!,收集了与互联网和网络相关的 shell 脚本食谱。本章旨在帮助读者了解如何使用 shell 脚本与网络交互,自动化任务,如从网页收集和解析数据、向网页发送 POST 和 GET 请求、编写客户端访问网络服务、下载网页等。

第六章, 备份计划,展示了用于执行数据备份、归档、压缩等操作的几个命令,以及它们在实际脚本示例中的用法。它介绍了诸如 tar、gzip、bunzip、cpio、lzma、dd、rsync、git、squashfs 等命令。本章还介绍了基本的加密技术。

第七章, 老男孩网络,收集了一些关于在 Linux 上进行网络和编写基于网络的脚本的有用命令的配方。本章以介绍基本的网络入门知识开始。本章中解释的重要任务包括使用 SSH 进行无密码登录,通过网络传输文件,列出网络上的活动机器,多播消息等等。

第八章, 戴上监视器帽,介绍了与 Linux 系统上的监视活动和用于日志记录和报告的任务相关的几个配方。本章解释了计算磁盘使用情况、监视用户访问、CPU 使用情况、syslog、常用命令等任务。

第九章, 管理调用,收集了一些系统管理的配方。本章解释了不同的命令,用于收集有关系统的详细信息,使用脚本进行用户管理,向用户发送消息,批量调整图像大小,从 shell 访问 MySQL 数据库等等。

您需要为这本书做好准备

任何 GNU/Linux 平台的基本用户体验都将帮助您轻松地跟随本书。我们已经尽量保持本书中所有配方的简洁和易于遵循。您对学习 Linux 平台的好奇心是本书的唯一先决条件。书中提供了解决脚本问题的逐步解释。为了运行和测试本书中的示例,建议安装 Ubuntu Linux,但是对于大多数任务来说,任何其他 Linux 发行版都足够了。您会发现本书是基本的 shell 脚本任务的简单参考,也是编写真实高效脚本的学习辅助工具。

这本书适合谁

如果您是一个想要掌握快速编写脚本以执行各种任务的初学者或中级用户,而不必阅读整个 man 页面,那么这本书适合您。您可以通过简单查看类似的配方及其描述来开始编写脚本和一行命令,而无需任何 shell 脚本或 Linux 的工作知识。中级或高级用户以及系统管理员或开发人员和程序员在编码时遇到问题时,可以将本书用作参考。

约定

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码词显示如下:"我们可以使用printf来使用格式化字符串。"

代码块设置如下:

#!/bin/bash 
#Filename: printf.sh

printf  "%-5s %-10s %-4s\n" No Name  Mark 
printf  "%-5s %-10s %-4.2f\n" 1 Sarath 80.3456 
printf  "%-5s %-10s %-4.2f\n" 2 James 90.9989 
printf  "%-5s %-10s %-4.2f\n" 3 Jeff 77.564

任何命令行输入或输出都写成如下形式:

$ chmod +s executable_file

# chown root.root executable_file
# chmod +s executable_file
$ ./executable_file

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会出现在这样。

读者反馈

我们的读者的反馈总是受欢迎的。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者的反馈对我们开发您真正受益的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在您的消息主题中提到书名。

如果有一本书是您需要的,并且希望我们出版,请在www.packtpub.com建议标题表单上给我们留言,或发送电子邮件至<suggest@packtpub.com>

如果您在某个专题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请参阅我们的作者指南 www.packtpub.com/authors

客户支持

既然您已经是 Packt 图书的自豪所有者,我们有一些事情可以帮助您充分利用您的购买。

提示

下载本书的示例代码

您可以从 www.PacktPub.com 的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.PacktPub.com/support 并注册,以便文件直接通过电子邮件发送给您。

勘误

尽管我们已经非常小心确保内容的准确性,但错误是难免的。如果您在我们的书中发现错误——也许是文本或代码中的错误——我们将不胜感激,如果您能向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问 www.packtpub.com/support,选择您的书,点击勘误提交表链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站上,或者添加到该标题的勘误列表中的任何现有勘误下的勘误部分。您可以通过从 www.packtpub.com/support 选择您的标题来查看任何现有的勘误。

盗版

互联网上的版权盗版问题是所有媒体的持续问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

如果您发现有涉嫌盗版的材料,请通过 <copyright@packtpub.com> 与我们联系。

我们感谢您在保护我们的作者和我们为您提供有价值的内容的能力方面的帮助。

问题

如果您在书的任何方面遇到问题,请通过 <questions@packtpub.com> 与我们联系,我们将尽力解决。

第一章:外壳某事

在本章中,我们将涵盖:

  • 在终端中打印

  • 玩转变量和环境变量

  • 使用 shell 进行数学计算

  • 玩转文件描述符和重定向

  • 数组和关联数组

  • 访问别名

  • 获取有关终端的信息

  • 获取、设置日期和延迟

  • 调试脚本

  • 函数和参数

  • 在变量中读取一系列命令的输出

  • 在不按回车键的情况下读取“n”个字符

  • 字段分隔符和迭代器

  • 比较和测试

介绍

类 UNIX 系统是令人惊叹的操作系统设计。即使经过了许多年,类 UNIX 架构的操作系统仍然是最佳设计之一。这种架构的最重要特性之一是命令行界面或 shell。shell 环境帮助用户与操作系统的核心功能进行交互和访问。在这种情况下,术语脚本更相关。脚本通常由解释器支持的编程语言支持。Shell 脚本是我们编写需要执行的一系列命令的文件。脚本文件是使用 shell 实用程序执行的。

在本书中,我们处理的是 Bash(Bourne Again Shell),这是大多数 GNU/Linux 系统的默认 shell 环境。由于 GNU/Linux 是基于类 UNIX 架构的最突出的操作系统,大多数示例和讨论都是以 Linux 系统为基础编写的。

本章的主要目的是让读者了解 shell 环境,并熟悉围绕 shell 的基本功能。命令是在 shell 终端中输入和执行的。在打开终端时,会出现提示符。它通常是以下格式:

username@hostname$

或者:

root@hostname#

或者简单地为$#

$表示普通用户,#表示管理员用户 root。Root 是 Linux 系统中权限最高的用户。

Shell 脚本是一个文本文件,通常以 shebang 开头,如下所示:

#!/bin/bash

对于 Linux 环境中的任何脚本语言,脚本都以特殊行 shebang 开头。shebang 是一个前缀为#!的解释器路径的行。/bin/bash是 Bash 的解释器命令路径。

脚本的执行可以通过两种方式进行。我们可以将脚本作为sh的命令行参数运行,或者以执行权限运行自行可执行文件。

可以将脚本作为文件名的命令行参数运行,如下所示:

$ sh script.sh # Assuming script is in the current directory.

或者:

$ sh /home/path/script.sh # Using full path of script.sh.

如果脚本作为sh的命令行参数运行,则脚本中的 shebang 无效。

为了自行执行 shell 脚本,需要可执行权限。在作为自行可执行文件运行时,它使用 shebang。它使用 shebang 中附加到#!的解释器路径运行脚本。可以设置脚本的执行权限如下:

$ chmod a+x script.sh

此命令为script.sh文件赋予所有用户的可执行权限。脚本可以执行如下:

$ ./script.sh #./ represents the current directory

或:

$ /home/path/script.sh # Full path of the script is used

shell 程序将读取第一行,并查看 shebang 是#!/bin/bash。它将识别/bin/bash并在内部执行脚本:

$ /bin/bash script.sh

当打开终端时,它最初会执行一组命令来定义各种设置,如提示文本、颜色等。这组命令(运行命令)是从一个名为.bashrc的 shell 脚本中读取的,该脚本位于用户的主目录(~/.bashrc)中。bash shell 还会维护用户运行的命令历史记录。它位于文件~/.bash_history中。~是用户主目录路径的缩写。

在 Bash 中,每个命令或命令序列都是使用分号或换行符分隔的。例如:

$ cmd1 ; cmd2

这相当于:

$ cmd1
$ cmd2

最后,#字符用于表示未处理注释的开始。注释部分以#开头,并一直延续到该行的末尾。注释行通常用于提供有关文件中代码的注释,或者阻止执行代码行。

现在让我们继续学习本章的基本配方。

在终端中打印

终端是用户与 shell 环境交互的交互式实用程序。在终端中打印文本是大多数 shell 脚本和实用程序需要定期执行的基本任务。可以通过各种方法和不同格式进行打印。

如何做...

echo是在终端中打印的基本命令。

echo默认情况下在每次调用结束时都会换行:

$ echo "Welcome to Bash"
Welcome to Bash

只需使用带有echo命令的双引号文本即可在终端中打印文本。同样,没有双引号的文本也会产生相同的输出:

$ echo Welcome to Bash
Welcome to Bash

另一种执行相同任务的方法是使用单引号:

$ echo 'text in quote'

这些方法可能看起来相似,但其中一些具有特定目的和副作用。考虑以下命令:

$ echo "cannot include exclamation - ! within double quotes"

这将返回以下内容:

bash: !: event not found error

因此,如果要打印!,不要在双引号内使用,或者可以使用特殊的转义字符(\)对!进行转义。

$ echo Hello world !

或者:

$ echo 'Hello world !'

或者:

$ echo "Hello world \!" #Escape character \ prefixed.

当使用双引号与echo一起使用时,你应该在发出echo之前添加set +H,这样你就可以使用!

每种方法的副作用如下:

  • 当不使用引号与echo一起使用时,我们不能使用分号,因为它在 bash shell 中充当命令之间的分隔符。

  • echo hello;helloecho hello作为一个命令,第二个hello作为第二个命令。

  • 当使用单引号与echo一起使用时,引号内的变量(例如,$var)不会被 Bash 展开,而是会按原样显示。

这意味着:

$ echo '$var'将返回$var

然而

如果定义了$var$ echo $var将返回变量$var的值,如果未定义,则返回空。

在终端中打印的另一个命令是printf命令。printf使用与 C 编程语言中的printf命令相同的参数。例如:

$ printf "Hello world"

printf接受带引号的文本或由空格分隔的参数。我们可以使用带有printf的格式化字符串。我们可以指定字符串宽度,左对齐或右对齐等。默认情况下,printf不像echo命令那样具有换行符。我们必须在需要时指定换行符,如下面的脚本所示:

#!/bin/bash 
#Filename: printf.sh

printf  "%-5s %-10s %-4s\n" No Name  Mark 
printf  "%-5s %-10s %-4.2f\n" 1 Sarath 80.3456 
printf  "%-5s %-10s %-4.2f\n" 2 James 90.9989 
printf  "%-5s %-10s %-4.2f\n" 3 Jeff 77.564

我们将收到格式化的输出:

No    Name       Mark
1     Sarath     80.35
2     James      91.00
3     Jeff       77.56

%s%c%d%f是格式替换字符,可以在引号格式字符串之后放置参数。

%-5s可以描述为具有左对齐的字符串替换(-表示左对齐),宽度等于5。如果未指定-,则字符串将对齐到右侧。宽度指定了为该变量保留的字符数。对于Name,保留的宽度为10。因此,任何名称都将位于为其保留的 10 个字符宽度内,其余字符将填充到总共 10 个字符。

对于浮点数,我们可以传递额外的参数来四舍五入小数位。

对于标记,我们已将字符串格式化为%-4.2f,其中.2指定四舍五入到小数点后两位。请注意,对于格式字符串的每一行,都会发出一个\n换行符。

还有更多...

应始终注意,echoprintf的标志(例如-e、-n 等)应出现在命令中的任何字符串之前,否则 Bash 将把标志视为另一个字符串。

echo中转义换行符

默认情况下,echo在其输出文本的末尾附加换行符。可以通过使用-n标志来避免这种情况。echo也可以接受双引号字符串中的转义序列作为参数。要使用转义序列,请使用echo作为echo -e "包含转义序列的字符串"。例如:

echo -e "1\t2\t3"
123

打印彩色输出

在终端上生成彩色输出非常有趣。我们使用转义序列生成彩色输出。

颜色代码用于表示每种颜色。例如,reset=0,black=30,red=31,green=32,yellow=33,blue=34,magenta=35,cyan=36,white=37。

为了打印彩色文本,输入以下内容:

echo -e "\e[1;31m This is red text \e[0m"

这里\e[1;31是将颜色设置为红色的转义字符串,\e[0m将颜色重置。将31替换为所需的颜色代码。

对于彩色背景,reset = 0,black = 40,red = 41,green = 42,yellow = 43,blue = 44,magenta = 45,cyan = 46,white=47,是常用的颜色代码。

为了打印彩色背景,输入以下内容:

echo -e "\e[1;42m Green Background \e[0m"

玩弄变量和环境变量

变量是每种编程语言的基本组成部分,用于保存不同的数据。脚本语言通常不需要在使用之前声明变量类型。它可以直接分配。在 Bash 中,每个变量的值都是字符串。如果我们使用引号或不使用引号分配变量,它们将被存储为字符串。Shell 环境和操作系统环境使用特殊变量存储特殊值,这些变量称为环境变量。

让我们看看配方。

做好准备

变量使用通常的命名构造命名。当应用程序执行时,它将被传递一组称为环境变量的变量。从终端,要查看与该终端进程相关的所有环境变量,发出env命令。对于每个进程,可以通过其运行时查看环境变量:

cat /proc/$PID/environ

PID设置为相关进程的进程 ID(PID 始终是整数)。

例如,假设正在运行一个名为 gedit 的应用程序。我们可以使用pgrep命令获取 gedit 的进程 ID,如下所示:

$ pgrep gedit
12501

您可以通过执行以下命令获取与进程关联的环境变量:

$ cat /proc/12501/environ
GDM_KEYBOARD_LAYOUT=usGNOME_KEYRING_PID=1560USER=slynuxHOME=/home/slynux

请注意,为了方便起见,许多环境变量被剥离。实际输出可能包含大量变量。

上述命令返回环境变量及其值的列表。每个变量表示为name=value对,并用空字符(\0)分隔。如果可以用\n替换\0字符,可以重新格式化输出,以在每行显示每个variable=value对。可以使用tr命令进行替换,如下所示:

$ cat /proc/12501/environ  | tr '\0' '\n'

现在,让我们看看如何分配和操作变量和环境变量。

如何做到…

变量可以按以下方式分配:

var=value

var是变量的名称,value 是要分配的值。如果value不包含任何空格字符(如空格),则无需用引号括起来,否则必须用单引号或双引号括起来。

请注意,var = valuevar=value是不同的。常见错误是写var =value而不是var=value。后者是赋值操作,而前者是相等操作。

使用以下方式通过在变量名称前加上$来打印变量的内容:

var="value" #Assignment of value to variable var.

echo $var

或:

echo ${var}

输出如下:

value

我们可以在双引号中使用printfecho中的变量值。

#!/bin/bash
#Filename :variables.sh
fruit=apple
count=5
echo "We have $count ${fruit}(s)"

输出如下:

We have 5 apple(s)

环境变量是未在当前进程中定义的变量,而是从父进程接收的变量。例如,HTTP_PROXY是一个环境变量。此变量定义了应该为 Internet 连接使用哪个代理服务器。

通常,它被设置为:

HTTP_PROXY=http://192.168.0.2:3128
export HTTP_PROXY

使用 export 命令设置env变量。现在,从当前 shell 脚本执行的任何应用程序都将接收到这个变量。我们可以为我们自己的目的在执行的应用程序或 shell 脚本中导出自定义变量。默认情况下,shell 提供了许多标准环境变量。

例如,PATH。典型的PATH变量将包含:

$ echo $PATH
/home/slynux/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

当给出一个执行命令时,shell 会自动在PATH环境变量的目录列表中搜索可执行文件(目录路径由“:”字符分隔)。通常,$PATH/etc/environment/etc/profile~/.bashrc中定义。当我们需要向PATH环境添加新路径时,我们使用:

export PATH="$PATH:/home/user/bin"

或者,我们也可以使用:

$ PATH="$PATH:/home/user/bin"
$ export PATH

$ echo $PATH
/home/slynux/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/user/bin

在这里,我们已经将/home/user/bin添加到PATH

一些知名的环境变量是:HOMEPWDUSERUIDSHELL等。

还有更多...

让我们看看与常规和环境变量相关的一些其他提示。

查找字符串的长度

获取变量值的长度如下:

length=${#var}

例如:

$ var=12345678901234567890
$ echo ${#var} 
20

length是字符串中的字符数。

识别当前的 shell

显示当前使用的 shell 如下:

echo $SHELL

或者,您也可以使用:

echo $0

例如:

$ echo $SHELL
/bin/bash

$ echo $0
bash

检查超级用户

UID 是一个重要的环境变量,可以用来检查当前脚本是否以 root 用户或常规用户身份运行。例如:

if [ $UID -ne 0 ]; then
echo Non root user. Please run as root.
else
echo "Root user"
fi

根用户的 UID 为 0。

修改 Bash 提示字符串(用户名@主机名:~$)

当我们打开终端或运行 shell 时,我们会看到一个提示字符串,如user@hostname: /home/$。不同的 GNU/Linux 发行版有稍微不同的提示和不同的颜色。我们可以使用PS1环境变量自定义提示文本。shell 的默认提示文本是在~/.bashrc文件中设置的。

  • 我们可以列出用于设置PS1变量的行如下:
$ cat ~/.bashrc | grep PS1
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '

  • 为了设置自定义提示字符串,输入:
slynux@localhost: ~$ PS1="PROMPT>"
PROMPT> Type commands here # Prompt string changed.

  • 我们可以使用特殊的转义序列如\e[1;31(参考本章的在终端中打印)来使用彩色文本。

还有一些特殊字符可以扩展到系统参数。例如,\u扩展到用户名,\h扩展到主机名,\w扩展到当前工作目录。

使用 shell 进行数学计算

算术运算是每种编程语言的基本要求。Bash shell 具有各种进行算术运算的方法。

准备就绪

Bash shell 环境可以使用let(( ))[]命令执行基本算术运算。两个实用程序exprbc在执行高级操作时也非常有帮助。

如何做到...

可以将数值分配为常规变量赋值,它以字符串形式存储。但是,我们使用方法来操作数字。

#!/bin/bash
no1=4;
no2=5;

let命令可以直接执行基本操作。

在使用let时,我们使用不带$前缀的变量名,例如:

let result=no1+no2
echo $result
  • 递增操作:
$ let no1++

  • 递减操作:
$ let no1--

  • 缩写:
let no+=6
let no-=6

这些分别等同于let no=no+6let no=no-6

  • 备用方法:

[]操作符可以像let命令一样使用,如下所示:

result=$[ no1 + no2 ]

[]操作符内部使用$前缀是合法的,例如:

result=$[ $no1 + 5 ]

(( ))也可以使用。当使用(( ))运算符时,变量名前面加上$,如下所示:

result=$(( no1 + 50 ))

expr也可以用于基本操作:

result=`expr 3 + 4`
result=$(expr $no1 + 5)

所有上述方法都不支持浮点数,只能操作整数。

bc精度计算器是一个用于数学运算的高级实用程序。它具有广泛的选项。我们可以执行浮点运算并使用高级函数,如下所示:

echo "4 * 0.56" | bc
2.24

no=54; 
result=`echo "$no * 1.5" | bc`
echo $result
81.0

可以通过stdin通过分号作为分隔符将附加参数传递给bc

  • 指定十进制精度(scale):在下面的示例中,scale=2参数设置小数位数为2。因此,bc的输出将包含两位小数:
echo "scale=2;3/8" | bc
0.37

  • 使用 bc 进行基数转换:我们可以从一个基数系统转换为另一个基数系统。让我们从十进制转换为二进制,再从二进制转换为八进制:
#!/bin/bash
Description: Number conversion

no=100
echo "obase=2;$no" | bc
1100100
no=1100100
echo "obase=10;ibase=2;$no" | bc
100
  • 计算平方和平方根可以按如下方式进行:
echo "sqrt(100)" | bc #Square root
echo "10¹⁰" | bc #Square

玩转文件描述符和重定向

文件描述符是与文件输入和输出相关联的整数。它们跟踪已打开的文件。最常见的文件描述符是 stdinstdoutstderr。我们可以将一个文件描述符的内容重定向到另一个文件描述符。以下示例将演示如何使用文件描述符进行操作和重定向。

准备工作

在编写脚本时,我们经常使用标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。通过过滤内容将输出重定向到文件是我们需要执行的基本操作之一。当一个命令输出一些文本时,它可以是错误或输出(非错误)消息。我们无法仅仅通过查看文本来区分它是输出文本还是错误文本。但是,我们可以使用文件描述符来处理它们。我们可以提取附加到特定描述符的文本。

文件描述符是与打开的文件或数据流相关联的整数。文件描述符 0、1 和 2 分别保留如下:

  • 0 – stdin(标准输入)

  • 1 – stdout(标准输出)

  • 2 – stderr(标准错误)

如何做到...

将输出文本重定向或保存到文件可以按以下方式完成:

$ echo "This is a sample text 1" > temp.txt

这将通过截断文件将回显文本存储在 temp.txt 中,在写入之前将清空文件内容。

接下来,考虑以下示例:

$ echo "This is sample text 2" >> temp.txt

这将把文本追加到文件中。

>>> 运算符是不同的。它们两个都将文本重定向到文件,但第一个会清空文件然后写入,而后一个会将输出添加到现有文件的末尾。

查看文件内容如下:

$ cat temp.txt
This is sample text 1
This is sample text 2

当我们使用重定向运算符时,它不会在终端上打印,而是被重定向到文件。使用重定向运算符时,默认情况下会采用标准输出。为了显式地采用特定的文件描述符,必须在运算符前加上描述符号码。

> 相当于 1>,类似地,>> 也适用于 1>>

让我们看看标准错误是什么,以及你如何重定向它。当命令输出错误消息时,stderr 消息会被打印。考虑以下示例:

$ ls +
ls: cannot access +: No such file or directory

这里 + 是一个无效的参数,因此会返回一个错误。

提示

成功和不成功的命令

当一个命令返回错误后,它会返回一个非零的退出状态。当成功完成后终止时,命令返回零。退出状态可以从特殊变量 $? 中读取(在执行命令后立即运行 echo $? 语句以打印退出状态)。

以下命令将 stderr 文本打印到屏幕而不是文件中:

$ ls + > out.txt 
ls: cannot access +: No such file or directory 

然而,在以下命令中,stdout 输出为空,因此会生成一个空文件 out.txt

$ ls + 2> out.txt # works

你可以将 stderr 独占地重定向到一个文件,将 stdout 重定向到另一个文件,如下所示:

$ cmd 2>stderr.txt 1>stdout.txt

也可以通过将 stderr 转换为 stdout 的方式将 stderrstdout 重定向到单个文件:

$ cmd 2>&1 output.txt

或者另一种方法:

$ cmd &> output.txt 

有时输出可能包含不必要的信息(如调试消息)。如果你不希望输出终端负担着 stderr 的细节,那么你应该将 stderr 输出重定向到 /dev/null,这样可以完全删除它。例如,假设我们有三个文件 a1、a2 和 a3。然而,a1 对用户没有读写执行权限。当你需要打印以 a 开头的文件的内容时,可以使用 cat 命令。

设置测试文件如下:

$ echo a1 > a1 
$ cp a1 a2 ; cp a2 a3;
$ chmod 000 a1  #Deny all permissions

使用通配符(a*)显示文件内容时,对于文件 a1 会显示错误消息,因为它没有适当的读取权限:

$ cat a*
cat: a1: Permission denied
a1
a1

这里的 cat: a1: Permission denied 属于 stderr 数据。我们可以将 stderr 数据重定向到文件,而 stdout 仍然会在终端上打印。考虑以下代码:

$ cat a* 2> err.txt #stderr is redirected to err.txt
a1
a1

$ cat err.txt
cat: a1: Permission denied

看一下以下代码:

$ some_command 2> /dev/null

在这种情况下,stderr输出被转储到/dev/null文件。/dev/null是一个特殊的设备文件,其中接收到的任何数据都被丢弃。空设备通常被称为位桶或黑洞。

当对stderrstdout执行重定向时,重定向的文本流入文件。由于文本已经被重定向并且已经进入文件,没有文本剩下通过管道(|)流向下一个命令,它会出现在下一个命令序列的stdin中。

然而,有一种巧妙的方法可以将数据重定向到文件,并为下一组命令提供重定向数据的副本作为stdin。这可以使用tee命令来实现。例如,要在终端上打印stdout并将stdout重定向到文件,tee的语法如下:

command | tee FILE1 FILE2

在以下代码中,tee命令接收stdin数据。它将stdout的副本写入文件out.txt,并将另一个副本作为stdin发送给下一个命令。cat –n命令为从stdin接收的每一行添加行号,并将其写入stdout

$ cat a* | tee out.txt | cat -n
cat: a1: Permission denied
 1a1
 2a1

如下所示检查out.txt的内容:

$ cat out.txt
a1
a1

请注意,cat: a1: Permission denied不会出现,因为它属于stdintee只能从stdin读取。

默认情况下,tee命令会覆盖文件,但可以通过提供-a选项来使用追加选项,例如:

$ cat a* | tee –a out.txt | cat –n.

命令以以下格式出现:command FILE1 FILE2…或者简单地command FILE

我们可以使用stdin作为命令参数。可以通过使用-作为命令的文件名参数来实现:

$ cmd1 | cmd2 | cmd -

例如:

$ echo who is this | tee -
who is this
who is this

或者,我们可以使用/dev/stdin作为输出文件名来使用stdin

类似地,使用/dev/stderr作为标准错误和/dev/stdout作为标准输出。这些是对应于stdinstderrstdout的特殊设备文件。

还有更多...

读取stdin输入的命令可以以多种方式接收数据。还可以使用cat和管道指定我们自己的文件描述符,例如:

$ cat file | cmd
$ cmd1 | cmd2

从文件重定向到命令

通过重定向,我们可以从文件中读取数据作为stdin,如下所示:

$ cmd < file

从脚本中包含的文本块进行重定向

有时我们需要将一块文本(多行文本)重定向为标准输入。考虑一个特殊情况,源文本放在 shell 脚本中。一个实际的用例是写入日志文件头数据。可以按照以下步骤执行:

#!/bin/bash
cat <<EOF>log.txt
LOG FILE HEADER
This is a test log file
Function: System statistics
EOF

出现在cat <<EOF >log.txt和下一个EOF行之间的行将作为stdin数据出现。如下所示打印log.txt的内容:

$ cat log.txt
LOG FILE HEADER
This is a test log file
Function: System statistics

自定义文件描述符

文件描述符是访问文件的抽象指示器。每个文件访问都与一个称为文件描述符的特殊数字相关联。0、1 和 2 是stdinstdoutstderr的保留描述符号。

我们可以使用exec命令创建自定义文件描述符。如果您已经熟悉其他编程语言的文件编程,您可能已经注意到打开文件的模式。通常使用三种模式:

  • 读取模式

  • 使用截断模式写入

  • 使用追加模式写入

<是一个用于从文件读取到stdin的操作符。>是用于写入文件并截断的操作符(数据被写入目标文件后截断内容)。>>是用于追加写入文件的操作符(数据被追加到现有文件内容中,目标文件的内容不会丢失)。文件描述符可以用三种模式之一创建。

创建用于读取文件的文件描述符,如下所示:

$ exec 3<input.txt # open for reading with descriptor number 3

我们可以按以下方式使用它:

$ echo this is a test line > input.txt
$ exec 3<input.txt

现在可以使用文件描述符3与命令一起使用。例如,cat <&3如下所示:

$ cat <&3
this is a test line

如果需要进行第二次读取,则不能重用文件描述符3。需要使用exec重新分配文件描述符3进行读取,以进行第二次读取。

创建一个用于写入的文件描述符(截断模式)如下:

$ exec 4>output.txt # open for writing

例如:

$ exec 4>output.txt
$ echo newline >&4
$ cat output.txt
newline

创建一个用于写入的文件描述符(追加模式)如下:

$ exec 5>>input.txt

例如:

$ exec 5>>input.txt
$ echo appended line >&5
$ cat input.txt 
newline
appended line

数组和关联数组

数组是存储使用索引作为单独实体的数据集合的非常重要的组件。

准备工作

Bash 支持常规数组和关联数组。常规数组是只能使用整数作为数组索引的数组。但是,关联数组是可以将字符串作为其数组索引的数组。

关联数组在许多类型的操作中非常有用。关联数组支持是在 Bash 的 4.0 版本中引入的。因此,旧版本的 Bash 将不支持关联数组。

如何做...

数组可以以许多方式定义。可以按照一行中的值列表定义数组,如下所示:

array_var=(1 2 3 4 5 6)
#Values will be stored in consecutive locations starting from index 0.

或者,可以按如下方式定义数组为一组索引-值对:

array_var[0]="test1"
array_var[1]="test2"
array_var[2]="test3"
array_var[3]="test4"
array_var[4]="test5"
array_var[5]="test6"

使用以下方法在给定索引处打印数组的内容:

$ echo ${array_var[0]}
test1
index=5
$ echo ${array_var[$index]}
test6

使用以下方法将数组中的所有值打印为列表:

$ echo ${array_var[*]}
test1 test2 test3 test4 test5 test6

或者,您可以使用:

$ echo ${array_var[@]}
test1 test2 test3 test4 test5 test6

按如下方式打印数组的长度(数组中的元素数):

$ echo ${#array_var[*]}
6

还有更多...

从 4.0 版本开始,Bash 引入了关联数组。它们是使用哈希技术解决许多问题的有用实体。让我们深入了解一下。

定义关联数组

在关联数组中,我们可以使用任何文本数据作为数组索引。但是,普通数组只能使用整数作为数组索引。

首先需要声明语句来将变量名声明为关联数组。声明可以如下进行:

$ declare -A ass_array

声明后,可以使用以下两种方法向关联数组添加元素:

  1. 通过使用内联索引-值列表方法,我们可以提供索引-值对的列表:
$ ass_array=([index1]=val1 [index2]=val2)

  1. 或者,您可以使用单独的索引-值赋值:
$ ass_array[index1]=val1
$ ass_array[index2]=val2

例如,考虑使用关联数组为水果分配价格:

$ declare -A fruits_value
$ fruits_value=([apple]='100dollars' [orange]='150 dollars')

按如下方式显示数组的内容:

$ echo "Apple costs ${fruits_value[apple]}"
Apple costs 100 dollars

数组索引列表

数组具有用于索引每个元素的索引。普通数组和关联数组在索引类型方面有所不同。我们可以按如下方式获取数组中的索引列表:

$ echo ${!array_var[*]}

或者,我们也可以使用:

$ echo ${!array_var[@]}

在先前的fruits_value数组示例中,考虑以下内容:

$ echo ${!fruits_value[*]}
orange apple

这也适用于普通数组。

访问别名

别名基本上是一个快捷方式,可以代替输入一长串命令。

准备工作

别名可以通过多种方式实现,可以使用函数或使用alias命令。

如何做...

别名可以按如下方式实现:

$ alias new_command='command sequence'

install命令提供快捷方式apt-get install,可以按如下方式完成:

$ alias install='sudo apt-get install'

因此,我们可以使用install pidgin代替sudo apt-get install pidgin

alias命令是临时的;别名仅在关闭当前终端之前存在。为了使这些快捷方式永久存在,将此语句添加到~/.bashrc文件中。在新的 shell 进程生成时,~/.bashrc中的命令总是被执行。

$ echo 'alias cmd="command seq"' >> ~/.bashrc

要删除别名,从~/.bashrc中删除其条目或使用unalias命令。另一种方法是定义一个具有新命令名称的函数,并将其写入~/.bashrc

我们可以将rm别名为删除原始文件并将其保留在备份目录中:

alias rm='cp $@ ~/backup; rm $@'

当创建别名时,如果要别名化的项目已经存在,则它将被新别名的命令替换为该用户的命令。

还有更多...

有时别名也可能会造成安全漏洞。看看如何识别它们:

转义别名

alias命令可用于为任何重要命令设置别名,并且您可能并不总是希望使用别名运行命令。我们可以通过转义要运行的命令来忽略当前定义的任何别名。例如:

$ \command

\字符转义命令,使其在没有任何别名更改的情况下运行。在不受信任的环境中运行特权命令时,通过在命令前加上\来忽略别名始终是一个良好的安全实践。攻击者可能已经使用自己的自定义命令将特权命令别名化,以窃取用户提供给命令的关键信息。

获取有关终端的信息

在编写命令行 shell 脚本时,我们经常需要大量操作有关当前终端的信息,例如列数、行数、光标位置、掩码密码字段等。本教程将帮助您了解收集和操作终端设置。

准备好

tputstty是可用于终端操作的实用程序。让我们看看如何使用它们执行不同的任务。

如何做...

获取终端中的列数和行数如下:

tput cols
tput lines

为了打印当前的终端名称,使用:

tput longname

要将光标移动到位置 100,100,可以输入:

tput cup 100 100

将终端的背景颜色设置如下:

tput setb no

no可以是 0 到 7 范围内的值。

将文本的前景颜色设置如下:

tput setf no

no可以是 0 到 7 范围内的值。

为了使文本加粗使用:

tput bold

通过使用以下方式开始和结束下划线:

tput smul
tput rmul

为了从光标删除到行尾,请使用:

tput ed

在输入密码时,我们不应该显示已输入的字符。在以下示例中,我们将看到如何使用stty来实现:

#!/bin/sh
#Filename: password.sh
echo -e "Enter password: "
stty -echo
read password
stty echo
echo
echo Password read.

上面的-echo选项禁用了对终端的输出,而echo则启用了输出。

获取、设置日期和延迟

许多应用程序需要以不同的格式打印日期,设置日期和时间,并根据日期和时间执行操作。延迟通常用于在程序执行期间提供等待时间(例如,1 秒)。脚本上下文,例如每五秒执行一次监视任务,需要理解如何在程序中编写延迟。本教程将向您展示如何处理日期和时间延迟。

准备好

日期可以以各种格式打印。我们还可以从命令行设置日期。在类 UNIX 系统中,日期以自 1970-01-01 00:00:00 UTC 以来的秒数存储为整数。这称为纪元或 UNIX 时间。让我们看看如何读取日期并设置日期。

如何做...

您可以按如下方式读取日期:

$ date
Thu May 20 23:09:04 IST 2010

纪元时间可以打印如下:

$ date +%s
1290047248

纪元被定义为自 1970 年 1 月 1 日协调世界时(UTC)午夜以来经过的秒数,不包括闰秒。纪元时间在需要计算两个日期或时间之间的差异时非常有用。您可以找出两个给定时间戳的纪元时间,并计算纪元值之间的差异。因此,您可以找出两个日期之间的总秒数。

我们可以从给定的格式化日期字符串中找出纪元。您可以使用多种日期格式作为输入。通常,如果您从系统日志或任何标准应用程序生成的输出中收集日期,您不需要担心使用的日期字符串格式。您可以按如下方式将日期字符串转换为纪元:

$ date --date "Thu Nov 18 08:07:21 IST 2010" +%s
1290047841

--date选项用于提供日期字符串作为输入。但是,我们可以使用任何日期格式选项来打印输出。从字符串中提取输入日期可以用于找出给定日期的工作日。

例如:

$ date --date "Jan 20 2001" +%A
Saturday

日期格式字符串列在以下表中:

日期组件 格式
工作日 %a(例如:Sat)%A(例如:Saturday)
%b(例如:Nov)%B(例如:November)
%d(例如:31)
格式中的日期(mm/dd/yy) %D(例如:10/18/10)
%y(例如:10)%Y(例如:2010)
小时 %I%H(例如:08)
分钟 %M(例如:33)
%S(例如:10)
纳秒 %N(例如:695208515)
以秒为单位的 UNIX 时间戳 %s(例如:1290049486)

使用以+为前缀的格式字符串的组合作为date命令的参数,以打印所需格式的日期。例如:

$ date "+%d %B %Y"
20 May 2010

我们可以按如下方式设置日期和时间:

# date -s "Formatted date string"

例如:

# date -s "21 June 2009 11:01:22"

有时我们需要检查一组命令所花费的时间。我们可以按如下方式显示它:

#!/bin/bash
#Filename: time_take.sh
start=$(date +%s)
commands;
statements;

end=$(date +%s)
difference=$(( end - start))
echo Time taken to execute commands is $difference seconds.

另一种方法是使用timescriptpath来获取执行脚本所需的时间。

还有更多...

在编写循环执行的监控脚本时,生成时间间隔是必不可少的。让我们看看如何生成时间延迟。

在脚本中产生延迟

为了在脚本中延迟执行一段时间,使用sleep:$ sleep no_of_seconds

例如,以下脚本使用tputsleep从 0 计数到 40:

#!/bin/bash
#Filename: sleep.sh
echo -n Count:
tput sc

count=0;
while true;
do
if [ $x -lt 40 ];
then let count++;
sleep 1;
tput rc
tput ed
echo -n $count;
else exit 0;
fi
done

在上面的示例中,一个变量 count 被初始化为 0,并在每次循环执行时递增。echo语句打印文本。我们使用tput sc来存储光标位置。在每次循环执行时,我们通过恢复数字的光标位置在终端中写入新的计数。使用tput rc来恢复光标位置。tput ed清除当前光标位置到行尾的文本,以便清除旧数字并写入计数。使用sleep命令在循环中提供 1 秒的延迟。

调试脚本

调试是每种编程语言都应该实现的关键功能之一,以在发生意外情况时产生回溯信息。调试信息可用于阅读和理解导致程序崩溃或以意外方式行事的原因。Bash 提供了一些调试选项,每个系统管理员都应该知道。还有一些其他巧妙的调试方法。

准备工作

调试 shell 脚本不需要特殊的实用程序。Bash 带有一些标志,可以打印脚本接受的参数和输入。让我们看看如何做。

如何做...

添加-x选项以启用 shell 脚本的调试跟踪,如下所示:

$ bash -x script.sh

使用-x标志运行脚本将打印每个源代码行的当前状态。请注意,您还可以使用sh –x script

-x标志会将脚本的每一行在执行时输出到stdout。但是,我们可能只需要观察源代码的某些部分,以便在某些部分打印命令和参数。在这种情况下,我们可以使用set 内置在脚本中启用和禁用调试打印。

  • set -x:在执行时显示参数和命令

  • set +x:禁用调试

  • set –v:在读取输入时显示输入

  • set +v:禁止打印输入

例如:

#!/bin/bash
#Filename: debug.sh
for i in {1..6}
do
set -x
echo $i
set +x
done
echo "Script executed"

在上述脚本中,echo $i的调试信息只会在该部分使用-x+x限制调试时打印。

上述调试方法由 bash 内置提供。但它们总是以固定格式产生调试信息。在许多情况下,我们需要以自己的格式获得调试信息。我们可以通过传递_DEBUG环境变量来设置这样的调试样式。

看看以下示例代码:

#!/bin/bash
function DEBUG()
{
[ "$_DEBUG" == "on" ] && $@ || :
}

for i in {1..10}
do
DEBUG echo $i
done

我们可以按如下方式运行上述脚本,调试设置为“on”:

$ _DEBUG=on ./script.sh

我们在每个需要打印调试信息的语句前加上DEBUG前缀。如果未将_DEBUG=on传递给脚本,则不会打印调试信息。在 Bash 中,命令:告诉 shell 什么都不做。

还有更多...

我们还可以使用其他方便的方法来调试脚本。我们可以巧妙地利用 shebang 来调试脚本。

Shebang hack

shebang 可以从#!/bin/bash更改为#!/bin/bash –xv,以启用调试而无需任何额外的标志(-xv标志本身)。

函数和参数

与其他脚本语言一样,Bash 也支持函数。让我们看看如何定义和使用函数。

如何做...

可以定义函数如下:

function fname()
{
statements;
}

或者,

fname()
{
statements;
}

函数可以通过使用其名称来调用:

$ fname ; # executes function

参数可以传递给函数,并且可以被我们的脚本访问:

fname arg1 arg2 ; # passing args

以下是函数fname的定义。在fname函数中,我们已经包含了各种访问函数参数的方式。

fname()
{
  echo $1, $2; #Accessing arg1 and arg2
  echo "$@"; # Printing all arguments as list at once
  echo "$*"; # Similar to $@, but arguments taken as single entity
  return 0; # Return value
}

类似地,参数可以传递给脚本,并且可以通过script:$0(脚本的名称)访问:

  • $1是第一个参数

  • $2是第二个参数

  • $n是第 n 个参数

  • "$@"扩展为"$1" "$2" "$3"等等

  • "$*"扩展为"$1c$2c$3",其中c是 IFS 的第一个字符

  • "$@"是最常用的。"$*"很少使用,因为它将所有参数作为单个字符串。

还有更多...

让我们探索更多有关 Bash 函数的提示。

递归函数

Bash 中的函数也支持递归(可以调用自身的函数)。例如,F() { echo $1; F hello; sleep 1; }

提示

分叉炸弹

😦){ 😐:& };:

这个递归函数是一个调用自身的函数。它无限地生成进程,并最终导致拒绝服务攻击。在函数调用后加上&将子进程放入后台。这是一个危险的代码,因为它会分叉进程,因此被称为分叉炸弹。

你可能会发现难以解释上面的代码。请参阅维基百科页面en.wikipedia.org/wiki/Fork_bomb了解更多关于分叉炸弹的细节和解释。

可以通过限制从配置文件/etc/security/limits.conf生成的最大进程数来防止它。

导出函数

可以使用export导出函数,就像导出环境变量一样,这样函数的范围可以扩展到子进程,如下所示:

export -f fname

读取命令返回值(状态)

我们可以通过以下方式获取命令或函数的返回值:

cmd; 
echo $?;

$?将给出命令cmd的返回值。

返回值称为退出状态。它可以用来分析命令是否成功执行完成。如果命令成功退出,退出状态将为零,否则将为非零。

我们可以通过以下方式检查命令是否成功终止:

#!/bin/bash
#Filename: success_test.sh
CMD="command" #Substitute with command for which you need to test exit status
$CMD
if [ $? –eq 0 ];
then
echo "$CMD executed successfully"
else
echo "$CMD terminated unsuccessfully"
fi

将参数传递给命令

命令的参数可以以不同的格式传递。假设-p-v是可用的选项,-k NO是另一个需要一个数字的选项。此外,命令需要一个文件名作为参数。可以按以下方式执行:

$ command -p -v -k 1 file

或者:

$ command -pv -k 1 file

或者:

$ command -vpk 1 file

或者:

$ command file -pvk 1

读取一系列命令的输出

Shell 脚本设计的最佳特性之一是轻松组合多个命令或实用程序以产生输出。一个命令的输出可以作为另一个命令的输入,后者将其输出传递给另一个命令,依此类推。这种组合的输出可以在一个变量中读取。本教程说明了如何组合多个命令以及如何读取其输出。

准备就绪

通常通过stdin或参数将输入提供给命令。输出显示为stderrstdout。当我们组合多个命令时,我们通常使用stdin提供输入和stdout提供输出。

命令被称为过滤器。我们使用管道连接每个过滤器。管道操作符是"|"。例如:

$ cmd1 | cmd2 | cmd3 

这里我们结合了三个命令。cmd1的输出传递给cmd2cmd2的输出传递给cmd3,最终输出(来自cmd3)将被打印或可以被重定向到文件。

如何做到...

看一下以下代码:

$ ls | cat -n > out.txt

这里ls的输出(当前目录的列表)被传递给cat -ncat –n为通过stdin接收的输入添加行号。因此,它的输出被重定向到out.txt文件。

我们可以读取由管道组合的一系列命令的输出,如下所示:

cmd_output=$(COMMANDS)

这被称为子 shell 方法。例如:

cmd_output=$(ls | cat -n)
echo $cmd_output

另一种方法,称为反引号,也可以用来存储命令的输出,如下所示:

cmd_output=`COMMANDS`

例如:

cmd_output=`ls | cat -n`
echo $cmd_output

反引号与单引号字符不同。它是键盘上~按钮上的字符。

还有更多...

有多种组合命令的方法。让我们看一些。

使用子 shell 生成一个单独的进程

子 shell 是独立的进程。可以使用( )操作符定义子 shell,如下所示:

pwd;
(cd /bin; ls);
pwd;

当在子 shell 中执行某些命令时,当前 shell 中不会发生任何更改;更改仅限于子 shell。例如,在子 shell 中使用cd命令更改当前目录时,目录更改不会反映在主 shell 环境中。

pwd命令打印工作目录的路径。

cd命令将当前目录更改为给定的目录路径。

使用子 shell 引用以保留间距和换行字符

假设我们正在使用子 shell 或反引号方法将命令的输出读取到一个变量中,我们总是用双引号引起来以保留间距和换行字符(\n)。例如:

$ cat text.txt
1
2
3

$ out=$(cat text.txt)
$ echo $out
1 2 3 # Lost \n spacing in 1,2,3 

$ out="$(cat tex.txt)"
$ echo $out
1
2
3

读取“n”个字符而不按回车键

read是一个重要的 Bash 命令,可用于从键盘或标准输入读取文本。我们可以使用read来交互式地从用户那里读取输入,但read还可以做更多。让我们看一个新的示例来说明read命令提供的一些最重要的选项。

准备工作

任何编程语言中的大多数输入库都是从键盘读取输入;但是当按下Return时,字符串输入终止。在某些关键情况下,无法按下Return,但终止是基于字符数或单个字符完成的。例如,在游戏中,按下上+时,球会向上移动。每次按下+然后按下Return来确认+按下是不高效的。read命令提供了一种在不必按下Return的情况下完成此任务的方法。

如何做...

以下语句将从输入中读取“n”个字符到变量variable_name中:

read -n number_of_chars variable_name

例如:

$ read -n 2 var
$ echo $var

read还有许多其他选项。让我们看看这些。

按照以下方式以非回显模式读取密码:

read -s var

使用read显示消息:

read -p "Enter input:"  var

按照以下方式在超时后读取输入:

read -t timeout var

例如:

$ read -t 2 var
#Read the string that is typed within 2 seconds into variable var.

使用分隔符字符来结束输入行,如下所示:

read -d delim_charvar

例如:

$ read -d ":" var
hello:#var is set to hello

字段分隔符和迭代器

内部字段分隔符是 shell 脚本中的一个重要概念。在操作文本数据时非常有用。我们现在将讨论分隔符,它们将不同的数据元素从单个数据流中分隔开。内部字段分隔符是一个特殊目的的分隔符。内部字段分隔符IFS)是一个存储分隔字符的环境变量。它是运行 shell 环境使用的默认分隔符字符串。

考虑需要在字符串或逗号分隔值CSV)中迭代单词的情况。在第一种情况下,我们将使用IFS=" ",在第二种情况下,IFS=","。让我们看看如何做。

准备工作

考虑 CSV 数据的情况:

data="name,sex,rollno,location"
#To read each of the item in a variable, we can use IFS.
oldIFS=$IFS
IFS=, now,
for item in $data;
do
echo Item: $item
done

IFS=$oldIFS

输出如下:

Item: name
Item: sex
Item: rollno
Item: location

IFS 的默认值是一个空格组件(换行符、制表符或空格字符)。

当 IFS 设置为“,”时,shell 将逗号解释为分隔符字符,因此在迭代期间,$item变量将以逗号分隔的子字符串作为其值。

如果 IFS 未设置为“,”,那么它将打印整个数据作为单个字符串。

如何做...

让我们通过考虑/etc/passwd文件来看一下 IFS 的另一个示例用法。在/etc/passwd文件中,每一行包含由":"分隔的项目。文件中的每一行对应于与用户相关的属性。

考虑输入:root:x:0:0:root:/root:/bin/bash。每行的最后一个条目指定用户的默认 shell。为了打印用户及其默认 shell,我们可以使用 IFS hack,如下所示:

#!/bin/bash
#Description: Illustration of IFS
line="root:x:0:0:root:/root:/bin/bash" 
oldIFS=$IFS;
IFS=":"
count=0
for item in $line;
do

[ $count -eq 0 ]  && user=$item;
[ $count -eq 6 ]  && shell=$item;
let count++
done;
IFS=$oldIFS
echo $user\'s shell is $shell;

输出将是:

root's shell is /bin/bash

循环在迭代一系列值时非常有用。Bash 提供了许多类型的循环。让我们看看如何使用它们。

For 循环

for var in list;
do
commands; # use $var
done
list can be a string, or a sequence.

我们可以轻松生成不同的序列。

echo {1..50}可以生成从 1 到 50 的数字列表

echo {a..z}{A..Z}或我们可以使用{a..h}生成部分列表。类似地,通过组合这些,我们可以连接数据。

在以下代码中,每次迭代,变量i将保存范围为az的字符:

for i in {a..z}; do actions; done;

for循环也可以采用 C 语言中的for循环格式。例如:

for((i=0;i<10;i++))
{
commands; # Use $i
}

While 循环

while condition
do
commands;
done

对于无限循环,使用true作为条件。

Until 循环

Bash 中还有一个称为until的特殊循环。这将执行循环,直到给定条件成为真。例如:

x=0;
until [ $x -eq 9 ]; # [ $x -eq 9 ] is the condition
do let x++; echo $x;
done

比较和测试

程序中的流程控制由比较和测试语句处理。Bash 还提供了几种选项来执行与 UNIX 系统级特性兼容的测试。

准备工作

我们可以使用ifif else和逻辑运算符来执行测试,并使用某些比较运算符来比较数据项。还有一个称为test的命令可用于执行测试。让我们看看如何使用这些命令。

如何做...

如果条件:

if condition;
then
commands;
fi

else if 和 else:

if condition; 
then
commands;
elif condition; 
then
    commands
else
    commands
fi

嵌套也是可能的,使用 if 和 else。if条件可能很长。我们可以使用逻辑运算符使它们更短,如下所示:

[ condition ] && action; # action executes if condition is true.
[ condition ] || action; # action executes if condition is false.

&&是逻辑 AND 操作,||是逻辑 OR 操作。在编写 Bash 脚本时,这是一个非常有用的技巧。现在让我们进入条件和比较操作。

数学比较

通常,条件被括在方括号[]中。请注意[]和操作数之间有一个空格。如果没有提供空格,将会显示错误。示例如下:

[ $var -eq 0 ] or [ $var -eq 0 ]

可以按照以下方式执行变量或值的数学条件:

[ $var -eq 0 ]  # It returns true when $var equal to 0.
[ $var -ne 0 ] # It returns true when $var not equals 0

其他重要的操作符包括:

  • -gt:大于

  • -lt:小于

  • -ge:大于或等于

  • -le:小于或等于

多个测试条件可以组合如下:

[ $var1 -ne 0 -a $var2 -gt 2 ]  # using AND -a
[ $var -ne 0 -o var2 -gt 2 ] # OR -o

与文件系统相关的测试

我们可以使用不同的条件标志来测试不同的文件系统相关属性,如下所示:

  • [ -f $file_var ]: 如果给定的变量保存了常规文件路径或文件名,则返回 true。

  • [ -x $var ]: 如果给定的变量保存了可执行文件路径或文件名,则返回 true。

  • [ -d $var ]: 如果给定的变量保存了目录路径或目录名称,则返回 true。

  • [ -e $var ]: 如果给定的变量保存了一个现有文件,则返回 true。

  • [ -c $var ]: 如果给定的变量保存了字符设备文件的路径,则返回 true。

  • [ -b $var ]: 如果给定的变量保存了块设备文件的路径,则返回 true。

  • [ -w $var ]: 如果给定的变量保存了可写文件的路径,则返回 true。

  • [ -r $var ]: 如果给定的变量保存了可读文件的路径,则返回 true。

  • [ -L $var ]: 如果给定的变量保存了符号链接的路径,则返回 true。

使用示例如下:

fpath="/etc/passwd"
if [ -e $fpath ]; then
echo File exists; 
else
echo Does not exist; 
fi

字符串比较

在使用字符串比较时,最好使用双方括号,因为使用单方括号有时会导致错误。有时使用单方括号会导致错误。因此最好避免使用它们。

可以比较两个字符串是否相同,如下所示;

  • [[ $str1 = $str2 ]]: 当 str1 等于 str2 时返回 true,即 str1 和 str2 的文本内容相同

  • [[ $str1 == $str2 ]]: 这是字符串相等检查的替代方法

我们可以检查两个字符串是否不相同,如下所示:

  • [[ $str1 != $str2 ]]: 当 str1 和 str2 不匹配时返回 true

我们可以找出字母顺序较小或较大的字符串,如下所示:

  • [[ $str1 > $str2 ]]: 当 str1 按字母顺序大于 str2 时返回 true

  • [[ $str1 < $str2 ]]: 当 str1 按字母顺序小于 str2 时返回 true

注意

请注意,在=之后和之前都提供了一个空格。如果不提供空格,它不是一个比较,而是一个赋值语句。

  • [[ -z $str1 ]]:如果 str1 包含空字符串,则返回 true

  • [[ -n $str1 ]]:如果 str1 包含非空字符串,则返回 true

使用逻辑运算符&&||可以更容易地组合多个条件,如下所示:

if [[ -n $str1 ]] && [[ -z $str2 ]] ;
then
commands;
fi

例如:

str1="Not empty "
str2=""
if [[ -n $str1 ]] && [[ -z $str2 ]];
then
echo str1 is non-empty and str2 is empty string.
fi

输出如下:

str1 is non-empty and str2 is empty string.

test命令可用于执行条件检查。它有助于避免使用许多大括号。可以在test命令中使用相同的一组条件,这些条件被包含在[]中。

例如:

if  [ $var -eq 0 ]; then echo "True"; fi
can be written as
if  test $var -eq 0 ; then echo "True"; fi

第二章:拥有一个好的命令

在本章中,我们将涵盖:

  • 使用 cat 进行连接

  • 记录和回放终端会话

  • 查找文件和文件列表

  • 将命令输出作为命令的参数(xargs)

  • 使用 tr 进行翻译

  • 校验和和验证

  • 排序、唯一和重复

  • 临时文件命名和随机数

  • 拆分文件和数据

  • 基于扩展名切分文件名

  • 使用 rename 和 mv 批量重命名文件

  • 拼写检查和字典操作

  • 自动化交互输入

介绍

命令是 UNIX-like 系统中美丽的组件。它们帮助我们完成许多任务,使我们的工作更加轻松。当你在任何地方练习使用命令时,你会喜欢它。许多情况会让你说“哇!”一旦你有机会尝试 Linux 提供的一些命令,使你的生活更轻松和更高效,你会想知道在没有使用它们之前你是如何做的。我个人最喜欢的一些命令是grepawksedfind

使用 UNIX/Linux 命令行是一门艺术。随着练习和经验的积累,你会变得更擅长使用它。本章将向您介绍一些最有趣和有用的命令。

使用 cat 进行连接

cat是命令行战士必须学习的第一批命令之一。cat是一个美丽而简单的命令。它通常用于读取、显示或连接文件的内容,但cat不仅仅是这样。

准备工作

当我们需要使用单行命令将标准输入数据与文件数据合并时,我们会感到困惑。将stdin数据与文件数据合并的常规方法是将stdin重定向到文件,然后追加两个文件。但是我们可以使用cat命令在单个调用中轻松完成。

如何做到…

cat命令是一个非常简单的命令,在日常生活中经常使用。cat代表连接。

读取文件内容的cat的一般语法是:

$ cat file1 file2 file3 ...

该命令输出作为命令行参数提供的文件的连接数据。例如:

$ cat file.txt
This is a line inside file.txt
This is the second line inside file.txt

它是如何工作的…

有很多功能与cat一起提供。让我们一起走过几种可能的cat使用技巧。

cat命令不仅可以从文件中读取和连接数据,还可以从标准输入中读取输入。

为了从标准输入中读取,使用管道操作符如下:

某些命令的输出 | cat

同样,我们可以使用cat将输入文件的内容与标准输入一起连接起来。将stdin和另一个文件的数据组合起来,如下所示:

$ echo 'Text through stdin' | cat – file.txt

在这段代码中,-充当stdin文本的文件名。

还有更多…

cat命令还有其他一些查看文件的选项。让我们一起来看看。

压缩空白行

有时文本中的许多空行需要压缩成一个以便阅读或其他目的。使用以下语法在文本文件中压缩相邻的空行:

$ cat -s file

例如:

$ cat multi_blanks.txt
line 1

line2

line3

line4

$ cat -s multi_blanks.txt # Squeeze adjacent blank lines
line 1

line2

line3

line4

或者,我们可以使用tr来删除所有空行,如下所示:

$ cat multi_blanks.txt | tr -s '\n'
line 1
line2
line3
line4

在上面的tr使用中,它将相邻的'\n'字符压缩为单个'\n'(换行符)。

显示制表符为^I

很难区分制表符和重复的空格字符。在编写像 Python 这样的语言的程序时,制表符和空格对缩进目的具有特殊意义。它们被不同对待。因此,使用制表符而不是空格会导致缩进问题。通过查看文本编辑器,可能很难跟踪制表符或空格的错误放置位置。cat有一个可以突出显示制表符的功能。这在调试缩进错误时非常有帮助。使用cat-T选项来突出显示制表符字符为^I。例如:

$ cat file.py
def function():
 var = 5
 next = 6
 third = 7

$ cat -T file.py
def function():
^Ivar = 5
 next = 6
^Ithird = 7^I

行号

cat命令使用-n标志将输出每一行的行号前缀。需要注意的是,cat命令永远不会更改文件;相反,它根据提供的选项对输入进行修改,并在stdout上产生输出。例如:

$ cat lines.txt
line 
line
line

$ cat -n lines.txt
 1	line
 2	line
 3	line

记录和重播终端会话

当您需要向某人展示如何在终端中执行某项操作,或者需要准备关于如何通过命令行执行某项操作的教程时,通常会手动输入命令并展示给他们。或者您可以录制屏幕录像并向他们播放视频。如果我们可以记录之前输入的命令的顺序和时间,并重新播放这些命令,以便其他人可以观看并仿佛他们正在输入呢?命令的输出将显示在终端上,直到播放完成。听起来有趣吗?可以使用scriptscriptreplay命令来实现。

准备工作

scriptscriptreplay命令在大多数 GNU/Linux 发行版中都可用。将终端会话记录到文件中会很有趣。您可以通过记录终端会话来创建命令行技巧和窍门的教程,以实现某些任务。您还可以分享记录的文件供他人回放,并了解如何使用命令行执行特定任务。

如何做…

我们可以开始记录终端会话,如下所示:

$ script -t 2> timing.log -a output.session
type commands;
…
..
exit

两个配置文件作为参数传递给script命令。一个文件用于存储每个命令运行的时间信息(timing.log),而另一个文件(output.session)用于存储命令输出。使用-t标志将时间数据转储到stderr。使用2>stderr重定向到timing.log

通过使用两个文件,timing.log(存储时间信息)和output.session(存储命令输出信息),我们可以按以下方式重播命令执行的顺序:

$ scriptreplay timing.log output.session
# Plays the sequence of commands and output

它是如何工作的…

通常,我们会录制桌面视频来准备教程。但是,视频需要大量的存储空间。但是终端脚本文件只是一个文本文件。因此,它的文件大小通常只有几千字节。

您可以与任何想要在他们的终端中重播终端会话的人分享文件timing.logoutput.session

script命令也可以用于设置可以广播给多个用户的终端会话。这是一种非常有趣的体验。让我们看看如何做。

打开两个终端,Terminal1 和 Terminal2。

  1. 在 Terminal1 中输入以下命令:
$ mkfifo scriptfifo

  1. 在 Terminal2 中输入以下命令:
$ cat scriptfifo

  1. 返回到 Terminal1 并输入以下命令:
$ script -f scriptfifo
$ commands;

当您需要结束会话时,请输入exit并按Return。它将显示消息“脚本完成,文件为 scriptfifo”。

现在 Terminal1 是广播者,Terminal2 是接收者。

当您在 Terminal1 上实时输入任何内容时,它将在 Terminal2 或任何提供以下命令的终端上播放:

cat scriptfifo

在计算机实验室或互联网上处理多个用户的教程会话时,可以使用此方法。这将节省带宽,并提供实时体验。

查找文件和文件列表

find是 UNIX/Linux 命令行工具箱中的伟大实用程序之一。它是 shell 脚本的非常有用的命令,但由于缺乏理解,大多数人并不有效地使用它。本教程涵盖了find的大多数用例以及如何使用它来解决不同标准的问题。

准备工作

find命令使用以下策略:find通过文件层次结构进行匹配,匹配符合指定条件的文件,并执行一些操作。让我们看看find的不同用例和基本用法。

如何做…

为了列出当前目录到下降子目录中的所有文件和文件夹,请使用以下语法:

$ find base_path

base_path可以是find应该开始下降的任何位置(例如,/home/slynux/)。

此命令的一个示例如下:

$ find . -print
# Print lists of files and folders

.指定当前目录,..指定父目录。这个约定贯穿整个 UNIX 文件系统。

-print参数指定打印匹配文件的名称(路径)。当使用-print时,'\n'将是分隔每个文件的定界字符。

-print0参数指定每个匹配文件名都用定界字符'\0'打印。当文件名包含空格字符时,这是有用的。

还有更多...

在这个示例中,我们学习了最常用的find命令的用法。find命令是一个强大的命令行工具,它配备了各种有趣的选项。让我们来看看find命令的一些不同选项。

基于文件名或正则表达式匹配的搜索

-name参数指定文件名的匹配字符串。我们可以将通配符作为其参数文本传递。*.txt匹配所有以.txt结尾的文件名并将它们打印出来。-print选项在终端中打印与给定为find命令选项的条件(例如,-name)匹配的文件名或文件路径。

$ find /home/slynux -name "*.txt" –print

find命令有一个–iname(忽略大小写)选项,它类似于-name–iname匹配名称时忽略大小写。

例如:

$ ls
example.txt  EXAMPLE.txt  file.txt
$ find . -iname "example*" -print
./example.txt
./EXAMPLE.txt

如果我们想匹配多个条件中的任何一个,我们可以使用 OR 条件,如下所示:

$ ls
new.txt  some.jpg  text.pdf
$ find . \( -name "*.txt" -o -name "*.pdf" \) -print
./text.pdf
./new.txt

上面的代码将打印所有.txt.pdf文件,因为find命令匹配.txt.pdf文件。\(\)用于将-name "*.txt" -o -name "*.pdf"视为单个单元。

-path参数可用于匹配与通配符匹配的文件路径。-name始终使用给定的文件名进行匹配。但是,-path匹配整个文件路径。例如:

$ find /home/users -path "*slynux*" -print
This will match files as following paths.
/home/users/list/slynux.txt
/home/users/slynux/eg.css

-regex参数类似于-path,但-regex根据正则表达式匹配文件路径。

正则表达式是通配符匹配的高级形式。它使我们能够指定带有模式的文本。通过使用这些模式,我们可以匹配文本并将其打印出来。使用正则表达式进行文本匹配的典型示例是:从给定的文本池中解析所有电子邮件地址。电子邮件地址采用name@host.root的形式。因此,它可以概括为[a-z0-9]+@[a-z0-9]+.[a-z0-9]++表示前一个字符类可以在后面的字符中重复一次或多次。

以下命令匹配.py.sh文件:

$ ls
new.PY  next.jpg  test.py
$ find . -regex ".*\(\.py\|\.sh\)$"
./test.py

类似地,使用-iregex忽略了可用的正则表达式的大小写。例如:

$ find . -iregex ".*\(\.py\|\.sh\)$"
./test.py
./new.PY

否定参数

find还可以使用“!”来否定参数。例如:

$ find . ! -name "*.txt" -print

上述find构造匹配所有文件名,只要名称不以.txt结尾。以下示例显示了该命令的结果:

$ ls
list.txt  new.PY  new.txt  next.jpg  test.py

$ find . ! -name "*.txt" -print
.
./next.jpg
./test.py
./new.PY

基于目录深度的搜索

当使用find命令时,它会递归地遍历所有子目录,直到尽可能地达到子目录树的叶子。我们可以通过给find命令一些深度参数来限制find命令遍历的深度。-maxdepth-mindepth是这些参数。

在大多数情况下,我们只需要在当前目录中搜索。它不应该进一步进入当前目录的子目录。在这种情况下,我们可以通过深度参数限制find命令应该下降的深度。为了限制find不进入当前目录的子目录,深度可以设置为 1。当我们需要下降到两个级别时,深度设置为 2,依此类推。

为了指定最大深度,我们使用-maxdepth级别参数。同样,我们也可以指定下降开始的最小级别。如果我们想要从第二级开始搜索,可以使用-mindepth级别参数设置最小深度。通过使用以下命令,将find命令限制为最大深度为 1:

$ find . -maxdepth 1 -type f -print

此命令仅列出当前目录中的所有普通文件。如果有子目录,则不会打印或遍历它们。同样,-maxdepth 2最多遍历两个下降级别的子目录。

-mindepth类似于-maxdepth,但它设置了find遍历的最小深度级别。它可以用于查找和打印位于基本路径最小深度级别的文件。例如,要打印出距离当前目录至少两个子目录的所有文件,请使用以下命令:

$ find . -mindepth 2 -type f -print
./dir1/dir2/file1
./dir3/dir4/f2

即使当前目录或dir1dir3中有文件,也不会被打印出来。

注意

-maxdepth-mindepth应该作为find的第三个参数进行指定。如果它们作为第四个或更多的参数进行指定,可能会影响find的效率,因为它必须进行不必要的检查(例如,如果-maxdepth被指定为第四个参数,-type被指定为第三个参数,find命令首先找出所有具有指定-type的文件,然后找出所有匹配的文件具有指定的深度。然而,如果深度被指定为第三个参数,-type被指定为第四个参数,find可以收集所有具有最多指定深度的文件,然后检查文件类型,这是搜索的最有效方式。

基于文件类型进行搜索

类 UNIX 操作系统将每个对象都视为文件。有不同类型的文件,如普通文件、目录、字符设备、块设备、符号链接、硬链接、套接字、FIFO 等。

文件搜索可以使用-type选项进行过滤。通过使用-type,我们可以指定find命令只匹配具有指定类型的文件。

只列出包括后代的目录如下:

$ find . -type d -print

很难分别列出目录和文件。但find可以帮助做到。只列出普通文件如下:

$ find . -type f -print

只列出符号链接如下:

$ find . -type l -print

您可以使用以下表中的type参数来正确匹配所需的文件类型:

文件类型 类型参数
普通文件 f
符号链接 l
目录 d
字符特殊设备 c
块设备 b
套接字 s
FIFO p

搜索文件时间

UNIX/Linux 文件系统的每个文件都有三种类型的时间戳。它们如下:

  • 访问时间(-atime):这是文件最后一次被某个用户访问的时间戳

  • 修改时间(-mtime):这是文件内容最后一次修改的时间戳

  • 更改时间(-ctime):这是文件的元数据(如权限或所有权)最后一次修改的时间戳

UNIX 中没有所谓的创建时间。

-atime-mtime-ctimefind中可用的时间参数选项。它们可以用"天数"的整数值来指定。这些整数值通常附加有-+符号。-符号表示小于,而+表示大于。例如:

  • 打印出在最近 7 天内访问过的所有文件如下:
$ find . -type f -atime -7 -print

  • 打印出所有访问时间正好是 7 天前的文件如下:
$ find . -type f -atime 7 -print

  • 打印出所有访问时间早于 7 天的文件如下:
$ find . -type f -atime +7 -print

同样,我们可以使用-mtime参数来搜索基于修改时间的文件,使用-ctime来搜索基于更改时间的文件。

-atime-mtime-ctime是基于时间的参数,使用以天为单位的时间度量。还有一些其他基于时间的参数,使用以分钟为单位的时间度量。这些如下:

  • -amin(访问时间)

  • -mmin(修改时间)

  • -cmin(更改时间)

例如:

为了打印所有访问时间早于七分钟的文件,请使用以下命令:

$ find . -type f -amin +7 -print

find的另一个很好的功能是-newer参数。通过使用-newer,我们可以指定一个参考文件来与时间戳进行比较。我们可以使用-newer参数找到所有比指定文件更新(修改时间更早)的文件。

例如,找出所有修改时间大于给定file.txt文件的修改时间的文件,如下所示:

$ find . -type f -newer file.txt -print

find命令的时间戳操作标志对于编写系统备份和维护脚本非常有用。

基于文件大小进行搜索

根据文件大小进行搜索,可以执行如下搜索:

$ find . -type f -size +2k
# Files having size greater than 2 kilobytes

$ find . -type f -size -2k
# Files having size less than 2 kilobytes

$ find . -type f -size 2k
# Files having size 2 kilobytes

我们可以使用不同的大小单位,而不是k,如下所示:

  • b – 512 字节块

  • c – 字节

  • w – 两个字节的字

  • k – 千字节

  • M – 兆字节

  • G – 千兆字节

基于文件匹配进行删除

-delete标志可用于删除由find匹配的文件。

从当前目录中删除所有.swp文件,方法如下:

$ find . -type f -name "*.swp" -delete

基于文件权限和所有权进行匹配

可以根据文件权限匹配文件。我们可以列出具有指定文件权限的文件,如下所示:

$ find . -type f -perm 644 -print
# Print files having permission 644

作为示例用例,我们可以考虑 Apache Web 服务器的情况。 Web 服务器中的 PHP 文件需要适当的权限才能执行。我们可以找出没有适当执行权限的 PHP 文件,如下所示:

$ find . –type f –name "*.php" ! -perm 644 –print

我们还可以根据文件的所有权搜索文件。使用-user USER选项可以找到特定用户拥有的文件。

USER参数可以是用户名或 UID。

例如,要打印所有由用户 slynux 拥有的文件的列表,可以使用以下命令:

$ find . -type f -user slynux -print

使用find执行命令或操作

find命令可以使用-exec选项与许多其他命令配合使用。-execfind附带的最强大的功能之一。

让我们看看如何使用-exec选项。

考虑前一节中的示例。我们使用-perm找出没有适当权限的文件。类似地,在需要将所有由特定用户(例如root)拥有的文件的所有权更改为另一个用户(例如 Web 服务器中的默认 Apache 用户www-data)的情况下,我们可以使用-user选项找到所有由 root 拥有的文件,并使用-exec执行所有权更改操作。

注意

执行所有权更改时,必须以 root 身份运行find命令。

让我们看看以下示例:

# find . -type f –user root –exec chown slynux {} \;

在此命令中,{}是与-exec选项一起使用的特殊字符串。对于每个文件匹配,{}将被替换为文件名,以替代-exec。例如,如果find命令找到两个文件test1.txttest2.txt,所有者为 slynux,find命令将执行:

chown slynux {}

这将解析为chown slynux test1.txtchown slynux test2.txt

另一个用法示例是将给定目录中的所有 C 程序文件连接起来,并将其写入单个文件all_c_files.txt。我们可以使用find递归匹配所有 C 文件,并使用-exec标志与cat命令,如下所示:

$ find . -type f -name "*.c" -exec cat {} \;>all_c_files.txt

-exec后面跟着任何命令。{}是一个匹配。对于每个匹配的文件名,{}将被替换为文件名。

为了将来自find的数据重定向到all_c_files.txt文件,我们使用了>运算符,而不是>>(追加),因为find命令的整个输出是单个数据流(stdin)。只有在需要将多个数据流追加到单个文件时才需要>>

例如,要将所有早于 10 天的.txt文件复制到目录OLD中,请使用以下命令:

$ find . -type f -mtime +10 -name "*.txt" -exec cp {} OLD  \;

同样,find命令可以与许多其他命令配合使用。

提示

-exec 与多个命令

我们不能在-exec参数中使用多个命令。它只接受单个命令,但我们可以使用一个技巧。在 shell 脚本中编写多个命令(例如,commands.sh)并将其与-exec一起使用,如下所示:

–exec ./commands.sh {} \;

-exec可以与printf结合产生非常有用的输出。例如:

$ find . -type f -name "*.txt" -exec printf "Text file: %s\n" {} \;

从 find 中跳过指定的目录

有时在进行目录搜索和执行某些操作时,需要跳过某些子目录以提高性能。例如,当程序员在开发源代码树上查找特定文件时,该源代码树位于版本控制系统(如 Git)下,源代码层次结构将始终包含每个子目录中的.git目录(.git存储每个目录的版本控制相关信息)。由于版本控制相关目录不会产生有用的输出,因此应将其从搜索中排除。排除文件和目录的技术称为修剪。可以按以下方式执行:

$ find devel/source_path  \( -name ".git" -prune \) -o \( -type f -print \)

# Instead of \( -type -print \), use required filter.

上述命令打印所有不来自.git目录的文件的名称(路径)。

在这里,\( -name ".git" -prune \)是排除部分,指定了应该排除.git目录,\( -type f -print \)指定了要执行的操作。要执行的操作放在第二个块-type f –print中(这里指定的操作是打印所有文件的名称和路径)。

玩转 xargs

我们使用管道将命令的stdout(标准输出)重定向到另一个命令的stdin(标准输入)。例如:

cat foo.txt | grep "test"

但是,一些命令接受命令行参数作为数据,而不是通过 stdin(标准输入)的数据流。在这种情况下,我们不能使用管道通过命令行参数提供数据。

我们应该选择替代方法。xargs是一个非常有用的命令,可以处理标准输入数据并转换为命令行参数。xargs可以操作 stdin 并将其转换为指定命令的命令行参数。此外,xargs还可以将任何单行或多行文本输入转换为其他格式,例如多行(指定列数)或单行,反之亦然。

所有的 Bash 黑客都喜欢单行命令。一行命令是通过使用管道操作符连接的命令序列,但不使用分号终止符(;)在使用的命令之间。编写一行命令可以使任务更有效和更简单地解决。它需要适当的理解和实践来制定解决文本处理问题的一行命令。xargs是构建一行命令的重要组件之一。

准备工作

xargs命令应该始终出现在管道操作符之后。xargs使用标准输入作为主要数据流源。它使用stdin并通过使用 stdin 数据源为执行命令提供命令行参数。例如:

command | xargs

如何做...

xargs命令可以通过重新格式化通过stdin接收的数据来为命令提供参数。

xargs可以充当替代品,在find命令的情况下可以执行与-exec参数类似的操作。让我们看看可以使用xargs命令执行的各种技巧。

  • 将多行输入转换为单行输出:

多行输入可以通过删除换行符并用" "(空格)字符替换来简单转换。'\n'被解释为换行符,这是行的分隔符。通过使用xargs,我们可以忽略所有带有空格的换行符,以便将多行转换为单行文本,如下所示:

$ cat example.txt # Example file
1 2 3 4 5 6 
7 8 9 10 
11 12

$ cat example.txt | xargs
1 2 3 4 5 6 7 8 9 10 11 12

  • 将单行转换为多行输出:

给定一行中的最大参数数= n,我们可以将任何stdin(标准输入)文本分割为每个 n 个参数的行。参数是由“ ”(空格)分隔的字符串片段。空格是默认分隔符。一行可以分成多行,如下所示:

$ cat example.txt | xargs -n 3
1 2 3 
4 5 6 
7 8 9 
10 11 12

它是如何工作的...

xargs命令适用于许多问题场景,具有丰富和简单的选项。让我们看看如何明智地使用这些选项来解决问题。

我们还可以使用自己的分隔符来分隔参数。为了指定输入的自定义分隔符,请使用-d选项,如下所示:

$ echo "splitXsplitXsplitXsplit" | xargs -d X
split split split split

在上面的代码中,stdin包含一个由多个'X'字符组成的字符串。我们可以使用'X'作为输入分隔符,通过与-d一起使用它。在这里,我们已经明确指定 X 作为输入分隔符,而在默认情况下,xargs将内部字段分隔符(空格)作为输入分隔符。

通过与上述命令一起使用-n,我们可以将输入拆分为每行两个单词的多行,如下所示:

$ echo "splitXsplitXsplitXsplit" | xargs -d X -n 2
split split
split split

还有更多...

我们已经学会了如何将stdin格式化为不同的输出,作为上述示例的参数。现在让我们学习如何将这些格式化的输出作为参数提供给命令。

通过读取标准输入将格式化的参数传递给命令

编写一个小的自定义 echo,以更好地理解使用 xargs 提供命令参数的示例用法。

#!/bin/bash
#Filename: cecho.sh

echo $*'#' 

当参数传递给cecho.sh时,它将打印以#字符终止的参数。例如:

$ ./cecho.sh arg1 arg2
arg1 arg2 #

让我们看一个问题:

  • 我有一个文件中的参数列表(每行一个参数),要提供给一个命令(比如cecho.sh)。我需要用两种方法提供参数。在第一种方法中,我需要为命令提供一个参数,如下所示:
./cecho.sh arg1
./cecho.sh arg2
./cecho.sh arg3

或者,我需要为每次执行命令提供两个或三个参数。对于每两个参数,它将类似于以下内容:

./cecho.sh arg1 arg2
./cecho.sh arg3
  • 在第二种方法中,我需要一次性提供所有参数给命令,如下所示:
./cecho.sh arg1 arg2 arg3

运行上述命令,并在阅读以下部分之前记下输出。

上述问题可以使用xargs解决。我们在一个名为args.txt的文件中有参数列表。内容如下:

$ cat args.txt
arg1
arg2
arg3

对于第一个问题,我们可以多次执行命令,每次执行一个参数,如下所示:

$ cat args.txt | xargs -n 1 ./cecho.sh
arg1 #
arg2 #
arg3 #

要执行每次执行 X 个参数的命令,请使用:

INPUT | xargs –n X

例如:

$ cat args.txt | xargs -n 2 ./cecho.sh 
arg1 arg2 #
arg3 #

对于第二个问题,我们可以一次执行命令,使用所有参数,如下所示:

$ cat args.txt | xargs ./ccat.sh
arg1 arg2 arg3 #

在上面的例子中,我们直接向特定命令(例如cecho.sh)提供了命令行参数。我们也可以从args.txt文件中提供参数。然而,在实时环境中,我们可能还需要在命令(例如cecho.sh)中添加一些常量参数,以及从args.txt中获取的参数。考虑以下格式的示例:

./cecho.sh –p arg1 –l

在上述命令执行中,arg1是唯一的可变文本。其他所有内容都应保持不变。我们应该从一个文件(args.txt)中读取参数并提供它:

./cecho.sh –p arg1 –l
./cecho.sh –p arg2 –l
./cecho.sh –p arg3 –l

为了提供如下所示的命令执行序列,xargs有一个-I选项。通过使用-I,我们可以指定一个替换字符串,当xargs扩展时将被替换。当-Ixargs一起使用时,它将执行每个参数的一个命令执行。

让我们这样做:

$ cat args.txt | xargs -I {} ./cecho.sh -p {} -l
-p arg1 -l #
-p arg2 -l #
-p arg3 -l #

-I {}指定替换字符串。对于为命令提供的每个参数,{}字符串将被通过stdin读取的参数替换。当与-I一起使用时,命令会像在循环中一样执行。当有三个参数时,命令将执行三次,以及命令{}。每次{}都会逐个替换参数。

使用 xargs 与 find

xargsfind是最好的朋友。它们可以结合起来轻松执行任务。通常,人们以错误的方式将它们结合在一起。例如:

$ find . -type f -name "*.txt"  -print | xargs rm -f 

这是危险的。有时可能会导致删除不必要的文件。在这里,我们无法预测定界字符(无论是'\n'还是' ')对于find命令的输出。许多文件名可能包含空格字符(' '),因此xargs可能会误解它作为定界符(例如,“hell text.txt”被xargs误解为“hell”和“text.txt”)。

因此,我们必须使用-print0以及find来生成一个带有分隔字符 null('\0')的输出,每当我们使用find的输出作为xargs的输入时。

让我们使用find来匹配和列出所有.txt文件,并使用xargs删除它们:

$ find . -type f -name "*.txt" -print0 | xargs -0 rm -f

这将删除所有.txt文件。xargs -0解释定界字符为\0

在源代码目录中计算 C 代码的行数,涵盖了许多 C 文件。

这是大多数程序员所做的任务,即计算所有 C 程序文件的 LOC(代码行数)。此任务的代码如下:

$ find source_code_dir_path -type f -name "*.c" -print0 | xargs -0 wc -l

使用 while 和子 shell 技巧处理 stdin

xargs受限于以有限的方式提供参数以提供参数。此外,xargs 无法向多组命令提供参数。为了执行从标准输入收集的参数的命令,我们有一种非常灵活的方法。我称之为子 shell hack。可以使用带有while循环的子 shell 来读取参数并以以下更复杂的方式执行命令:

$ cat files.txt  | ( while read arg; do cat $arg; done )
# Equivalent to cat files.txt | xargs -I {} cat {}

在这里,通过使用while循环替换cat $arg,我们可以执行许多具有相同参数的命令操作。我们还可以将输出传递给其他命令,而不使用管道。子 shell( )技巧可以在各种问题环境中使用。当包含在子 shell 操作符中时,它作为一个具有多个命令的单个单元。

$ cmd0 | ( cmd1;cmd2;cmd3) | cmd4

如果cmd1cd /,在子 shell 中,工作目录的路径会更改。但是,此更改仅存在于子 shell 中。cmd4将看不到目录更改。

使用 tr 进行转换

tr是 UNIX 命令战士工具包中的一个小而美丽的命令。它是经常用于制作美丽的单行命令的重要命令之一。

tr可用于执行标准输入中的字符替换、字符删除和重复字符的挤压。它通常被称为 translate,因为它可以将一组字符转换为另一组字符。

准备好

tr只通过stdin(标准输入)接受输入。它不能通过命令行参数接受输入。它具有以下调用格式:

tr [options] set1 set2

stdin输入的字符从set1映射到set2,并将输出写入stdout(标准输出)。set1set2是字符类或一组字符。如果集的长度不相等,则通过重复最后一个字符来扩展set2的长度到set1的长度,否则,如果set2的长度大于set1的长度,则从set2中忽略超出set1长度的所有字符。

如何做...

为了执行输入中的字符从大写转换为小写的转换,请使用以下命令:

$ echo "HELLO WHO IS THIS" | tr 'A-Z' 'a-z'

'A-Z''a-z'是集合。我们可以通过附加字符或字符类来指定自定义集合。

'ABD-}''aA.,''a-ce-x''a-c0-9'等都是有效的集合。我们可以轻松定义集合。我们可以使用'startchar-endchar'格式,而不是编写连续的字符序列。它也可以与任何其他字符或字符类结合使用。如果startchar-endchar不是有效的连续字符序列,则它们被视为三个字符的集合(例如,startchar-endchar)。您还可以使用特殊字符,如'\t''\n'或任何 ASCII 字符。

它是如何工作的...

通过使用tr与集合的概念,我们可以轻松地将字符从一个集合映射到另一个集合。让我们通过一个示例来了解如何使用tr来加密和解密数字字符:

$ echo 12345 | tr '0-9' '9876543210'
87654 #Encrypted

$ echo 87654 | tr '9876543210' '0-9'
12345 #Decrypted

让我们尝试另一个有趣的例子。

ROT13 是一个众所周知的加密算法。在 ROT13 方案中,相同的函数用于加密和解密文本。ROT13 方案对字符进行了 13 个字符的字母旋转。让我们使用tr执行 ROT13,如下所示:

$ echo "tr came, tr saw, tr conquered." | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'

输出将是:

ge pnzr, ge fnj, ge pbadhrerq.

通过再次将加密文本发送到相同的 ROT13 函数,我们得到:

$ echo ge pnzr, ge fnj, ge pbadhrerq. | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'

输出将是:

tr came, tr saw, tr conquered.

tr可以将制表符转换为空格,如下所示:

$ cat text | tr '\t' ' '

还有更多...

使用 tr 删除字符

tr有一个-d选项,可以通过使用指定的要删除的字符集在stdin上删除一组字符。

$ cat file.txt | tr -d  '[set1]'
#Only set1 is used, not set2

例如:

$ echo "Hello 123 world 456" | tr -d '0-9'
Hello  world
# Removes the numbers from stdin and print

补集字符集

我们可以使用-c标志通过set1来对set1进行补集。-c [set]等同于指定一个包含[set]的补集字符的集合。

tr -c [set1] [set2]

set1的补集意味着它是除set1中的字符之外的所有字符的集合。

最佳的用法示例是从输入文本中删除除补集中指定的字符之外的所有字符。例如:

$ echo hello 1 char 2 next 4 | tr -d -c '0-9 \n'
 1  2  4

在这里,补集是包含所有数字、空格字符和换行符的集合。由于-dtr一起使用,所有其他字符都被删除。

使用 tr 挤压字符

tr命令在许多文本处理上下文中非常有用。在许多情况下,需要将连续重复的字符挤压成单个字符。挤压空格是一个经常发生的任务。

tr提供了-s选项,用于从输入中挤出重复的字符。可以按以下方式执行:

$ echo "GNU is       not     UNIX. Recursive   right ?" | tr -s ' '
GNU is not UNIX. Recursive right ?
# tr -s '[set]'

让我们以一种巧妙的方式使用tr从文件中添加给定的数字列表,如下所示:

$ cat sum.txt
1
2
3
4
5

$ cat sum.txt | echo $[ $(tr '\n' '+' ) 0 ]
15

这种黑客行为是如何工作的?

在这里,tr命令用于用'+'字符替换'\n',因此我们形成字符串"1+2+3+..5+",但在字符串末尾我们有一个额外的+运算符。为了抵消+运算符的影响,附加了0

$[ operation ] 执行一个数字操作。因此,它形成如下字符串:

echo $[ 1+2+3+4+5+0 ]

如果我们使用循环从文件中读取数字执行加法,将需要几行代码。这里一行代码就能完成任务。通过实践可以掌握编写一行代码的技巧。

字符类

tr可以使用不同的字符类作为集合。不同的类如下:

  • alnum:字母数字字符

  • alpha:字母字符

  • cntrl:控制(不可打印)字符

  • digit:数字字符

  • graph:图形字符

  • lower:小写字母字符

  • print:可打印字符

  • punct:标点字符

  • space:空白字符

  • upper:大写字符

  • xdigit:十六进制字符

我们可以选择所需的类,并将它们与以下内容一起使用:

tr [:class:] [:class:]

例如:

tr '[:lower:]' '[:upper:]'

校验和和验证

校验和程序用于从文件生成校验和密钥字符串,并通过使用该校验和字符串稍后验证文件的完整性。文件可能分布在网络或任何存储介质上到不同的目的地。由于许多原因,文件可能因数据传输过程中丢失了一些位而损坏。这些错误经常发生在从互联网下载文件、通过网络传输、CD ROM 损坏等过程中。

因此,我们需要知道接收的文件是否正确,通过应用某种测试来确定。用于进行文件完整性测试的特殊密钥字符串称为校验和

我们计算原始文件和接收文件的校验和。通过比较两个校验和,我们可以验证接收的文件是否正确。如果源位置的原始文件和目的地计算出的校验和相等,这意味着我们已经收到了正确的文件,没有在数据传输过程中造成任何错误的数据丢失,否则,用户必须重复数据传输并再次尝试校验和比较。

在编写备份脚本或由网络传输文件的维护脚本时,校验和至关重要。通过使用校验和验证,可以识别在网络数据传输过程中损坏的文件,并可以从源重新发送这些文件到目的地。因此,始终可以确保接收到的数据的完整性。

准备工作

最著名和广泛使用的校验和技术是 md5sumsha1sum。它们通过将相应的算法应用于文件内容生成校验和字符串。让我们看看如何生成文件的校验和并验证文件的完整性。

如何做…

为了计算 md5sum,使用以下命令:

$ md5sum filename
68b329da9893e34099c7d8ad5cb9c940 filename

如上所示,md5sum 是一个 32 个字符的十六进制字符串。

我们将校验和输出重定向到文件,并使用该 MD5 文件进行验证,如下所示:

$ md5sum filename > file_sum.md5

工作原理…

md5sum 校验和计算的语法如下:

$ md5sum file1 file2 file3 ..

当使用多个文件时,输出将包含每个文件的校验和,每行一个校验和字符串,如下所示:

[checksum1]   file1
[checksum1]   file2
[checksum1]   file3

可以使用生成的文件验证文件的完整性,如下所示:

$ md5sum -c file_sum.md5
# It will output message whether checksum matches or not

或者,如果需要使用所有可用的 .md5 信息检查所有文件,可以使用:

$ md5sum *.md5

SHA1 是另一种常用的校验和算法,类似于 md5sum。它从给定的输入文件生成一个 40 个字符的十六进制代码。用于计算 SHA1 字符串的命令是 sha1sum。它的用法与 md5sum 非常相似。在本文中先前提到的所有命令中用 sha1sum 替换 md5sum。将输出文件名更改为 file_sum.sha1,而不是 file_sum.md5

校验和验证非常有用,可以验证我们从互联网下载的文件的完整性。我们从互联网下载的 ISO 镜像通常更容易出现错误位。因此,为了检查我们是否正确接收了文件,广泛使用校验和。对于相同的文件数据,校验和程序将始终生成相同的校验和字符串。

还有更多…

当与多个文件一起使用时,校验和也非常有用。让我们看看如何对多个文件应用校验和并验证正确性。

目录的校验和

文件的校验和是针对文件计算的。计算目录的校验和意味着我们需要递归计算目录中所有文件的校验和。

可以通过 md5deepsha1deep 命令实现。安装 md5deep 软件包以使这些命令可用。以下是此命令的示例:

$ md5deep -rl directory_path > directory.md5
# -r  for enable recursive.
# -l for using relative path. By default it writes absolute file path in output

或者,将其与 find 结合使用以递归计算校验和:

$ find directory_path -type f -print0 | xargs -0 md5sum >> directory.md5

要验证,请使用以下命令:

$ md5sum -c directory.md5

排序、唯一和重复项

排序是我们在文本文件中经常遇到的常见任务。因此,在文本处理任务中,sort 非常有用。sort 命令帮助我们对文本文件和 stdin 执行排序操作。它通常也可以与许多其他命令配合使用以生成所需的输出。uniq 是另一个经常与 sort 命令一起使用的命令。它有助于从文本或 stdin 中提取唯一行。sortuniq 可以组合以查找重复项。本文介绍了 sortuniq 命令的大多数用例。

准备工作

sort 命令接受文件名作为输入,也可以从 stdin(标准输入)中读取,并通过写入 stdout 输出结果。uniq 命令遵循相同的操作顺序。

如何做…

我们可以轻松地对给定的一组文件(例如 file1.txtfile2.txt)进行排序,如下所示:

$ sort file1.txt file2.txt .. > sorted.txt

或:

$ sort file1.txt file2.txt .. -o sorted.txt

为了从已排序的文件中查找唯一行,请使用:

$ cat sorted_file.txt | uniq> uniq_lines.txt

工作原理…

有许多情况可以使用 sortuniq 命令。让我们看看各种选项和用法技巧。

要进行数字排序,请使用:

$ sort -n file.txt

要按相反顺序排序,请使用:

$ sort -r file.txt

按月份排序(按照 1 月、2 月、3 月的顺序)使用:

$ sort -M months.txt

可以按以下方式测试文件是否已排序:

#!/bin/bash
#Desc: Sort
sort -C file ;
if [ $? -eq 0 ]; then
   echo Sorted;
else
   echo Unsorted;
fi

# If we are checking numerical sort, it should be sort -nC

要合并两个已排序的文件而不再次排序,请使用:

$ sort -m sorted1 sorted2

还有更多...

根据键或列进行排序

如果需要对文本进行排序,则使用列排序如下:

$ cat data.txt
1  mac      2000
2  winxp    4000
3  bsd      1000
4  linux    1000

我们可以以多种方式对其进行排序;当前是按序列号(第一列)进行数字排序。我们还可以按第二列和第三列进行排序。

-k指定要执行排序的键。键是要进行排序的列号。-r指定 sort 命令以相反顺序排序。例如:

# Sort reverse by column1
$ sort -nrk 1  data.txt
4	linux		1000 
3	bsd		1000 
2	winxp		4000 
1	mac		2000 
# -nr means numeric and reverse

# Sort by column 2
$ sort -k 2  data.txt
3	bsd		1000 
4	linux		1000 
1	mac		2000 
2	winxp		4000

注意

始终要小心-n选项进行数字排序。sort 命令以不同的方式处理字母排序和数字排序。因此,为了指定数字排序,应提供-n选项。

通常,默认情况下,键是文本文件中的列。列由空格字符分隔。但在某些情况下,我们需要将键指定为给定字符编号范围内的一组字符(例如,key1= character4-character8)。在需要显式指定键为字符范围的情况下,我们可以指定键为字符位置的范围,如下所示:

$ cat data.txt
1010hellothis
2189ababbba
7464dfddfdfd
$ sort -nk 2,3 data.txt

要使用突出显示的字符作为数字键。要提取它们,请使用它们的起始位置和结束位置作为键格式。

要使用第一个字符作为键,请使用:

$ sort -nk 1,1 data.txt

通过使用以下命令,使 sort 的输出与\0终止符兼容,从而与xargs兼容:

$ sort -z data.txt | xargs -0
#Zero terminator is used to make safe use with xargs

有时文本可能包含不必要的多余字符,如空格。要按字典顺序忽略它们进行排序,忽略标点和折叠,使用:

$ sort -bd unsorted.txt

选项-b用于忽略文件中的前导空格,选项-d用于指定按字典顺序排序。

uniq

uniq是一个用于从给定输入(stdin或从文件名作为命令参数)中找出唯一行的命令,通过消除重复项。它也可以用于从输入中找出重复行。uniq只能应用于排序后的数据输入。因此,uniq始终应与使用管道或使用排序文件作为输入的sort命令一起使用。

您可以从给定的输入数据中生成唯一行(唯一行表示打印输入中的所有行,但重复行仅打印一次)如下:

$ cat sorted.txt
bash 
foss 
hack 
hack

$ uniq sorted.txt
bash 
foss 
hack 

或者:

$ sort unsorted.txt | uniq

或者:

$ sort -u unsorted.txt

仅显示唯一行(输入文件中未重复或重复的行)如下:

$ uniq -u sorted.txt
bash
foss

或者:

$ sort unsorted.txt | uniq -u

要计算文件中每行出现的次数,请使用以下命令:

$ sort unsorted.txt | uniq -c
 1 bash
 1 foss
 2 hack

查找文件中的重复行如下:

$ sort unsorted.txt  | uniq -d
hack

要指定键,我们可以使用-s-w参数的组合。

  • -s指定要跳过的前N个字符的数字

  • -w`指定要比较的最大字符数

此比较键用作uniq操作的索引,如下所示:

$ cat data.txt
u:01:gnu 
d:04:linux 
u:01:bash 
u:01:hack

我们需要使用突出显示的字符作为唯一性键。这用于忽略前 2 个字符(-s 2),并使用-w选项指定比较字符的最大数量(-w 2):

$ sort data.txt | uniq -s 2 -w 2
d:04:linux 
u:01:bash 

当我们使用一个命令的输出作为 xargs 命令的输入时,最好为输出的每行使用零字节终止符,这充当xargs的源。在使用uniq命令的输出作为xargs的源时,我们应该使用零终止输出。如果不使用零字节终止符,则空格字符默认为xargs命令中的分隔符以拆分参数。例如,来自stdin的文本“this is a line”将被xargs视为四个单独的参数。但实际上,它是一行。当使用零字节终止符时,\0被用作分隔符字符,因此,包括空格的单行被解释为单个参数。

可以从uniq命令生成零字节终止的输出,如下所示:

$ uniq -z file.txt

以下命令从files.txt中读取的文件名中删除所有文件:

$ uniq –z file.txt | xargs -0 rm

如果文件中存在多行文件名条目,则uniq命令仅将文件名写入stdout一次。

使用 uniq 生成字符串模式

这里有一个有趣的问题:我们有一个包含重复字符的字符串。我们如何找出每个字符在字符串中出现的次数,并输出以下格式的字符串?

输入:ahebhaaa

输出:4a1b1e2h

每个字符都重复一次,并且它们中的每一个都以它们在字符串中出现的次数作为前缀。我们可以使用uniqsort来解决这个问题,如下所示:

INPUT= "ahebhaaa"
OUTPUT=` echo $INPUT | sed 's/[^\n]/&\n/g' | sed '/^$/d' | sort | uniq -c | tr -d ' \n'`
echo $OUTPUT 

在上面的代码中,我们可以将每个管道命令拆分如下:

echo $INPUT  # Print the input to stdout
sed 's/./&\n/g'

为每个字符附加一个换行符,以便每行只出现一个字符。这样做是为了使字符可以通过sort命令进行排序。sort命令只能接受由换行符分隔的项目。

  • sed '/^$/d':这里最后一个字符被替换为字符+\n。因此会形成一个额外的换行符,并且会在末尾形成一个空行。此命令删除末尾的空行。

  • sort:由于每个字符都出现在每行中,因此可以对其进行排序,以便作为 uniq 的输入。

  • uniq -c:此命令打印每行重复的次数。

  • tr -d ' \n':这将从输入中删除空格字符和换行字符,以便以给定格式生成输出。

临时文件命名和随机数

在编写 shell 脚本时,我们经常需要存储临时数据。存储临时数据的最合适位置是/tmp(系统在重新启动时将清除该位置)。我们可以使用两种方法为临时数据生成标准文件名。

如何做…

tempfile在非 Debian Linux 发行版中看不到。tempfile命令随 Debian 系发行版一起提供,例如 Ubuntu、Debian 等。

以下代码将临时文件名分配给变量temp_file

temp_file=$(tempfile)

使用echo $temp_file在终端中打印临时文件名。

输出将类似于/tmp/fileaZWm8Y

有时我们可能会使用带有随机数字附加的文件名作为临时文件名。可以按以下方式完成:

temp_file="/tmp/file-$RANDOM"

$RANDOM环境变量始终返回一个随机数。

它是如何工作的…

我们可以使用自己的临时文件名,而不是使用tempfile命令。大多数经验丰富的 UNIX 程序员使用以下约定:

temp_file="/tmp/var.$$"

附加了.$$后缀。$$在执行时会扩展为当前脚本的进程 ID。

拆分文件和数据

在某些情况下,将文件拆分为许多较小的部分变得至关重要。在早期,当存储器受限于软盘等设备时,将文件拆分为较小的文件大小以在许多磁盘中传输数据至关重要。然而,如今我们拆分文件是为了其他目的,例如可读性,生成日志等。

如何做…

生成一个 100kb 的测试文件(data.file)如下:

$ dd if=/dev/zero bs=100k count=1 of=data.file

上述命令创建了一个大小为 100kb 的用零填充的文件。

您可以通过指定拆分大小将文件拆分为较小的文件,如下所示:

$ split -b 10k data.file
$ ls
data.file  xaa  xab  xac  xad  xae  xaf  xag  xah  xai  xaj

它将data.file拆分为许多文件,每个文件大小为 10k。这些块将以xabxacxad等方式命名。这意味着它们将具有字母后缀。要使用数字后缀,可以使用额外的-d参数。还可以使用-a length指定后缀长度,如下所示:

$ split -b 10k data.file -d  -a 4
$ ls
data.file x0009  x0019  x0029  x0039  x0049  x0059  x0069  x0079

我们可以使用M代替k(千字节)后缀,使用M代替MB,使用G代替GB,使用c代替字节,使用w代替字,等等。

还有更多…

split命令有更多选项。让我们来看看它们。

指定拆分文件的文件名前缀

上述拆分文件具有文件名前缀"x"。我们还可以通过提供前缀文件名来使用自己的文件名前缀。拆分命令的最后一个命令参数是PREFIX。它的格式是:

$ split [COMMAND_ARGS] PREFIX

让我们使用拆分文件的前缀文件名运行上一个命令:

$ split -b 10k data.file -d  -a 4 split_file
$ ls
data.file       split_file0002  split_file0005  split_file0008  strtok.c
split_file0000  split_file0003  split_file0006  split_file0009
split_file0001  split_file0004  split_file0007

为了根据每个拆分的行数而不是块大小拆分文件,使用-l no_of_lines如下:

$ split -l 10 data.file 
# Splits into files of 10 lines each.

还有另一个有趣的实用程序叫做csplit。它可以用于根据指定条件和字符串匹配选项拆分日志文件。让我们看看如何使用它。

csplitsplit实用程序的一个变体。split实用程序只能根据块大小或行数拆分文件。csplit根据上下文拆分文件。它可以用于根据某个特定单词或文本内容的存在来拆分文件。

看一个示例日志:

$ cat server.log
SERVER-1 
[connection] 192.168.0.1 success 
[connection] 192.168.0.2 failed 
[disconnect] 192.168.0.3 pending 
[connection] 192.168.0.4 success 
SERVER-2 
[connection] 192.168.0.1 failed 
[connection] 192.168.0.2 failed 
[disconnect] 192.168.0.3 success 
[connection] 192.168.0.4 failed 
SERVER-3 
[connection] 192.168.0.1 pending 
[connection] 192.168.0.2 pending 
[disconnect] 192.168.0.3 pending 
[connection] 192.168.0.4 failed

我们可能需要从每个文件中的每个SERVER的内容中拆分文件为server1.logserver2.logserver3.log。可以按以下方式完成:

 $ csplit server.log /SERVER/ -n 2 -s {*}  -f server -b "%02d.log"  ; rm server00.log 

$ ls
server01.log  server02.log  server03.log  server.log

命令的详细信息如下:

  • /SERVER/是用于匹配拆分的行。

  • /[REGEX]/ 是格式。它从当前行(第一行)复制到包含"SERVER"的匹配行,但不包括匹配行。

  • {*}用于指定根据匹配重复拆分直到文件的末尾。通过使用{integer},我们可以指定要继续的次数。

  • -s是一个标志,使命令保持安静,而不是打印其他消息。

  • -n用于指定要用作后缀的数字位数。01、02、03 等。

  • -f用于指定拆分文件的文件名前缀(在上一个示例中的前缀是"server")。

  • -b用于指定后缀格式。"%02d.log"类似于 C 中的printf参数格式。这里的文件名=前缀+后缀="server" + "%02d.log"

我们删除server00.log,因为第一个拆分文件是一个空文件(匹配单词是文件的第一行)。

根据扩展名切片文件名

几个自定义的 shell 脚本根据文件名执行操作。我们可能需要执行诸如保留扩展名重命名文件,将文件从一种格式转换为另一种格式(通过保留名称更改扩展名),提取文件名的一部分等操作。Shell 具有内置功能,可以根据不同条件对文件名进行切片。让我们看看如何做到这一点。

如何做…

name.extension中的名称可以通过使用%运算符轻松提取。您可以按如下方式从"sample.jpg"中提取名称:

file_jpg="sample.jpg"
name=${file_jpg%.*}
echo File name is: $name

输出是:

File name is: sample

下一个任务是从文件名中提取文件的扩展名。可以使用#运算符提取扩展名。

从变量file_jpg中的文件名中提取.jpg如下:

extension=${file_jpg#*.}
echo Extension is: jpg

输出是:

Extension is: jpg

它是如何工作的..

在第一个任务中,为了从格式为name.extension的文件名中提取名称,我们使用了%运算符。

${VAR%.*} 可以解释为:

  • $VARIABLE中删除通配符模式的字符串匹配,该模式出现在%的右侧(在上一个示例中为.*)。从右到左的方向进行评估应该使通配符匹配。

  • VAR=sample.jpg。因此,从右到左的通配符匹配是.jpg。因此它从$VAR字符串中删除,并且输出将是"sample"。

%是一种非贪婪操作。它从右到左找到通配符的最小匹配。还有一个运算符%%,类似于%。但它是贪婪的。这意味着它匹配通配符的最大字符串。

例如,我们有:

VAR=hack.fun.book.txt

通过使用%运算符,我们有:

$ echo ${VAR%.*}

输出将是:hack.fun.book

运算符%从右到左执行.*的非贪婪匹配(.txt)。

通过使用%%运算符,我们有:

$ echo ${VAR%%.*}

输出将是:hack

%%运算符从右到左进行贪婪匹配.*(.fun.book.txt)。

在第二个任务中,我们使用了#运算符从文件名中提取扩展名。它类似于%。但它是从左到右进行评估的。

${VAR#*.} 可以解释为:

#右侧出现的通配符模式匹配的字符串匹配从左到右的方向应该使通配符匹配。

类似地,与%%一样,我们还有另一个贪婪运算符#,即##

它通过从左到右评估并从指定变量中删除匹配字符串来进行贪婪匹配。

让我们使用这个例子:

VAR=hack.fun.book.txt

通过使用#运算符,我们有:

$ echo ${VAR#*.} 

输出将是:fun.book.txt

运算符#hack.进行了从左到右的非贪婪匹配。

通过使用##运算符,我们有:

$ echo ${VAR##*.}

输出将是:txt

运算符##从左到右(txt)进行贪婪匹配。

注意

运算符比#运算符更受欢迎,用于从文件名中提取扩展名,因为文件名可能包含多个'.'字符。由于##进行贪婪匹配,它总是只提取扩展名。

以下是一个实际示例,可用于提取域名的不同部分,给定 URL="www.google.com":

$ echo ${URL%.*} # Remove rightmost .*
www.google

$ echo ${URL%%.*} # Remove right to leftmost  .* (Greedy operator)
www

$ echo ${URL#*.} # Remove leftmost  part before *.
google.com

$ echo ${URL##*.} # Remove left to rightmost  part before *. (Greedy operator)
com

批量重命名和移动文件

重命名多个文件是我们经常遇到的任务之一。一个简单的例子是,当您从数码相机下载照片到计算机时,您可能会删除不必要的文件,导致图像文件的编号不连续。有时您可能需要使用自定义前缀和连续编号对它们进行重命名。我们有时使用第三方工具执行重命名操作。我们可以使用 Bash 命令在几秒钟内执行重命名操作。

将所有文件名中包含特定子字符串(例如,文件名具有相同前缀)或具有特定文件类型的文件移动到给定目录是我们经常执行的另一个用例。让我们看看如何编写脚本来执行这些类型的操作。

准备工作

rename命令有助于使用 Perl 正则表达式更改文件名。通过组合findrenamemv命令,我们可以执行很多操作。

如何做…

在当前目录中重命名图像文件为特定格式的最简单方法是使用以下脚本:

#!/bin/bash
#Filename: rename.sh
#Description: Rename jpg and png files

count=1;
for img in *.jpg *.png
do
new=image-$count.${img##*.}

mv "$img" "$new" 2> /dev/null

if [ $? -eq  0 ];
then

echo "Renaming $img to $new"
let count++

fi

done 

输出如下:

$ ./rename.sh
Renaming hack.jpg to image-1.jpg
Renaming new.jpg to image-2.jpg
Renaming next.jpg to image-3.jpg

该脚本将当前目录中的所有.jpg.png文件重命名为格式为image-1.jpgimage-2.jpgimage-3.jpgimage-4.png等的新文件名。

它是如何工作的…

在上面的重命名脚本中,我们使用了for循环来遍历所有以.jpg扩展名结尾的文件名。通配符*.jpg*.png用于匹配所有 JPEG 和 PNG 文件。我们可以对扩展名匹配进行一点改进。.jpg通配符只匹配小写的扩展名。然而,我们可以通过用.[jJ][pP][gG]替换.jpg来使其不区分大小写。因此它可以匹配文件file.jpg以及file.JPGfile.Jpg。在 Bash 中,当字符被包含在[]中时,它意味着从[]中包含的字符集中匹配一个字符。

上述代码中的for img in *.jpg *.png将被扩展为:

for img in hack.jpg new.jpg next.jpg

我们初始化了一个变量count=1,以便跟踪图像编号。下一步是使用mv命令重命名文件。文件的新名称应该经过制定。脚本中的${img##*.}解析了当前循环中的文件名的扩展名(有关${img##*.}的解释,请参见基于扩展名切片文件名配方)。

let count++用于递增每次循环执行的文件编号。

您可以看到使用2>运算符将错误重定向(stderr)到/dev/null是为了阻止错误消息打印到终端。

由于我们使用*.png*.jpg,如果通配符匹配的至少一个图像不存在,shell 将解释通配符本身作为一个字符串。在上面的输出中,您可以看到.png文件不存在。因此它将把*.png作为另一个文件名并执行mv *.png image-X.png,这将导致错误。使用[ $? –eq 0 ]if语句来检查退出状态($?)。如果最后执行的命令成功,$?的值将为 0,否则返回非零。当mv命令失败时,它返回非零,因此用户将不会看到消息"重命名文件",计数也不会增加。

有各种其他方法可以执行重命名操作。让我们来看看其中的一些。

*.JPG重命名为*.jpg

$ rename *.JPG *.jpg

将文件名中的空格替换为"_"字符如下:

$ rename 's/ /_/g' *

# 's/ /_/g'是文件名中的替换部分,*是目标文件的通配符。它可以是*.txt或任何其他通配符模式。

您可以按照以下方式将任何文件名从大写转换为小写,反之亦然:

$ rename 'y/A-Z/a-z/' *
$ rename 'y/a-z/A-Z/' *

为了递归地将所有.mp3文件移动到给定目录中,请使用:

$ find path -type f -name "*.mp3" -exec mv {} target_dir \;

通过以下方式递归重命名所有文件,将空格替换为"_"字符:

$ find path -type f -exec rename 's/ /_/g' {} \;

拼写检查和字典操作

大多数 Linux 发行版都附带一个字典文件。然而,我发现很少有人知道字典文件,因此许多人未能利用它们。有一个名为aspell的命令行实用程序,它可以作为拼写检查器。让我们看看一些利用字典文件和拼写检查器的脚本。

如何做...

/usr/share/dict/目录包含一些字典文件。字典文件是包含字典单词列表的文本文件。我们可以使用这个列表来检查一个单词是否是字典单词。

$ ls /usr/share/dict/ 
american-english  british-english

为了检查给定的单词是否是字典单词,请使用以下脚本:

#!/bin/bash
#Filename: checkword.sh
word=$1
grep "^$1$" /usr/share/dict/british-english -q 
if [ $? -eq 0 ]; then
  echo $word is a dictionary word;
else
  echo $word is not a dictionary word;
fi

用法如下:

$ ./checkword.sh ful 
ful is not a dictionary word 

$ ./checkword.sh fool 
fool is a dictionary word

它是如何工作的...

grep中,^是单词起始标记字符,字符$是单词结束标记。

-q用于抑制任何输出并保持沉默。

或者,我们可以使用拼写检查aspell来检查一个单词是否在字典中,如下所示:

#!/bin/bash 
#Filename: aspellcheck.sh
word=$1 

output=`echo \"$word\" | aspell list` 

if [ -z $output ]; then 
        echo $word is a dictionary word; 
else 
        echo $word is not a dictionary word; 
fi 

aspell list命令在给定输入不是字典单词时返回输出文本,并且在输入为字典单词时不输出任何内容。-z检查确保$output是否为空字符串。

列出文件中以给定单词开头的所有单词如下:

$ look word filepath

或者,使用:

$ grep "^word" filepath

默认情况下,如果未给出文件名参数,则look命令使用默认字典(/usr/share/dict/words)并返回输出。

$ look word
# When used like this it takes default dictionary as file

例如:

$ look android
android
android's
androids

自动化交互式输入

自动化命令行实用程序的交互式输入对于编写自动化工具或测试工具非常有用。在处理需要交互式输入的命令时,会有许多情况。交互式输入是用户在命令要求输入时键入的输入。执行命令并提供交互式输入的示例如下:

$ command
Enter a number: 1
Enter name : hello
You have entered 1,hello

准备工作

自动化实用程序可以自动接受输入,就像上面提到的方式一样,对于提供输入给本地命令以及远程应用程序都是有用的。让我们看看如何自动化它们。

如何做...

考虑交互式输入的顺序。从前面的代码中,我们可以得出以下序列的步骤:

1[Return]hello[Return]

通过观察实际在键盘上键入的字符,将上述步骤1,Return,hello,Return转换为以下字符串。

"1\nhello\n"

当我们按下Return时,\n字符被发送。通过附加回车(\n)字符,我们得到传递给stdin(标准输入)的实际字符串。

因此,通过发送用户输入的等效字符串,我们可以自动化交互过程中的输入传递。

它是如何工作的...

让我们编写一个脚本,以交互方式读取输入,并将此脚本用于自动化示例:

#!/bin/bash
#Filename: interactive.sh
read -p "Enter number:" no ;
read -p "Enter name:" name
echo You have entered $no, $name;

让我们按以下方式自动发送输入到命令:

$ echo -e "1\nhello\n" | ./interactive.sh 
You have entered 1, hello 

因此,使用\n来制作输入是有效的。

我们使用了echo -e来生成输入序列。如果输入很大,我们可以使用输入文件和重定向运算符来提供输入。

$ echo -e "1\nhello\n"  > input.data
$ cat input.data
1
hello

您也可以手动制作输入文件,而不使用echo命令手动输入。例如:

$ ./interactive.sh < input.data

这将从文件中重定向交互式输入数据。

如果您是一名逆向工程师,您可能已经玩过缓冲区溢出漏洞。为了利用它们,我们需要重定向 shellcode,比如"\xeb\x1a\x5e\x31\xc0\x88\x46",它是用十六进制编写的。这些字符不能直接通过键盘输入,因为键盘上没有这些字符的键。因此我们应该使用:

echo -e "\xeb\x1a\x5e\x31\xc0\x88\x46"

这将把 shellcode 重定向到一个有漏洞的可执行文件。

我们已经描述了一种通过将预期输入文本重定向通过stdin(标准输入)来自动化交互式输入程序的方法。我们发送输入而不检查程序要求的输入。我们通过期望程序按特定(静态)顺序要求输入来发送输入。如果程序随机或以不同顺序要求输入,或者有时根本不要求某些输入,则上述方法将失败。它将向程序的不同输入提示发送错误的输入。为了处理动态输入供应并通过检查程序在运行时的输入要求来提供输入,我们有一个称为expect的实用工具。expect命令通过程序发送正确的输入到正确的输入提示。让我们看看如何使用expect

还有更多...

交互式输入的自动化也可以使用其他方法。Expect 脚本是另一种自动化方法。让我们来看看。

使用 expect 进行自动化

expect实用程序在大多数常见的 Linux 发行版中默认不包含。您必须使用软件包管理器手动安装 expect 软件包。

expect期望特定的输入提示,并通过检查输入提示中的消息发送数据。

#!/usr/bin/expect 
#Filename: automate_expect.sh
spawn ./interactive .sh 
expect "Enter number:" 
send "1\n" 
expect "Enter name:" 
send "hello\n" 
expect eof 

运行如下:

$ ./automate_expect.sh

在这个脚本中:

  • spawn参数指定要自动化的命令

  • expect参数提供了预期的消息

  • send是要发送的消息。

  • expect eof定义了命令交互的结束

第三章:文件输入,文件输出

在本章中,我们将涵盖:

  • 生成任意大小的文件

  • 文本文件上的交集和集差(A-B)

  • 查找和删除重复文件

  • 为长路径创建目录

  • 文件权限、所有权和粘性位

  • 使文件不可变

  • 批量生成空白文件

  • 查找符号链接及其目标

  • 列举文件类型统计

  • 回环文件和挂载

  • 创建 ISO 文件,混合 ISO

  • 查找文件之间的差异,打补丁

  • 头和尾-打印最后或前 10 行

  • 仅列出目录-替代方法

  • 使用 pushd 和 popd 进行快速命令行目录导航

  • 计算文件中的行数、单词数和字符数

  • 打印目录树

介绍

UNIX 将操作系统中的每个对象都视为文件。我们可以找到与执行的每个操作相关联的文件,并可以利用它们进行不同的系统或进程相关的操作。例如,我们使用的命令终端与设备文件相关联。我们可以通过向特定终端的相应设备文件写入来写入终端。文件可以采用不同的形式,如目录、常规文件、块设备、字符特殊设备、符号链接、套接字、命名管道等。文件名、大小、文件类型、修改时间、访问时间、更改时间、inode、关联的链接以及文件所在的文件系统都是文件可能具有的属性和属性。本章涉及处理与文件相关的任何操作或属性的配方。

生成任意大小的文件

出于各种原因,您可能需要生成一个填充有随机数据的文件。这可能是为了创建一个用于执行测试的测试文件,例如使用大文件作为输入的应用程序效率测试,或者测试将文件拆分成多个文件,或者创建回环文件系统(回环文件是可以包含文件系统本身的文件,这些文件可以类似于使用mount命令挂载物理设备)。通过编写特定的程序来创建这样的文件是很困难的。因此,我们使用通用实用程序。

如何做...

使用dd命令创建指定大小的大文件是最简单的方法。dd命令克隆给定的输入并将精确的副本写入输出。输入可以是stdin、设备文件、常规文件等。输出可以是stdout、设备文件、常规文件等。dd命令的示例如下:


$ dd if=/dev/zero of=junk.data bs=1M count=1
1+0 records in
1+0 records out
1048576 bytes (1.0 MB) copied, 0.00767266 s, 137 MB/s

上述命令将创建一个名为junk.data的文件,其大小正好为 1MB。让我们来看看参数:if代表- 输入文件,of代表- 输出文件,bs代表块的字节数,count代表要复制的bs块数。

在这里,我们只通过将bs设置为 1MB 并将计数设置为 1 来创建一个大小为 1MB 的文件。如果bs设置为2M,计数设置为 2,则总文件大小将为 4MB。

我们可以使用以下各种单位来指定 大小BS)。将以下任何字符附加到数字后,以指定以字节为单位的大小:

单位大小 代码
字节(1B) c
字(2B) w
块(512B) b
千字节(1024B) k
兆字节(1024 KB) M
Giga Byte(1024 MB) G

我们可以使用这个来生成任意大小的文件。我们可以使用前面表中提到的其他单位符号,而不是 MB。

/dev/zero是一个字符特殊设备,它无限返回零字节(\0)。

如果未指定输入参数(if),它将默认从stdin读取输入。同样,如果未指定输出参数(of),它将使用stdout作为默认输出接收器。

dd命令还可用于通过传输大量数据并检查命令输出(例如,1048576 字节(1.0 MB)已复制,0.00767266 秒,137 MB/s,如前面的示例所示)来测量内存操作的速度。

交集和集合差异(A-B)的文本文件

交集和集合差异操作在集合论数学课上通常被使用。然而,在文本上进行类似的操作在某些情况下也非常有帮助。

准备工作

comm命令是一个用于比较两个文件的实用程序。它有许多很好的选项,可以安排输出,以便我们可以执行交集、差异和集合差异操作。

  • 交集:交集操作将打印指定文件彼此之间共有的行。

  • 差异:差异操作将打印指定文件包含的行,而这些行在所有这些文件中都不相同。

  • 集合差异:集合差异操作将打印文件“A”中与指定的所有文件集合(例如“B”加“C”)中不匹配的行。

如何做...

请注意,comm接受排序后的文件作为输入。看一下以下示例:

$ cat A.txt
apple
orange
gold
silver
steel
iron

$ cat B.txt
orange
gold
cookies
carrot

$ sort A.txt -o A.txt ; sort B.txt -o B.txt
$ comm A.txt B.txt 
apple
 carrot
 cookies
 gold
iron
 orange
silver
steel

输出的第一列包含了在A.txt中的行,不包括两个文件中的共同行。第二列包含了在B.txt中的行,不包括共同行。第三列包含了来自A.txtB.txt的共同行。每一列都是用制表符(\t)分隔的。

有一些选项可用于根据我们的要求格式化输出。例如:

  • -1从输出中删除第一列

  • -2删除第二列

  • -3删除第三列

为了打印两个文件的交集,我们需要删除第一列和第二列,只打印第三列,如下所示:

$ comm A.txt B.txt -1 -2
gold
orange

打印在两个文件中不常见的行,如下所示:

$ comm A.txt B.txt  -3
apple
 carrot
 cookies
iron
silver
steel

comm命令中使用-3参数可以从输出中删除第三列。但是,它会将第一列和第二列写入输出。第一列包含了A.txt中不包括B.txt中的行。同样,第二列包含了B.txt中不包括A.txt中的行。由于输出是双列输出,因此并不是很有用。每个唯一行的列都有空白字段。因此,两列都不会在同一行上有内容。两列中的其中一列将会有内容。为了使其成为可用的输出文本格式,我们需要删除空白字段,并将两列合并为单列输出,如下所示:

apple
carrot
cookies
iron
silver
steel

为了产生这样的输出,我们需要删除行开头的\t字符。我们可以删除每行开头的\t字符,并将列统一为一列,如下所示:

$ comm A.txt B.txt  -3 | sed 's/^\t//'
apple
carrot
cookies
iron
silver
steel

sed命令被连接到comm输出。sed删除了行开头的\t字符。sed脚本中的s代表替换。/^\t/匹配行开头的\t^是行开始标记)。//(没有字符)是每个行开头的\t的替换字符串。因此,每个行开头的\t都被删除了。

可以按照以下段落中的说明执行两个文件的集合差异操作。

集合差异操作使您能够比较两个文件,并打印出所有在文件A.txtB.txt中的行,不包括在A.txtB.txt中共同的行。当A.txtB.txt作为comm命令的参数给出时,输出将包含第一列,其中包含了相对于B.txt的集合差异,第二列将包含相对于A.txt的集合差异。

通过删除不必要的列,我们可以产生A.txtB.txt的集合差异,如下所示:

  • A.txt 的集合差异
$ comm A.txt B.txt -2 -3

-2 -3删除第二列和第三列。

  • B.txt 的集合差异
$ comm A.txt B.txt -1 -3

-2 -3删除第二列和第三列。

查找和删除重复文件

重复文件是相同文件的副本。在某些情况下,我们可能需要删除重复文件并保留其中的一个副本。通过查看文件内容来识别重复文件是一项有趣的任务。可以使用一组 shell 实用程序来完成。本文介绍了查找重复文件并根据结果执行操作的方法。

准备工作

重复文件是具有不同名称但相同数据的文件。我们可以通过比较文件内容来识别重复文件。校验和是通过查看文件内容计算的。由于具有完全相同内容的文件将产生重复的校验和值,我们可以使用这一点来删除重复的行。

如何做...

生成一些测试文件如下:

$ echo "hello" > test ; cp test test_copy1 ; cp test test_copy2;
$ echo "next" > other;
# test_copy1 and test_copy2 are copy of test

用于删除重复文件的脚本的代码如下:

#!/bin/bash
#Filename: remove_duplicates.sh
#Description:  Find and remove duplicate files and keep one sample of each file.

ls -lS | awk 'BEGIN { 
getline;getline; 
name1=$8; size=$5 
 } 
{ name2=$8; 
if (size==$5) 
{ 

"md5sum "name1 | getline; csum1=$1;
"md5sum "name2 | getline; csum2=$1;
if ( csum1==csum2 ) 
{print name1; print name2 } 

}; 
size=$5; name1=name2; 
 }' | sort -u > duplicate_files 

cat duplicate_files | xargs -I {} md5sum {} | sort | uniq -w 32 | awk '{ print "^"$2"$" }' | sort -u >  duplicate_sample

echo Removing..
comm duplicate_files duplicate_sample  -2 -3 | tee /dev/stderr | xargs rm
echo Removed duplicates files successfully.

运行如下:

$ ./remove_duplicates.sh

它是如何工作的...

上述命令将在目录中查找相同文件的副本,并删除除文件的一个副本之外的所有副本。让我们看看代码如何工作。ls -lS将列出当前目录中按文件大小排序的文件的详细信息。awk将读取ls -lS的输出,并对输入文本的列和行进行比较,以找出重复文件。

前面代码的逻辑如下:

  • 我们按文件大小排序列出文件,以便大小相似的文件将被分组在一起。首先识别具有相同文件大小的文件,以找到相同的文件。接下来,我们计算文件的校验和。如果校验和匹配,则文件是重复的,重复文件组的一组将被删除。

  • awkBEGIN{}块在从文件中读取行之前首先执行。行的读取发生在{}块中,在读取和处理所有行结束后,执行END{}块中的语句。ls -lS的输出是:

total 16
4 -rw-r--r-- 1 slynux slynux 5 2010-06-29 11:50 other
4 -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test
4 -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test_copy1
4 -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test_copy2

  • 第一行的输出告诉我们文件的总数,这在这种情况下是没有用的。我们使用getline读取第一行,然后将其丢弃。我们需要比较每一行和下一行的大小。为此,我们使用getline显式地读取第一行,并存储名称和大小(它们是第八列和第五列)。因此,使用getline提前读取一行。现在,当awk进入{}块(其中其余的行被读取)时,该块对每次离线读取都执行。它比较当前行获取的大小和存储在size变量中的先前存储的大小。如果它们相等,这意味着两个文件的大小相同。因此,它们需要进一步通过md5sum进行检查。

我们已经采取了一些巧妙的方法来解决这个问题。

awk中可以读取外部命令的输出:

"cmd"| getline

然后我们在行$0中接收输出,每列输出可以在$1,$2,..$n中接收,依此类推。在这里,我们读取csum1csum2变量中文件的 md5sum。变量name1name2用于存储连续的文件名。如果两个文件的校验和相同,则确认它们是重复的,并打印出来。

我们需要找到一组重复文件中的一个文件,以便我们可以删除除一个之外的所有其他重复文件。我们计算重复文件的md5sum并通过仅比较每行的md5sum(使用-w 32md5sum输出的前 32 个字符;通常,md5sum输出由 32 个字符的哈希后跟文件名组成)找到重复文件组中的一个文件。因此,每个重复文件组中的一个样本被写入duplicate_sample

现在,我们需要删除duplicate_files中列出的所有文件,但不包括duplicate_sample中列出的文件。comm命令打印duplicate_files中的文件,但不在duplicate_sample中。

为此,我们使用了一个集合差异操作(参考交集、差异和集合差异的用法)。

comm 始终接受排序后的文件。因此,在重定向到 duplicate_filesduplicate_sample 之前,使用 sort -u 作为过滤器。

这里使用 tee 命令执行一个技巧,以便它可以将文件名传递给 rm 命令以及 printtee 将出现在 stdin 中的行写入文件并将它们发送到 stdout。我们还可以通过重定向到 stderr 将文本打印到终端。/dev/stderr 是对应于 stderr(标准错误)的设备。通过重定向到 stderr 设备文件,通过 stdin 出现的文本将被打印到终端作为标准错误。

另请参阅

  • 基本 awk 入门 第四章 解释了 awk 命令。

  • 校验和验证 第二章 解释了 md5sum 命令。

为长路径创建目录

有时我们需要创建一个空目录树。如果给定路径中存在一些中间目录,还必须包含检查目录是否存在的检查。这将使代码变得更大且低效。让我们看看使用情况和解决问题的示例。

准备就绪

mkdir 是创建目录的命令。例如:

$ mkdir dirpath

如果目录已经存在,它将返回一个 "文件已存在" 的错误消息,如下所示:

mkdir: cannot create directory `dir_name': File exists 

给定一个目录路径(/home/slynux/test/hello/child)。目录 /home/slynux 已经存在。我们需要在路径中创建其余的目录(/home/slynux/test/home/slynux/test/hello/home/slynux/test/hello)。

以下代码用于确定路径中的每个目录是否存在:

if  [ -e /home/slynux ]; then
  # Create next level directory
fi

-e 是在条件构造 [ ] 中使用的参数,用于确定文件是否存在。在类 UNIX 系统中,目录也是一种文件类型。[ -e FILE_PATH ] 如果文件存在,则返回 true。

如何做...

需要执行以下代码序列来在树中创建多层级的目录:

$ mkdir /home 2> /dev/null
$ mkdir /home/slynux 2> /dev/null
$ mkdir /home/slynux/test 2> /dev/null
$ mkdir /home/slynux/test/hello 2> /dev/null
$ mkdir /home/slynux/test/hello/child 2> /dev/null

如果遇到错误,比如 "目录已存在",它会被忽略,并且错误消息会被使用 2> 重定向转储到 /dev/null 设备。但这很冗长且非标准。执行此操作的标准单行命令是:

$ mkdir -p /home/slynux/test/hello/child

这个单一命令代替了上面列出的五个不同的命令。它会忽略任何级别的目录是否存在,并创建缺失的目录。

文件权限、所有权和粘滞位

文件权限和所有权是 UNIX/Linux 文件系统的一个显著特征,如扩展(ext FS)。在许多情况下,在 UNIX/Linux 平台上工作时,我们会遇到与权限和所有权相关的问题。这个示例是权限和所有权的不同用例的演练。

准备就绪

在 Linux 系统中,每个文件都与许多类型的权限相关联。在这些权限中,三组权限(用户、组和其他人)通常被操纵。

用户 是文件的所有者。组是允许对文件进行一些访问的用户集合(由系统定义)。其他是除了文件的用户或组所有者之外的任何实体。

可以使用 ls -l 命令列出文件的权限:

-rw-r--r-- 1 slynux slynux  2497  2010-02-28 11:22 bot.py
-rw-r--r-- 1 slynux slynux  16237 2010-02-06 21:42 c9.php
drwxr-xr-x 2 slynux slynux  4096  2010-05-27 14:31a.py
-rw-r--r-- 1 slynux slynux  539   2010-02-10 09:11 cl.pl

输出的第一列指定了以下内容。第一个字母对应于:

  • "-"——如果是一个普通文件。

  • "d"——如果是一个目录

  • "c"——对于字符设备

  • "b"——对于块设备

  • "l"——如果是一个符号链接

  • "s"——对于套接字

  • "p"——对于管道

其余部分可以分为三组三个字母(------)。前三个---字符对应用户(所有者)的权限,第二组三个字符对应组的权限,第三组三个字符对应其他人的权限。九个字符序列中的每个字符(九个权限)指定了权限是设置还是未设置。如果权限被设置,字符将出现在相应的位置,否则在该位置出现'-'字符,这意味着相应的权限未设置(不可用)。

让我们看看这三个字符集对用户、组和其他人分别意味着什么。

用户:

权限字符串:rwx------

三个字母中的第一个字母指定用户是否对文件具有读取权限。如果用户的读取权限被设置,字符r将出现在第一个位置。类似地,第二个字符指定写入(修改)权限(w),第三个字符指定用户是否具有执行(x)权限(运行文件的权限)。执行权限通常设置为可执行文件。用户还有一个称为 setuid(S)的特殊权限,它出现在执行(x)的位置。setuid 权限使可执行文件在由另一个用户运行时有效地作为其所有者执行。

具有 setuid 权限的文件的示例如下:

-rwS------

读取、写入和执行权限也适用于目录。但是,在目录的上下文中,读取、写入和执行权限的解释略有不同,如下所示:

  • 目录的读取权限(r)使得能够读取目录中文件和子目录的列表

  • 目录的写入权限(w)使得能够在目录中创建或删除文件和子目录

  • 执行权限(x)指定是否可以访问目录中的文件和子目录

组:

权限字符串:---rwx---

第二组三个字符指定组的权限。权限rwx的解释与用户的权限相同。组具有一个称为 setgid(S)的位,而不是 setuid。它使得能够以所有者组的有效组运行可执行文件。但是,启动命令的组可能不同。组权限的示例如下:

----rwS---

其他人:

权限字符串:------rwx

其他权限出现在权限字符串的最后三个字符集中。其他人拥有与用户和组相同的读取、写入和执行权限。但它没有S权限(比如 setuid 和 setgid)。

目录具有一种称为粘滞位的特殊权限。当为目录设置了粘滞位时,创建目录的用户即使组和其他人具有写权限,也只能删除目录中的文件。粘滞位出现在其他人权限集的执行字符(x)的位置。它表示为字符tT。如果执行权限未设置且设置了粘滞位,则t出现在x的位置。如果设置了粘滞位和执行权限,则T出现在x的位置。

例如:

------rwt,------rwT

默认情况下,具有粘滞位的目录的典型示例是/tmp。粘滞位是一种写保护。

在每个ls -l输出行中,字符串slynux slynux对应于所拥有的用户和所拥有的组。这里的第一个'slynux'是用户,第二个'slynux'是组所有者。

如何做到...

为了设置文件的权限,我们使用chmod命令。

假设我们需要设置权限:rwx rw- r--

可以使用 chmod 设置如下:

$ chmod u=rwx g=rw o=r filename

在这里:

  • u =指定用户权限

  • g =指定组权限

  • o =指定其他人的权限

为了在当前文件上添加额外的权限,使用 + 为用户、组或其他添加权限,使用 - 删除权限。为已经具有权限 rwx rw- r-- 的文件添加可执行权限如下:

$ chmod o+x filename

此命令为其他用户添加了 x 权限。

为所有权限类别(用户、组和其他)添加可执行权限如下:

$ chmod a+x filename

这里的 a 表示所有。

为了删除任何权限,使用 -。例如:

$ chmod a-x filename

权限也可以使用八进制数设置。权限用三位数的八进制数表示,其中每个数字对应用户、组和其他。

读、写和执行权限具有以下唯一的八进制数:

  • r-- = 4

  • -w- = 2

  • --x = 1

我们可以通过添加所需权限集的八进制值来获得所需的权限组合。例如:

  • rw- = 4 + 2 = 6

  • r-x = 4 + 1 = 5

数字方法中的权限 rwx rw- r-- 如下:

  • rwx = 4 + 2 + 1 = 7

  • rw- = 4 + 2 = 6

  • r-- = 4

因此,rwx rw- r-- 等于 764,使用八进制值设置权限的命令是:

$ chmod 764 filename

还有更多...

让我们看看可以为文件和目录执行的一些额外任务。

更改所有权

为了更改文件的所有权,使用 chown 命令如下:

$ chown user.group filename

例如:

$ chown slynux.slynux test.sh

这里,slynux 是用户和组。

设置粘滞位

粘滞位是应用于目录的一种有趣的权限类型。通过设置粘滞位,它限制只有拥有它的用户才能删除文件,即使组和其他人有足够的权限。

为了设置粘滞位,在目录上使用 chmod 如下应用 +t

$ chmod a+t directory_name

对文件递归应用权限

有时可能需要递归更改当前目录中所有文件和目录的权限。可以按以下方式完成:

$ chmod 777 . –R

-R 选项指定递归应用更改权限。

我们使用“.” 指定路径为当前工作目录。它相当于:

$ chmod 777 "$(pwd)" –R.
Sarath Lakshman 7 January 2011 8:41 PM

递归应用所有权

我们可以使用 chown 命令的 -R 标志递归应用所有权,如下所示:

$ chown user.group . -R

以不同用户身份运行可执行文件(setuid)

有些可执行文件需要以不同的用户(而不是启动文件执行的当前用户)的身份有效执行,例如通过文件路径 ./executable_name。文件的一种特殊权限属性称为 setuid 权限,使得在其他用户运行程序时有效地以文件所有者的身份执行。

首先将所有权更改为需要每次执行的用户,并登录为所有者用户。然后,运行以下命令:

$ chmod +s executable_file

# chown root.root executable_file
# chmod +s executable_file
$ ./executable_file

现在每次以 root 用户的身份有效执行。

setuid 受限制,因此 setuid 对脚本无效,但对于 Linux ELF 二进制文件有效。这是确保安全性的修复。

使文件不可变

在 Linux 中常见的扩展类型文件系统上(例如 ext2、ext3、ext4 等),可以使文件不可变。某些类型的文件属性帮助设置文件的不可变属性。当文件被设置为不可变时,任何用户或超级用户都无法删除文件,直到从文件中删除不可变属性为止。我们可以通过查看 /etc/mtab 文件轻松找到任何挂载分区的文件系统类型。文件的第一列指定分区设备路径(例如 /dev/sda5),第三列指定文件系统类型(例如 ext3)。让我们看看如何使文件不可变。

准备好了

chattr 可用于使文件不可变。但是,它不是唯一可以通过 chattr 更改的扩展属性。

使文件不可变是保护文件免受修改的方法之一。最著名的例子是/etc/shadow文件。shadow 文件包含当前系统中每个用户的加密密码。通过注入加密密码,我们可以登录到系统。用户通常可以使用passwd命令更改密码。当您执行passwd命令时,它实际上修改了/etc/shadow文件。我们可以使 shadow 文件不可变,以便任何用户都无法更改密码。让我们看看如何做到这一点。

如何做...

可以按照以下方式使文件不可变:

chattr +i file

或者:

$ sudo chattr +i file

因此文件被设置为不可变。现在尝试以下命令:

rm file
rm: cannot remove `file': Operation not permitted

为了使其可写,按照以下方式移除不可变属性:

chattr -i file

批量生成空文件

有时我们可能需要生成测试用例。我们可能会使用操作数千个文件的程序。但是测试文件是如何生成的呢?

准备就绪

touch是一个可以创建空文件或修改文件时间戳的命令。让我们看看如何使用它们。

如何做...

使用以下命令将创建一个名为filename的空文件:

$ touch filename

按照以下方式生成具有不同名称模式的批量文件:

for name in {1..100}.txt
do
touch $name
done

在上面的代码中,{1..100}将被扩展为字符串"1, 2, 3, 4, 5, 6, 7...100"。我们可以使用各种简写模式,如test{1..200}.ctest{a..z}.txt等,而不是{1..100}.txt

如果文件已经存在,则touch命令会将与文件相关的所有时间戳更改为当前时间。但是,如果我们想要指定只修改某些时间戳,我们可以使用以下选项:

  • touch -a仅修改访问时间

  • touch -m仅修改修改时间

我们可以按照以下方式指定时间和日期来为文件盖上时间戳,而不是使用当前时间:

$ touch -d "Fri Jun 25 20:50:14 IST 1999" filename

-d一起使用的日期字符串不一定总是以相同的格式。它将接受任何标准日期格式。我们可以从字符串中省略时间,并提供方便的日期格式,如“Jan 20 2010”。

查找符号链接及其目标

符号链接在类 UNIX 系统中很常见。我们可能会遇到基于符号链接的各种操作。这个示例可能没有任何实际目的,但它可以练习处理符号链接,这可能有助于编写其他目的的 shell 脚本。

准备就绪

符号链接只是指向其他文件的指针。它们在功能上类似于 Mac OS X 中的别名或 Windows 中的快捷方式。删除符号链接时,不会对原始文件造成任何伤害。

如何做...

我们可以按照以下方式创建符号链接:

$ ln -s target symbolic_link_name

例如:

$ ln –l -s /var/www/ ~/web

这在登录用户的主目录中创建了一个名为“web”的符号链接。该链接指向/var/www/。这可以在以下命令的输出中看到:

$ ls web
lrwxrwxrwx 1 slynux slynux 8 2010-06-25 21:34 web -> /var/www

web -> /var/www指定web指向/var/www

对于每个符号链接,权限表示块(lrwxrwxrwx)以字母“l”开头,表示符号链接。

因此,为了打印当前目录中的符号链接,请使用以下命令:

$ ls -l | grep "^l" | awk '{ print $8 }'

grep将过滤ls -l输出的行,以便仅显示以 l 开头的行。^是字符串的起始标记。awk用于打印第八列。因此它打印第八列,也就是文件名。

打印符号链接的另一种方法是使用find,如下所示:

$ find . -type l -print

在上述命令中,在find参数type中,我们指定了“l”,这将指示find命令仅搜索符号链接文件。-print选项用于将符号链接列表打印到标准输出(stdout)。文件搜索应该从当前目录开始,给出为'.'。

为了打印符号链接的目标,请使用以下命令:

$ ls -l web | awk '{ print $10 }'
/var/www

ls –l命令列出每行对应文件的许多细节。ls –l web列出名为web的文件的细节,这是一个符号链接。ls –l输出的第十列包含文件指向的链接(如果文件是符号链接)。因此,为了找到与符号链接关联的目标,我们可以使用awk从文件详细列表(从ls –l的输出)中打印第十列。

或者,我们可以使用标准方法来读取给定符号链接的目标路径,使用readlink命令。这是最常用的方法,可以如下使用:

$ readlink web
/var/www

枚举文件类型统计

有许多文件类型。编写一个脚本来枚举目录内所有文件及其后代,并打印提供文件类型(不同文件类型的文件)和每种文件类型的数量的报告将是一个有趣的练习。这个配方是一个关于如何编写脚本来枚举大量文件并收集详细信息的练习。

准备好

文件命令可用于通过查看文件的内容来查找文件的类型。在 UNIX/Linux 系统中,文件类型不是基于文件的扩展名确定的(就像 Microsoft Windows 平台那样)。这个配方旨在收集一些文件的文件类型统计信息。为了存储相同类型文件的计数,我们可以使用一个关联数组,file命令可以用于从每个文件中获取文件类型的详细信息。

如何做...

为了打印文件的文件类型,使用以下命令:

$ file filename

$ file /etc/passwd
/etc/passwd: ASCII text

仅通过排除文件名打印文件类型如下:

$ file -b filename
ASCII text

文件统计脚本如下:

#!/bin/bash
# Filename: filestat.sh

if [ $# -ne 1 ];
then
  echo $0 basepath;
  echo
fi
path=$1

declare -A statarray;

while read line;
do
  ftype=`file -b "$line"`
  let statarray["$ftype"]++;

done< <(find $path -type f -print)

echo ============ File types and counts =============
for ftype in "${!statarray[@]}";
do
  echo $ftype :  ${statarray["$ftype"]}
done

用法如下:

$ ./filestat.sh /home/slynux/temp

下面显示了一个示例输出:

$ ./filetype.sh /home/slynux/programs
============ File types and counts =============
Vim swap file : 1
ELF 32-bit LSB executable : 6
ASCII text : 2
ASCII C program text : 10

它是如何工作的...

在这里,声明了一个名为statarray的关联数组,以便它可以将文件类型作为文件索引并将每种文件类型的计数存储在数组中。每次遇到文件类型时,使用let来增加计数。使用find命令递归获取文件路径列表。使用while循环逐行迭代find命令的输出。在前一个脚本中,使用输入行ftype=file -b "$line"``来使用file命令找出文件类型。–b选项指定文件命令仅打印文件类型(在输出中不包括文件名)。文件类型输出包括更多细节,例如图像编码和分辨率(在图像文件的情况下)。但我们对更多细节不感兴趣,我们只需要基本信息。详细信息以逗号分隔,如下例所示:

$ file a.out -b
ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped

我们需要仅从上述细节中提取"ELF 32 位 LSB 可执行文件"。因此,我们使用cut –d, -f1,它指定使用","作为分隔符,并仅打印第一个字段。

完成< <(find $path –type f –print);是一段重要的代码。逻辑如下:

读取行;

某事

完成<文件名

我们使用find的输出而不是文件名。

<(find $path -type f -print)相当于一个文件名。但它用子进程输出替换了文件名。请注意,这里有一个额外的<

${!statarray[@]}用于返回数组索引的列表。

回环文件和挂载

回环文件系统是 Linux 系统中非常有趣的组件。我们通常在设备上创建文件系统(例如,磁盘驱动器分区)。这些存储设备可用作设备文件,如/dev/device_name。为了使用存储设备文件系统,我们需要将其挂载到某个目录,称为挂载点。回环文件系统是我们在文件中创建的文件系统,而不是物理设备。我们可以将这些文件挂载为设备到挂载点。让我们看看如何做到这一点。

准备好

回环文件系统驻留在一个文件上。我们通过将这些文件附加到设备文件来挂载这些文件。回环文件系统的一个示例是初始 ramdisk 文件,您可以在boot/initrd.img中看到。它在一个文件中存储了内核的初始文件系统。

让我们看看如何在大小为 1GB 的文件上创建 ext4 文件系统。

如何做...

以下命令将创建一个大小为 1GB 的文件。

$ dd if=/dev/zero of=loopbackfile.img bs=1G count=1
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 37.3155 s, 28.8 MB/s

您可以看到所创建文件的大小超过了 1GB。这是因为硬盘是一个块设备,因此存储是按块大小的整数倍分配的。

现在使用mkfs命令格式化 1GB 文件如下:

# mkfs.ext4 loopbackfile.img

此命令将其格式化为 ext4。使用以下命令检查文件类型:

$ sudo file loopbackfile.img
loopbackfile.img: Linux rev 1.0 ext4 filesystem data, UUID=c9d56c42-f8e6-4cbd-aeab-369d5056660a (extents) (large files) (huge files)

现在您可以按以下方式挂载回环文件:

$ sudo mkdir /mnt/loopback
# mount -o loop loopback.img /mnt/loopback

-o loop附加选项用于挂载任何回环文件系统。

这是快捷方法。我们不将其附加到任何设备上。但在内部,它会附加到一个名为/dev/loop1loop2的设备上。

我们可以手动执行如下操作:

# losetup /dev/loop1 loopback.img
# mount /dev/loop1 /mnt/loopback

第一种方法并非在所有情况下都适用。假设我们想要创建一个硬盘文件,然后想要对其进行分区并挂载一个子分区,我们不能使用mount -o loop。我们必须使用第二种方法。按以下方式对零转储文件进行分区:

# losetup /dev/loop1 loopback.img
# fdisk /dev/loop1

loopback.img中创建分区,以便按以下方式挂载第一个分区:

# losetup -o 32256 /dev/loop2 loopback.img

现在/dev/loop2代表第一个分区。

-o是偏移标志。32256字节用于 DOS 分区方案。第一个分区从硬盘的起始位置偏移 32256 字节后开始。

我们可以通过指定所需的偏移量来设置第二个分区。挂载后,我们可以执行所有常规操作,就像在物理设备上一样。

为了umount,使用以下语法:

# umount mount_point

例如:

# umount /mnt/sda1

或者,我们可以使用设备文件路径作为umount命令的参数,如:

# umount /dev/sda1

请注意,umount命令应以 root 用户身份执行,因为它是一个特权命令。

还有更多...

让我们更多地了解附加挂载选项。

作为回环挂载挂载 ISO 文件

ISO 文件是任何光学介质的存档。我们可以通过回环挂载的方式挂载 ISO 文件,就像我们挂载物理光盘一样。

挂载点只是一个目录,用作通过文件系统访问设备内容的访问路径。我们甚至可以使用非空目录作为挂载路径。然后,挂载路径将包含来自设备的数据,而不是原始内容,直到设备被卸载。例如:

# mkdir /mnt/iso
# mount -o loop linux.iso /mnt/iso

现在使用/mnt/iso中的文件执行操作。ISO 是一个只读文件系统。

使用 sync 立即刷新更改

在挂载设备上进行更改时,更改不会立即写入物理设备。只有当缓冲区满时才会写入。但是我们可以使用sync命令强制立即写入更改,如下所示:

# sync

您应该以 root 身份执行sync命令。

创建 ISO 文件,混合 ISO

ISO 映像是一种存储光盘的确切存储映像的存档格式,如 CD-ROM、DVD-ROM 等。我们通常将 ISO 映像刻录到光盘上。但是,如果您想要创建光盘的映像,该怎么办?为此,我们需要从光盘创建 ISO 映像。许多人依赖第三方实用程序从光盘创建 ISO 映像。但是,使用命令行,这只是一个单行工作。

此外,许多人不区分可引导和不可引导的光盘。可引导光盘能够从自身引导,并运行操作系统或其他产品。不可引导的 ISO 无法做到这一点。人们通常遵循的做法是从可引导 CD-ROM 复制文件并将其粘贴到另一个位置以保留副本。之后,他们使用复制的目录来刻录 CD-ROM。但是,这样做将失去其可引导性质。为了保持可引导性质,应将其复制为磁盘映像或 ISO 文件。

如今,大多数人使用闪存驱动器或硬盘等设备来替代光盘。当我们将可引导的 ISO 写入闪存驱动器时,除非使用专门为此目的设计的特殊混合 ISO 映像,否则它将不再是可引导的。

这个配方将让您深入了解 ISO 映像和操作。

准备好了

正如我们在本书中多次描述的那样,UNIX 将所有内容都视为文件。每个设备都是一个文件。因此,如果我们想要复制设备的精确映像,该怎么办?我们需要从中读取所有数据并写入另一个文件,对吧?

我们知道,cat命令可以用于读取任何数据,并且可以使用重定向将其写入文件。

如何做到...

要从/dev/cdrom创建 ISO 映像,请使用以下命令:

# cat /dev/cdrom > image.iso

这将起作用,它将读取设备的所有字节并写入 ISO 映像。

使用cat命令创建 ISO 映像是一种棘手的方法。但创建 ISO 映像的最佳方式是使用dd实用程序。

# dd if=/dev/cdrom of=image.iso

mkisofs是用于创建 ISO 系统的命令。mkisofs的输出文件可以使用诸如cdrecord之类的实用程序写入 CD ROM 或 DVD ROM。我们可以使用mkisofs创建一个包含所有所需文件的目录的 ISO 文件,这些文件应该出现为 ISO 文件的内容,如下所示:

$ mkisofs -V "Label" -o image.iso source_dir/ 

mkisofs命令中的-o选项指定 ISO 文件路径。source_dir是应用作 ISO 源内容的目录路径,-V选项指定应用于 ISO 文件的标签。

还有更多...

让我们学习更多与 ISO 文件相关的命令和技术。

从闪存驱动器或硬盘引导的混合 ISO

通常,可引导的 ISO 文件无法传输或写入 USB 存储设备,并从 USB 键引导操作系统。但是称为混合 ISO 的特殊类型的 ISO 文件可以被刷写,并且能够从这些设备引导。

我们可以使用isohybrid命令将标准 ISO 文件转换为混合 ISO。isohybrid命令是一个新的实用程序,大多数 Linux 发行版默认情况下不包括此命令。您可以从以下网址下载 syslinux 软件包:syslinux.zytor.com

看看以下命令:

# isohybrid image.iso

使用此命令,我们将获得一个名为image.iso的混合 ISO,并且可以将其写入 USB 存储设备。

使用以下命令将 ISO 写入 USB 存储:

# dd if=image.iso of=/dev/sdb1 

使用适当的设备替代sdb1

或者,您可以使用以下命令:

# cat image.iso > /dev/sdb1

从命令行刻录 ISO

cdrecord命令用于将 ISO 文件刻录到 CD ROM 或 DVD ROM 中。可以使用以下命令将图像刻录到 CD ROM 中:

# cdrecord -v dev=/dev/cdrom image.iso

一些额外的选项如下:

  • 我们可以使用-speed选项指定刻录速度,如下所示:
-speed SPEED

例如:

# cdrecord –v dev=/dev/cdrom image.iso –speed 8

速度为 8x,指定为 8。

  • CD ROM 可以进行多会话刻录,这样我们可以多次在一张光盘上刻录数据。可以使用-multi选项执行多会话刻录,如下所示:
# cdrecord –v dev=/dev/cdrom image.iso -multi

玩转 CD ROM 托盘

尝试以下命令并玩得开心:

  • $ eject

此命令用于弹出托盘。

  • $ eject -t

此命令用于关闭托盘。

尝试编写一个循环,打开托盘并关闭托盘“N”次。

查找文件之间的差异,打补丁

当文件有多个版本可用时,将文件之间的差异突出显示比手动比较两个文件更有用。如果文件有 1000 多行,手动比较实际上非常困难和耗时。本教程说明了如何生成带有行号的文件之间的差异。在多个开发人员处理大文件时,当其中一个人进行了更改并且需要向其他人显示这些更改时,将整个源代码发送给其他开发人员在空间和时间上都是昂贵的,手动检查更改。发送一个不同的文件是有帮助的。它只包含已更改、添加或删除的行,并附有行号。这个差异文件称为补丁文件。我们可以使用补丁命令将补丁文件中指定的更改添加到原始源代码中。我们也可以通过再次打补丁来还原更改。让我们看看如何做到这一点。

如何做...

diff命令实用程序用于生成差异文件。

为了生成差异信息,创建以下文件:

  • 文件 1:version1.txt
this is the original text
line2
line3
line4
happy hacking !
  • 文件 2:version2.txt
this is the original text 
line2
line4
happy hacking ! 
GNU is not UNIX

非统一的diff输出(不带-u标志)将如下所示:

$ diff version1.txt version2.txt 
3d2
<line3
6c5
> GNU is not UNIX

统一的diff输出将如下所示:

$ diff -u version1.txt version2.txt
--- version1.txt	2010-06-27 10:26:54.384884455 +0530 
+++ version2.txt	2010-06-27 10:27:28.782140889 +0530 
@@ -1,5 +1,5 @@ 
this is the original text 
line2
-line3
line4
happy hacking !
-
+GNU is not UNIX

-u选项用于生成统一的输出。每个人都更喜欢统一的输出,因为统一的输出更易读,而且更容易解释两个文件之间所做的差异。

在统一的diff中,以+开头的行是新添加的行,以-开头的行是被删除的行。

可以通过将diff输出重定向到文件来生成补丁文件,如下所示:

$ diff -u version1.txt version2.txt > version.patch

现在使用补丁命令,我们可以将更改应用到任何两个文件中。当应用到version1.txt时,我们得到version2.txt文件。当应用到version2.txt时,我们得到version1.txt

使用以下命令应用补丁:

$ patch -p1 version1.txt < version.patch
patching file version1.txt

现在我们有了与version2.txt内容相同的version1.txt

为了将更改还原,使用以下命令:

$ patch -p1 version1.txt < version.patch 
patching file version1.txt
Reversed (or previously applied) patch detected!  Assume -R? [n] y
#Changes are reverted.

使用-R选项以及补丁命令来在不提示用户输入y/n的情况下还原更改。

还有更多...

让我们来看一下diff提供的其他功能。

针对目录生成差异

diff命令也可以递归地针对目录进行操作。它将为目录中所有后代文件生成差异输出。

使用以下命令:

$ diff -Naur directory1 directory2

上述每个选项的解释如下:

  • -N是用于将缺失的文件视为空文件

  • -a是为了将所有文件视为文本文件

  • -u是为了生成统一的输出

  • -r是为了递归遍历目录中的文件

head 和 tail - 打印最后或前 10 行

当查看一个包含成千上万行的大文件时,我们不会使用cat命令来打印整个文件内容。相反,我们寻找一个样本(例如,文件的前 10 行或最后 10 行)。我们可能还需要打印前 n 行或最后 n 行。还可能需要打印除了最后的“n”行之外的所有行或除了前面的“n”行之外的所有行。

另一个用例是打印从第 n 行到第 m 行的行。

headtail命令可以帮助我们做到这一点。

如何做...

head命令总是读取输入文件的头部部分。

如下所示打印前 10 行:

$ head file

从 stdin 读取数据如下:

$ cat text | head

指定要打印的前几行的数量如下:

$ head -n 4 file

这个命令打印四行。

如下所示打印除了最后的N行之外的所有行:

$ head -n -N file

请注意这是负 N。

例如,要打印除了最后 5 行之外的所有行,请使用以下代码:

$ seq 11 | head -n -5
1
2
3
4
5
6

然而,以下命令将从 1 打印到 5:

$ seq 100 | head -n 5

通过排除最后几行进行打印是head的一个非常重要的用法。但是人们总是寻找其他复杂的方法来做同样的事情。

打印文件的最后 10 行如下:

$ tail file

为了从stdin读取,您可以使用以下代码:

$ cat text | tail

打印最后 5 行如下:

$ tail -n 5 file

为了打印除了前 N 行之外的所有行,请使用以下代码:

$ tail -n +(N+1)

例如,要打印除了前 5 行之外的所有行,N + 1 = 6,因此命令将如下所示:

$ seq 100 | tail -n +6 

这将打印从 6 到 100。

tail的一个重要用途是读取不断增长的文件。由于新行不断附加到文件的末尾,tail可以用来显示随着写入文件而不断增加的所有新行。当我们简单运行tail时,它将读取最后 10 行并退出。然而,到那时,某个进程可能已经向文件附加了新行。为了不断监视文件的增长,tail有一个特殊选项-f--follow,它使tail能够跟踪附加的行并保持与数据增长的更新:

$ tail -f growing_file

这种增长文件的一个例子是日志文件。监视文件增长的命令将是:

# tail -f /var/log/messages

或者

$ dmesg | tail -f

我们经常运行dmesg来查看内核环形缓冲区消息,无论是调试 USB 设备还是查看sdXXsd设备的次要编号)。tail -f还可以添加一个睡眠间隔-s,这样我们就可以设置在监视文件更新的时间间隔。

tail具有一个有趣的属性,允许它在给定的进程 ID 死亡后终止。

假设我们正在读取一个增长的文件,并且一个进程Foo正在向文件附加数据,应该执行tail -f直到进程Foo死亡。

$ PID=$(pidof Foo)
$ tail -f file --pid $PID

当进程Foo终止时,tail也会终止。

让我们来看一个例子。

使用任何文本编辑器创建一个新文件file.txt并打开文件。

在 gedit 中向文件添加新行并频繁保存文件。

现在运行:

$ PID=$(pidof gedit)
$ tail -f file.txt --pid $PID

当您频繁更改文件时,tail命令将将其写入终端。当您关闭gedit时,tail命令将被终止。

仅列出目录-替代方法

尽管只列出目录似乎是一个简单的任务,但许多人可能无法做到。我经常看到这种情况,即使是问擅长 shell 脚本的人也是如此。这个技巧很值得知道,因为它介绍了多种只列出目录的技巧和技术。

准备工作

有多种只列出目录的方法。当您询问人们这些技术时,他们可能会给出的第一个答案可能是dir。但是,这是错误的。dir命令只是另一个像ls一样的命令,比ls的选项少。让我们看看如何列出目录。

如何做到...

当前路径中目录可以显示的四种方式。它们是:

  • *$ ls -d /

只有与-d结合使用的组合才会打印目录。

  • $ ls -F | grep "/$"

当使用-F参数时,所有条目都附加有某种文件字符,如@*|等。对于目录,条目都附加有/字符。我们使用grep来过滤只以/$结尾的条目。

  • $ ls -l | grep "^d"

ls -d输出每个文件条目的行的第一个字符是文件类型字符。对于目录,文件类型字符是"d"。因此我们使用grep来过滤以"d"开头的行。^是行起始指示符。

  • $ find . -type d -maxdepth 1 -print

find命令可以使用参数type作为目录,并且maxdepth设置为1,因为它不应搜索后代目录。

使用 pushd 和 popd 进行快速命令行导航

在终端或 shell 提示符上处理多个位置时,我们的常见做法是复制和粘贴路径。只有在使用鼠标时,复制粘贴才有效。当只有命令行访问而没有 GUI 时,很难通过多个路径进行导航。例如,如果我们正在处理位置/var/www/home/slynux/usr/src,当我们需要逐个导航到这些位置时,每次需要在路径之间切换时键入路径是非常困难的。因此,基于命令行界面(CLI)的导航技术,如 pushd 和 popd 被使用。让我们看看如何练习它们。

准备就绪

pushdpopd用于在多个目录之间切换,而无需复制粘贴目录路径。pushdpopd在堆栈上操作。我们知道堆栈是后进先出(LIFO)的数据结构。它将在堆栈中存储目录路径,并使用推送和弹出操作在它们之间切换。

如何做到...

在使用pushdpopd时,我们省略了cd命令的使用。

为了推送并更改目录到一个路径使用:

~ $ pushd /var/www

现在堆栈包含/var/www ~,当前目录更改为/var/www

现在再次按以下方式推送下一个目录路径:

/var/www $ pushd /usr/src

现在堆栈包含/usr/src /var/www ~,当前目录是/usr/src

您可以类似地推送所需的许多目录路径。

使用以下命令查看堆栈内容:

$ dirs
/usr/src /var/www ~ /usr/share /etc
0        1        2 3          4 

当您想要切换到列表中的任何路径时,从0n为每个路径编号,然后使用需要切换的路径编号,例如:

$ pushd +3

它将旋转堆栈并切换到目录/usr/share

pushd将始终将路径添加到堆栈中,要从堆栈中删除路径,请使用popd

通过使用以下方法删除最后推送的路径并更改目录到下一个目录:

$ popd

假设堆栈是/usr/src /var/www ~ /usr/share /etc,当前目录是/usr/srcpopd将把堆栈更改为/var/www ~ /usr/share /etc并将目录更改为/var/www

为了从列表中删除特定路径,使用popd +no

no从左到右被计为0n

还有更多...

让我们看看基本的目录导航实践。

最常用的目录切换

当使用三个以上的目录路径时,可以使用pushdpopd。但是当您只使用两个位置时,有一种替代和更简单的方法。那就是cd -

如果当前路径是/var/www,执行以下操作:

/var/www $  cd /usr/src
/usr/src $ # do something

现在要切换回/var/www,您不必再次输入,只需执行:

/usr/src $ cd -

现在您可以按以下方式切换到/usr/src

/var/www $ cd -

计算文件中的行数、单词数和字符数

对文本或文件中的行数、单词数和字符数进行计数对于文本操作非常有用。在几种情况下,单词或字符的计数以间接方式用于执行一些技巧,以产生所需的输出模式和结果。本书在其他章节中包括了一些这样棘手的例子。计数 LOC代码行数)对于开发人员来说是一个重要的应用。我们可能需要计算特定类型的文件,而排除不必要的文件。wc与其他命令的组合有助于执行这项工作。

准备就绪

wc是用于计数的实用程序。它代表Word Count (wc)。让我们看看如何使用wc来计算行数、单词数和字符数。

如何做到...

按以下方式计算行数:

$ wc -l file

为了使用stdin作为输入,使用以下命令:

$ cat file | wc -l

按以下方式计算单词数:

$ wc -w file
$ cat file | wc -w

为了计算字符数,请使用:

$ wc -c file
$ cat file | wc -c

例如,我们可以按以下方式计算文本中的字符数:

echo -n 1234 | wc -c
4

-n用于避免额外的换行符。

wc没有任何选项执行时:

$ wc file

它将打印由制表符分隔的行数、单词数和字符数。

还有更多...

让我们看看wc命令的其他可用选项。

打印最长行的长度

wc也可以使用-L选项打印最长行的长度:

$ wc file -L

打印目录树

以图形方式表示目录和文件系统的树层次结构在准备教程和文档时非常有用。有时,在编写某些监控脚本时,使用易于阅读的树形表示来查看文件系统也是很有用的。让我们看看如何做到这一点。

准备工作

tree命令是帮助打印文件和目录的图形树的英雄。通常,tree不随 Linux 发行版一起提供。您需要使用软件包管理器安装它。

如何做...

以下是一个示例 UNIX 文件系统树:

$ tree ~/unixfs
unixfs/
|-- bin
|   |-- cat
|   `-- ls
|-- etc
|   `-- passwd
|-- home
|   |-- pactpub
|   |   |-- automate.sh
|   |   `-- schedule
|   `-- slynux
|-- opt
|-- tmp
`-- usr
8 directories, 5 files

tree命令带有许多有趣的选项,让我们看看其中的一些。

仅突出显示与模式匹配的文件,如下所示:

$ tree path -P PATTERN # Pattern should be wildcard

例如:

$ tree PATH -P "*.sh" # Replace PATH with a directory path
|-- home
|   |-- pactpub
|   |   `-- automate.sh

仅通过使用排除匹配模式来突出显示文件:

$ tree path -I PATTERN

为了打印大小以及文件和目录,使用-h选项如下:

$ tree -h

还有更多...

让我们看看tree命令提供的一个有趣选项。

树的 HTML 输出

可以从tree命令生成 HTML 输出。例如,使用以下命令创建一个带有树输出的 HTML 文件。

$ tree PATH -H http://localhost -o out.html

http://localhost替换为您想要托管文件的 URL。将 PATH 替换为基本目录的真实路径。对于当前目录,请使用'.'作为路径。

从目录列表生成的网页将如下所示:

树的 HTML 输出

第四章:发短信和开车

在这一章中,我们将涵盖:

  • 基本正则表达式入门

  • 使用 grep 在文件中搜索和挖掘“文本”

  • 使用 cut 按列切割文件

  • 确定给定文件中使用的单词频率

  • 基本 sed 入门

  • 基本 awk 入门

  • 从文本或文件中替换字符串

  • 压缩或解压 JavaScript

  • 在文件中迭代行、单词和字符

  • 将多个文件合并为列

  • 在文件或行中打印第 n 个单词或列

  • 打印行号或模式之间的文本

  • 使用脚本检查回文字符串

  • 以相反的顺序打印行

  • 从文本中解析电子邮件地址和 URL

  • 在文件中打印模式之前或之后的一组行

  • 从文件中删除包含特定单词的句子

  • 使用 awk 实现 head、tail 和 tac

  • 文本切片和参数操作

介绍

Shell 脚本语言中包含了 UNIX/Linux 系统的基本问题解决组件。Bash 总是可以为 UNIX 环境中的问题提供一些快速解决方案。文本处理是 Shell 脚本使用的关键领域之一。它带有诸如 sed、awk、grep、cut 等美丽的实用程序,可以组合使用以解决与文本处理相关的问题。大多数编程语言都设计为通用的,因此编写能够处理文本并产生所需输出的程序需要付出很大的努力。由于 Bash 是一种考虑到文本处理的语言,它具有许多功能。

各种实用程序帮助以字符、行、单词、列、行等细节处理文件。因此我们可以以多种方式操纵文本文件。正则表达式是模式匹配技术的核心。大多数文本处理实用程序都带有正则表达式支持。通过使用合适的正则表达式字符串,我们可以产生所需的输出,如过滤、剥离、替换、搜索等。

本章包括了一系列配方,涵盖了基于文本处理的许多问题背景,这将有助于编写真实脚本。

基本正则表达式入门

正则表达式是基于模式匹配的文本处理技术的核心。要流利地编写文本处理工具,必须对正则表达式有基本的理解。正则表达式是一种微型、高度专门化的编程语言,用于匹配文本。使用通配符技术,使用模式匹配文本的范围非常有限。这个配方是基本正则表达式的介绍。

准备工作

正则表达式是大多数文本处理实用程序中使用的语言。因此,您将在许多其他配方中使用在本配方中学到的技术。[a-z0-9_]+@[a-z0-9]+\.[a-z]+ 是一个用于匹配电子邮件地址的正则表达式的示例。

这看起来奇怪吗?别担心,一旦你理解了概念,它就真的很简单。

如何做...

在本节中,我们将介绍正则表达式、POSIX 字符类和元字符。

让我们首先了解正则表达式(regex)的基本组件。

regex 描述 示例
^ 行首标记。 ^tux 匹配以tux开头的行的字符串。
` regex 描述
--- --- ---
^ 行首标记。 ^tux 匹配以tux开头的行的字符串。
| 行尾标记。 | tux$ 匹配以tux结尾的行的字符串。
. 匹配任何一个字符。 Hack. 匹配 Hack1Hacki 但不匹配 Hack12Hackil,只有一个额外的字符匹配。
[] 匹配[chars]中包含的任何一个字符。 coo[kl] 匹配 cookcool
[^] 匹配除了[^chars]中包含的字符之外的任何一个字符。 9[⁰¹] 匹配 9293 但不匹配 9190
[-] 匹配[]中指定范围内的任何字符。 [1-5] 匹配从 15 的任何数字。
? 前面的项目必须匹配一次或零次。 colou?r匹配colorcolour但不匹配colouur
+ 前面的项目必须匹配一次或多次。 Rollno-9+匹配Rollno-99Rollno-9但不匹配Rollno-
* 前面的项目必须匹配零次或多次。 co*l匹配clcolcoool
() 从正则表达式匹配中创建一个子字符串。 ma(tri)?x匹配maxmatrix
{n} 前面的项目必须匹配 n 次。 [0-9]{3}匹配任意三位数。[0-9]{3}可以扩展为:[0-9][0-9][0-9]
{n,} 前面的项目应该至少匹配的次数。 [0-9]{2,}匹配任何数字,即两位数或更多。
{n, m} 指定前面的项目应该匹配的最小和最大次数。 [0-9]{2,5}匹配任何有两到五位数字的数字。
&#124; 交替-|两侧的项目之一应该匹配。 Oct (1st &#124; 2nd)匹配Oct 1stOct 2nd
\ 用于转义上述任何特殊字符的转义字符。 a\.b匹配a.b但不匹配ajb。它通过前缀\忽略.的特殊含义。

POSIX 字符类是一种特殊的元序列,形式为[:...:],可用于匹配指定字符范围。POSIX 类如下:

正则表达式 描述 例子
[:alnum:] 字母数字字符 [[:alnum:]]+
[:alpha:] 字母字符(小写和大写) [[:alpha:]]{4}
[:blank:] 空格和制表符 [[:blank:]]*
[:digit:] 数字 [[:digit:]]?
[:lower:] 小写字母 [[:lower:]]{5,}
[:upper:] 大写字母 ([[:upper:]]+)?
[:punct:] 标点符号 [[:punct:]]
[:space:] 包括换行符、回车符等所有空白字符。 [[:space:]]+

元字符是一种 Perl 风格的正则表达式,它受到一些文本处理实用程序的支持。并非所有实用程序都支持以下符号。但上述字符类和正则表达式是被普遍接受的。

正则表达式 描述 例子
\b 单词边界 \bcool\b只匹配cool而不匹配coolant
\B 非单词边界 cool\B匹配coolant而不是cool
\d 单个数字字符 b\db匹配b2b而不是bcb
\D 单个非数字 b\Db匹配bcb而不是b2b
\w 单个单词字符(字母数字和 _) \w匹配1a而不是&
\W 单个非单词字符 \w匹配&而不是1a
\n 换行符 \n匹配一个换行符。
\s 单个空格 x\sx匹配xx而不是xx
\S 单个非空格 x\Sx匹配xkx而不是xx
\r 回车 \r匹配回车。

它是如何工作的...

在前一节中看到的表格是正则表达式的关键元素表。通过使用表中的合适键,我们可以构建任何适当的正则表达式字符串来根据上下文匹配文本。正则表达式是一种通用语言,用于匹配文本。因此,在本教程中我们不会介绍任何工具。但是,它遵循本章中的其他教程。

让我们看一些文本匹配的例子:

  • 为了匹配给定文本中的所有单词,我们可以将正则表达式写成:
( ?[a-zA-Z]+ ?)

"?"是在单词前后表示可选空格的符号。[a-zA-Z]+表示一个或多个字母字符(a-z 和 A-Z)。

  • 为了匹配 IP 地址,我们可以将正则表达式写成:
[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}

或者

[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}

我们知道 IP 地址的格式是 192.168.0.2. 它是由四个整数(每个从 0-255)用点分隔的形式(例如,192.168.0.2)。

[0-9][:digit:]表示匹配数字 0-9。{1,3}匹配一到三个数字,\.匹配“.”。

还有更多...

让我们看看正则表达式中特定字符的特殊含义是如何指定的。

特殊字符的处理

正则表达式使用一些特殊字符,如$^.*+{}。但如果我们想将这些字符用作非特殊字符(普通文本字符)呢?让我们看一个例子。

正则表达式:[a-z]*.[0-9]

这是如何解释的?

它可以是零个或多个[a-z] ([a-z]*),然后是任意一个字符(.),然后是集合[0-9]中的一个字符,使其匹配abcdeO9

它也可以被解释为[a-z]之一,然后是一个字符*,然后是一个字符.(句号),然后是一个数字,使其匹配x*.8

为了克服这个问题,我们在字符前加上反斜杠“\”(这样做称为“转义字符”)。具有多重含义的字符,如*,前缀为“\”,使其成为特殊含义或使其成为非特殊含义。是否转义特殊字符或非特殊字符取决于您正在使用的工具。

使用 grep 在文件中搜索和挖掘“文本”

在文件中搜索是文本处理中的一个重要用例。我们可能需要通过文件中的数千行进行搜索,以便通过使用某些规范找出一些所需的数据。这个示例将帮助您学习如何从数据池中定位给定规范的数据项。

做好准备

grep命令是在文本中搜索的主要 UNIX 实用程序。它接受正则表达式和通配符。我们可以使用grep提供的许多有趣选项以各种格式生成输出。让我们看看如何做到这一点。

如何做...

在文件中搜索单词如下:

$ grep match_pattern filename
this is the line containing match_pattern

或:

$ grep "match_pattern" filename
this is the line containing match_pattern

它将返回包含给定match_pattern的文本行。

我们也可以按如下方式从stdin读取:

$ echo -e "this is a word\nnext line" | grep word 
this is a word

使用单个grep调用在多个文件中执行搜索如下:

$ grep "match_text" file1 file2 file3 ... 

我们可以使用--color选项在行中突出显示单词如下:

$ grep word filename –-color=auto
this is the line containing word

通常,grep命令将match_text视为通配符。要将正则表达式用作输入参数,应添加-E选项,这意味着扩展的正则表达式。或者我们可以使用启用正则表达式的grep命令,egrep。例如:

$ grep -E "[a-z]+"

或:

$ egrep "[a-z]+"

为了仅输出文件中文本的匹配部分,请使用-o选项如下:

$ echo this is a line. | grep -o -E "[a-z]+\."
line

或:

$ echo this is a line. | egrep -o "[a-z]+\."
line.

为了打印除包含match_pattern的行之外的所有行,请使用:

$ grep -v  match_pattern file

grep中添加-v选项可以反转匹配结果。

计算文件或文本中出现匹配字符串或正则表达式的行数如下:

$ grep -c "text" filename
10

应该注意,-c仅计算匹配行的数量,而不是匹配的次数。例如:

$ echo -e "1 2 3 4\nhello\n5 6" | egrep  -c "[0-9]"
2

尽管有 6 个匹配项,但它打印出2,因为只有2个匹配行。单行中的多个匹配只计算一次。

为了计算文件中匹配项的数量,请使用以下技巧:

$ echo -e "1 2 3 4\nhello\n5 6" | egrep  -o "[0-9]" | wc -l
6

打印匹配字符串的行号如下:

$ cat sample1.txt
gnu is not unix
linux is fun
bash is art
$ cat sample2.txt
planetlinux

$ grep linux -n sample1.txt
2:linux is fun

或:

$ cat sample1.txt | grep linux -n

如果使用多个文件,它还将打印带有结果的文件名如下:

$ grep linux -n sample1.txt sample2.txt
sample1.txt:2:linux is fun
sample2.txt:2:planetlinux

打印模式匹配的字符或字节偏移如下:

$ echo gnu is not unix | grep -b -o "not"
7:not

行中字符串的字符偏移是从 0 开始的计数器。在上面的例子中,not位于第七个偏移位置(即not从行中的第七个字符开始(gnu is not unix)。

-b选项始终与-o一起使用。

要在许多文件中搜索并找出某个文本匹配的文件,请使用:

$ grep -l linux sample1.txt sample2.txt
sample1.txt
sample2.txt

-l参数的反义词是-L-L参数返回一个非匹配文件的列表。

还有更多...

我们已经使用了grep命令的基本用法示例。但是grep命令具有丰富的功能。让我们看看grep提供的不同选项。

递归搜索多个文件

要在许多后代目录中递归搜索文本,请使用:

$ grep "text" . -R -n

在此命令中,"."指定当前目录。

例如:

$ cd src_dir
$ grep "test_function()" . -R -n
./miscutils/test.c:16:test_function();

test_function()存在于miscutils/test.c的第 16 行。

注意

这是开发人员最常用的命令之一。它用于查找源代码文件中是否存在某个文本。

忽略模式的大小写

-i参数有助于匹配模式在不考虑字符是大写还是小写的情况下进行评估。例如:

$ echo hello world | grep -i "HELLO"
hello

通过匹配多个模式进行 grep

通常,我们可以指定单个模式进行匹配。但是,我们可以使用-e参数指定多个模式进行匹配,如下所示:

$ grep -e "pattern1" -e "pattern"

例如:

$ echo this is a line of text | grep -e "this" -e "line" -o
this
line

还有另一种指定多个模式的方法。我们可以使用一个模式文件来读取模式。按行编写模式以匹配并执行grep,并使用-f参数如下:

$ grep -f pattern_file source_filename

例如:

$ cat pat_file
hello
cool

$ echo hello this is cool | grep -f pat_file
hello this is cool

在 grep 搜索中包括和排除文件(通配符模式)

grep可以包括或排除要搜索的文件。我们可以使用通配符模式指定包含文件或排除文件。

要递归地仅在目录中搜索.c.cpp文件,并排除所有其他文件类型,请使用:

$ grep "main()" . -r  --include *.{c,cpp}

请注意,some{string1,string2,string3}扩展为somestring1 somestring2 somestring3

排除搜索中的所有 README 文件如下:

$ grep "main()" . -r –-exclude "README" 

要排除目录,请使用--exclude-dir选项。

要从文件中读取要排除的文件列表,请使用--exclude-from FILE

使用带有零字节后缀的 xargs 的 grep

xargs命令通常用于将文件名列表作为命令行参数提供给另一个命令。当文件名用作命令行参数时,建议使用零字节终止符而不是空格终止符。一些文件名可能包含空格字符,它将被误解为终止符,并且一个文件名可能会被分成两个文件名(例如,New file.txt可能被解释为两个文件名Newfile.txt)。通过使用零字节后缀可以避免这个问题。我们使用xargs来接受来自grepfind等命令的stdin文本。这些命令可以输出带有零字节后缀的文本到stdout。为了指定文件名的输入终止符是零字节(\0),我们应该在xargs中使用-0

创建一些测试文件如下:

$ echo "test" > file1
$ echo "cool" > file2
$ echo "test" > file3

在以下命令序列中,grep输出带有零字节终止符(\0)的文件名。通过使用grep-Z选项来指定。xargs -0读取输入并使用零字节终止符分隔文件名:

$ grep "test" file* -lZ | xargs -0 rm

通常,-Z-l一起使用。

grep 的静默输出

grep的先前提到的用法以不同的格式返回输出。有些情况下,我们需要知道文件是否包含指定的文本。我们必须执行一个返回 true 或 false 的测试条件。可以使用安静条件(-q)来执行。在安静模式下,grep命令不会将任何输出写入标准输出。相反,它运行命令并根据成功或失败返回退出状态。

我们知道,如果成功,命令返回 0,如果失败,则返回非零。

让我们通过一个脚本,以安静模式使用grep来测试文件中是否出现匹配文本。

#!/bin/bash 
#Filename: silent_grep.sh
#Description: Testing whether a file contain a text or not 

if [ $# -ne 2 ]; 
then
echo "$0 match_text filename"
fi

match_text=$1 
filename=$2 

grep -q $match_text $filename

if [ $? -eq 0 ];
then
echo "The text exists in the file"
else
echo "Text does not exist in the file"
fi

可以通过提供匹配单词(Student)和文件名(student_data.txt)作为命令参数来运行silent_grep.sh脚本:

$ ./silent_grep.sh Student student_data.txt 
The text exists in the file 

打印文本匹配之前和之后的行

基于上下文的打印是grep的一个很好的特性。假设找到了给定匹配文本的匹配行,grep通常只打印匹配行。但是我们可能需要在匹配行之后的“n”行或匹配行之前的“n”行或两者之间。可以使用grep中的上下文行控制来执行。让我们看看如何做到这一点。

为了在匹配后打印三行,请使用-A选项:

$ seq 10 | grep 5 -A 3
5
6
7
8

为了在匹配之前打印三行,请使用-B选项:

$ seq 10 | grep 5 -B 3
2
3
4
5

要在匹配项之后和之前打印三行,请使用-C选项如下:

$ seq 10 | grep 5 -C 3
2
3
4
5
6
7
8

如果有多个匹配项,每个部分由一行“--”分隔:

$ echo -e "a\nb\nc\na\nb\nc" | grep a -A 1
a
b
--
a
b

使用 cut 按列切割文件

我们可能需要按列而不是按行切割文本。假设我们有一个包含学生报告的文本文件,其中包含编号姓名成绩百分比等列。我们需要提取学生的姓名到另一个文件或文件中的任何第 n 列,或提取两列或更多列。本教程将说明如何执行此任务。

准备好了

cut是一个小型实用程序,通常用于按列切割。它还可以指定分隔每一列的分隔符。在cut术语中,每一列被称为一个字段。

如何做...

为了提取第一个字段或列,使用以下语法:

cut -f FIELD_LIST filename

FIELD_LIST是要显示的列的列表。该列表由逗号分隔的列号组成。例如:

$ cut -f 2,3 filename

这里显示了第二列和第三列。

cut还可以从stdin中读取输入文本。

制表符是字段或列的默认分隔符。如果找到没有分隔符的行,它们也会被打印。为了避免打印没有分隔符字符的行,请在cut后面加上-s选项。以下是使用cut命令进行列处理的示例:

$ cat student_data.txt 
No  Name     Mark   Percent
1   Sarath    45     90
2   Alex      49     98
3   Anu       45     90

$ cut -f1 student_data.txt
No 
1 
2 
3 

提取多个字段如下:

$ cut -f2,4 student_data.txt
Name     Percent
Sarath   90
Alex     98
Anu      90

要打印多个列,请将由逗号分隔的列号列表作为-f的参数。

我们还可以使用--complement选项来补充提取的字段。假设您有许多字段,想要打印除第三列之外的所有列,请使用:

$ cut -f3 –-complement student_data.txt
No  Name    Percent 
1   Sarath  90
2   Alex    98
3   Anu     90

要指定字段的分隔符字符,请使用-d选项如下:

$ cat delimited_data.txt
No;Name;Mark;Percent
1;Sarath;45;90
2;Alex;49;98
3;Anu;45;90

$ cut -f2 -d";" delimited_data.txt
Name
Sarath
Alex
Anu

还有更多...

cut 命令有更多选项来指定要显示为列的字符序列。让我们看看cut提供的其他选项。

指定字符或字节范围作为字段

假设我们不依赖分隔符,但需要提取字段,以便定义一系列字符(从 0 开始计算为行的开头)作为字段,这样的提取可以使用cut来实现。

让我们看看有哪些可能的符号:

N- 从第 N 个字节、字符或字段到行尾
N-M 从第 N 到第 M(包括)个字节、字符或字段
-M 从第一个到第 M 个(包括)字节、字符或字段

我们使用上述符号来指定字段作为字节或字符的范围,具有以下选项:

  • -b 用于字节

  • -c 用于字符

  • -f 用于定义字段

例如:

$ cat range_fields.txt
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxy

您可以按以下方式打印前五个字符:

$ cut -c1-5 range_fields.txt
abcde
abcde
abcde
abcde

前两个字符可以打印如下:

$ cut range_fields.txt -c-2
ab
ab
ab
ab

-c替换为-b以按字节计数。

在使用-c-f-b时,可以指定输出分隔符如下:

--output-delimiter "delimiter string"

使用-b-c提取多个字段时,--output-delimiter是必须的。否则,如果未提供它,您无法区分字段。例如:

$ cut range_fields.txt -c1-3,6-9 --output-delimiter ","
abc,fghi
abc,fghi
abc,fghi
abc,fghi

给定文件中使用的单词频率

查找文件中使用的单词频率是一个有趣的练习,可以应用文本处理技能。有很多不同的方法可以做到这一点。让我们看看如何做到这一点。

准备好了

我们可以使用关联数组、awk、sed、grep 等不同的方式来解决这个问题。

如何做...

单词是由空格和句点分隔的字母字符。首先,我们应该解析给定文件中的所有单词。因此,需要找出每个单词的计数。可以使用正则表达式和诸如 sed、awk 或 grep 之类的工具来解析单词。

要找出每个单词的计数,我们可以采用不同的方法。一种方法是循环遍历每个单词,然后使用另一个循环遍历单词并检查它们是否相等。如果它们相等,增加一个计数并在文件末尾打印它。这是一种低效的方法。在关联数组中,我们使用单词作为数组索引,计数作为数组值。我们只需要一个循环就可以通过循环遍历每个单词来实现这一点。array[word] = array[word] + 1,而最初它的值被设置为0。因此,我们可以得到一个包含每个单词计数的数组。

现在让我们来做吧。创建如下的 shell 脚本:

#!/bin/bash
#Name: word_freq.sh
#Description: Find out frequency of words in a file

if [ $# -ne 1 ];
then
echo "Usage: $0 filename";
exit -1
fi

filename=$1

egrep -o "\b[[:alpha:]]+\b" $filename | \

awk '{ count[$0]++ }
END{ printf("%-14s%s\n","Word","Count") ;
for(ind in count)
{  printf("%-14s%d\n",ind,count[ind]);  }

}'

一个示例输出如下:

$ ./word_freq.sh words.txt 
Word          Count 
used           1
this           2 
counting       1

它是如何工作的...

这里使用egrep -o "\b[[:alpha:]]+\b" $filename来仅输出单词。-o选项将打印由换行字符分隔的匹配字符序列。因此我们在每行收到单词。

\b是单词边界字符。[:alpha:]是字母的字符类。

awk命令用于避免对每个单词进行迭代。由于awk默认情况下执行{}块中的语句对每一行,我们不需要特定的循环来执行。因此,使用关联数组递增计数为count[$0]++。最后,在END{}块中,我们通过迭代单词来打印单词及其计数。

另请参阅

  • 数组和关联数组第一章,解释了 Bash 中的数组

  • 基本的 awk 入门,解释了 awk 命令

基本的 sed 入门

sed 代表流编辑器。这是文本处理的一个非常重要的工具。它是一个可以玩弄正则表达式的神奇实用程序。sed命令的一个众所周知的用法是文本替换。本教程将涵盖大多数经常使用的sed技术。

如何做…

sed可以用于在给定文本中用另一个字符串替换字符串的出现。它可以使用正则表达式进行匹配。

$ sed 's/pattern/replace_string/' file

或者

$ cat file | sed 's/pattern/replace_string/' file

这个命令从stdin中读取。

要将更改与替换保存到同一文件中,请使用-i 选项。大多数用户在进行替换后使用多个重定向来保存文件,如下所示:

$ sed 's/text/replace/' file > newfile
$ mv newfile file

然而,它可以在一行内完成,例如:

$ sed -i 's/text/replace/' file

先前看到的sed命令将替换每行中模式的第一个出现。但是为了替换每个出现,我们需要在末尾添加g参数,如下所示:

$ sed 's/pattern/replace_string/g' file

/g后缀表示它将替换每个出现。然而,有时我们不需要替换前 N 个出现,而只需要其余的。有一个内置选项可以忽略前 N 个出现并从第"N+1"次出现开始替换。

看看以下命令:

$ echo this thisthisthis | sed 's/this/THIS/2g' 
thisTHISTHISTHIS

$ echo this thisthisthis | sed 's/this/THIS/3g' 
thisthisTHISTHIS

$ echo this thisthisthis | sed 's/this/THIS/4g' 
thisthisthisTHIS

在需要从第 N 次出现开始替换时,放置/Ng

/sed中是一个分隔符字符。我们可以使用任何分隔符字符,如下所示:

sed 's:text:replace:g'
sed 's|text|replace|g'

当分隔符字符出现在模式内部时,我们必须使用\前缀进行转义,如下所示:

sed 's|te\|xt|replace|g'

\|是出现在替换中的分隔符。

还有更多...

sed命令具有许多用于文本处理的选项。通过将sed中可用的选项与逻辑序列结合,可以在一行中解决许多复杂的问题。让我们看看sed可用的一些不同选项。

删除空白行

使用sed删除空白行是一种简单的技术。空白可以与正则表达式^$匹配:

$ sed '/^$/d' file

/pattern/d将删除匹配模式的行。

对于空白行,行结束标记出现在行开始标记旁边。

匹配字符串符号(&)

sed中,我们可以使用&作为替换模式的匹配字符串,以便我们可以在替换字符串中使用匹配的字符串。

例如:

$ echo this is an example | sed 's/\w\+/[&]/g'
[this] [is] [an] [example]

这里的正则表达式\w\+匹配每个单词。然后我们用[&]替换它。&对应于匹配的单词。

子字符串匹配符号(\1)

&是一个字符串,对应于给定模式的匹配字符串。但我们也可以匹配给定模式的子字符串。让我们看看如何做到这一点。

$ echo this is digit 7 in a number | sed 's/digit \([0-9]\)/\1/'
this is 7 in a number

它用\(pattern\)替换digit 77。匹配的子字符串是7()中的模式用斜杠转义。对于第一个子字符串匹配,相应的表示是\1,对于第二个是\2,依此类推。看下面的多次匹配示例:

$ echo seven EIGHT | sed 's/\([a-z]\+\) \([A-Z]\+\)/\2 \1/'
EIGHT seven

([a-z]\+\)匹配第一个单词,\([A-Z]\+\)匹配第二个单词。\1\2用于引用它们。这种引用方式称为回溯引用。在替换部分,它们的顺序被改变为\2 \1,因此它们以相反的顺序出现。

多个表达式的组合

使用管道替换多个sed的组合如下:

sed 'expression' | sed 'expression'

相当于:

$ sed 'expression; expression'

引用

通常情况下,sed表达式使用单引号引起来。但也可以使用双引号。双引号通过评估来扩展表达式。当我们想在sed表达式中使用某个变量字符串时,使用双引号是有用的。

例如:

$ text=hello
$ echo hello world | sed "s/$text/HELLO/" 
HELLO world 

$text被评估为"hello"。

基本 awk 入门

awk是一种设计用于处理数据流的工具。它非常有趣,因为它可以操作列和行。它支持许多内置功能,如数组、函数等,就像 C 编程语言一样。灵活性是它的最大优势。

如何做…

awk脚本的结构如下:

awk ' BEGIN{  print "start" } pattern { commands } END{ print "end" } file

awk命令也可以从stdin读取。

awk脚本通常由三部分组成:BEGINEND和带有模式匹配选项的常用语句块。它们三者都是可选的,脚本中任何一个都可以不存在。脚本通常用单引号或双引号括起来,如下所示:

awk 'BEGIN { statements } { statements } END { end statements }'

或者,也可以使用:

awk "BEGIN { statements } { statements } END { end statements }"

例如:

$ awk 'BEGIN { i=0 } { i++ } END{ print i}' filename

或者:

$ awk "BEGIN { i=0 } { i++ } END{ print i }" filename

它是如何工作的…

awk命令的工作方式如下:

  1. 执行BEGIN { commands }块中的语句。

  2. 从文件或stdin读取一行,并执行pattern { commands }。重复此步骤,直到到达文件的末尾。

  3. 当到达输入流的末尾时,执行END { commands }块。

BEGIN块在awk开始从输入流中读取行之前执行。这是一个可选块。在BEGIN块中编写的常见语句包括变量初始化、为输出表打印输出标题等。

END块类似于BEGIN块。当awk完成从输入流中读取所有行时,END块会被执行。在END块中,打印所有行的计算值后进行分析或打印结论等语句是常用的语句(例如,在比较所有行后,打印文件中的最大数)。这是一个可选块。

最重要的块是带有模式块的常用命令。这个块也是可选的。如果没有提供这个块,默认情况下会执行{ print }以打印读取的每一行。这个块对awk读取的每一行都会执行。

它就像一个 while 循环,用提供的语句在循环体内读取行。

读取一行,检查提供的模式是否与该行匹配。模式可以是正则表达式匹配、条件、行范围匹配等。如果当前读取的行与模式匹配,它会执行{ }中的语句。

模式是可选的。如果不使用模式,所有行都会匹配,并且执行{ }内的语句。

让我们看下面的例子:

$ echo -e "line1\nline2" | awk 'BEGIN{ print "Start" } { print } END{ print "End" } '
Start
line1
line2
End

当使用print而没有参数时,它将打印当前行。关于print有两件重要的事情需要记住。当 print 的参数用逗号分隔时,它们将以空格分隔打印。双引号在awkprint上下文中用作连接运算符。

例如:

$ echo | awk '{ var1="v1"; var2="v2"; var3="v3"; \
print var1,var2,var3 ; }'

上述语句将打印变量的值如下:

v1 v2 v3

echo命令将一行写入标准输出。因此,awk{ }块中的语句将执行一次。如果awk的标准输入包含多行,则awk中的命令将执行多次。

连接可以如下使用:

$ echo | awk '{ var1="v1"; var2="v2"; var3="v3"; \
print var1"-"var2"-"var3 ; }'

输出将是:

v1-v2-v3

{ }就像循环中的块,遍历文件的每一行。

提示

通常,我们将初始变量赋值,比如var=0;和打印文件头的语句放在BEGIN块中。在END{}块中,我们放置打印结果等语句。

还有更多…

awk命令具有许多丰富的功能。为了掌握 awk 编程的艺术,您应该熟悉重要的awk选项和功能。让我们了解awk的基本功能。

特殊变量

可以使用的一些特殊变量与awk一起使用如下:

  • NR:它代表记录数,并对应于当前执行的行号。

  • NF:它代表字段数,并对应于当前执行行下的字段数(字段由空格分隔)。

  • $0:它是一个变量,包含当前执行行的文本内容。

  • $1:它是一个变量,保存第一个字段的文本。

  • $2:它是保存第二个字段文本的变量。

例如:

$ echo -e "line1 f2 f3\nline2 f4 f5\nline3 f6 f7" | \

awk '{
print "Line no:"NR",No of fields:"NF, "$0="$0, "$1="$1,"$2="$2,"$3="$3 
}' 
Line no:1,No of fields:3 $0=line1 f2 f3 $1=line1 $2=f2 $3=f3 
Line no:2,No of fields:3 $0=line2 f4 f5 $1=line2 $2=f4 $3=f5 
Line no:3,No of fields:3 $0=line3 f6 f7 $1=line3 $2=f6 $3=f7

我们可以将一行的最后一个字段打印为print $NF,倒数第二个字段为$(NF-1)等等。

awk提供了与 C 中相同语法的printf()函数。我们也可以使用它来代替 print。

让我们看一些基本的awk使用示例。

按如下方式打印每一行的第二个和第三个字段:

$awk '{ print $3,$2 }'  file

为了计算文件中的行数,使用以下命令:

$ awk 'END{ print NR }' file

这里我们只使用END块。NR将在进入每一行时由awk更新其行号。当它到达最后一行时,它将具有最后一行号的值。因此,在END块中,NR将具有最后一行号的值。

您可以将字段 1 的每一行的所有数字相加如下:

$ seq 5 | awk 'BEGIN{ sum=0; print "Summation:" } 
{ print $1"+"; sum+=$1 } END { print "=="; print sum }' 
Summation: 
1+ 
2+ 
3+ 
4+ 
5+ 
==
15

将变量值从外部传递给 awk

通过使用-v参数,我们可以将外部值(而不是来自stdin)传递给awk如下:

$ VAR=10000
$ echo | awk -v VARIABLE=$VAR'{ print VARIABLE }'
1

有一种灵活的替代方法可以从外部传递多个变量值给awk。例如:

$ var1="Variable1" ; var2="Variable2"
$ echo | awk '{ print v1,v2 }' v1=$var1 v2=$var2
Variable1 Variable2

当输入是通过文件而不是标准输入给出时,使用:

$ awk '{ print v1,v2 }' v1=$var1 v2=$var2 filename

在上述方法中,变量被指定为键值对,由空格分隔(v1=$var1 v2=$var2)作为命令参数传递给awk的 BEGIN、{ }和 END 块之后。

使用 getline 显式读取一行

通常,grep默认读取文件中的所有行。如果要读取特定行,可以使用getline函数。有时我们可能需要从BEGIN块中读取第一行。

语法是:getline var

变量var将包含该行的内容。

如果getline没有参数调用,我们可以使用$0$1$2来访问行的内容。

例如:

$ seq 5 | awk 'BEGIN { getline; print "Read ahead first line", $0 } { print $0 }'
Read ahead first line 1
2
3
4
5

使用过滤模式过滤由 awk 处理的行

我们可以为要处理的行指定一些条件。例如:

$ awk 'NR < 5' # Line number less than 5
$ awk 'NR==1,NR==4' #Line numbers from 1-5
$ awk '/linux/' # Lines containing the pattern linux (we can specify regex)
$ awk '!/linux/' # Lines not containing the pattern linux

设置字段的分隔符

默认情况下,字段的分隔符是空格。我们可以使用-F "delimiter"来明确指定分隔符:

$ awk -F: '{ print $NF }' /etc/passwd

或:

awk 'BEGIN { FS=":" } { print $NF }' /etc/passwd

我们可以通过在BEGIN块中设置OFS="delimiter"来设置输出字段分隔符。

从 awk 读取命令输出

在以下代码中,echo将产生一行空行。cmdout变量将包含grep root /etc/passwd命令的输出,并打印包含root的行:

在变量'output'中读取'command'的语法如下:

"command" | getline output ;

例如:

$ echo | awk '{ "grep root /etc/passwd" | getline cmdout ; print cmdout }'
root:x:0:0:root:/root:/bin/bash

通过使用getline,我们可以将外部 shell 命令的输出读入名为cmdout的变量中。

awk支持关联数组,可以使用文本作为索引。

在 awk 中使用循环

awk中有一个for循环。它的格式是:

for(i=0;i<10;i++) { print $i ; }

或者:

for(i in array) { print array[i]; }

awk带有许多内置的字符串操作函数。让我们来看看其中的一些:

  • length(string): 它返回字符串的长度。

  • index(string, search_string): 它返回search_string在字符串中的位置。

  • split(string, array, delimiter): 它将使用分隔符生成的字符串列表存储在数组中。

  • substr(string, start-position, end-position): 它返回通过使用起始和结束字符偏移创建的子字符串。

  • sub(regex, replacement_str, string): 它用replacment_str替换字符串中第一次出现的正则表达式匹配。

  • gsub(regex, replacment_str, string): 它类似于sub()。但它替换每一个正则表达式匹配。

  • match(regex, string): 它返回正则表达式(regex)是否在字符串中找到匹配的结果。如果找到匹配,则返回非零,否则返回零。match()关联有两个特殊变量。它们是RSTARTRLENGTHRSTART变量包含正则表达式匹配开始的位置。RLENGTH变量包含正则表达式匹配的字符串的长度。

从文本或文件中替换字符串

字符串替换是一个经常使用的文本处理任务。通过匹配所需的文本,可以很容易地使用正则表达式来完成。

准备就绪

当我们听到“替换”这个术语时,每个系统管理员都会想起 sed。 sed是 UNIX-like 系统下进行文本或文件替换的通用工具。让我们看看如何做到这一点。

如何做...

sed入门配方包含了大部分sed的用法。您可以按以下方式替换字符串或模式:

$ sed 's/PATTERN/replace_text/g' filename

或者:

$ stdin | sed 's/PATTERN/replace_text/g'

我们也可以使用双引号(")而不是单引号(')。当使用双引号(")时,我们可以在sed模式和替换字符串中指定变量。例如:

$ p=pattern
$ r=replaced
$ echo "line containing apattern" | sed "s/$p/$r/g" 
line containing a replaced

我们也可以在sed中不使用g

$ sed 's/PATTEN/replace_text/' filename

然后它将只替换PATTERN第一次出现的情况。/g代表全局。这意味着它将替换文件中PATTERN的每一个出现。

还有更多...

我们已经看到了使用sed进行基本文本替换。让我们看看如何将替换后的文本保存在源文件中。

将替换保存在文件中

当将文件名传递给sed时,它的输出将可用于stdout。为了将更改保存在文件中,而不是将输出流发送到stdout,请使用以下-i选项:

$ sed 's/PATTERN/replacement/' -i filename

例如,用以下方法在文件中替换所有三位数为另一个指定的数字:

$ cat sed_data.txt
11 abc 111 this 9 file contains 111 11 88 numbers 0000

$ cat sed_data.txt  | sed 's/\b[0-9]\{3\}\b/NUMBER/g'
11 abc NUMBER this 9 file contains NUMBER 11 88 numbers 0000

上面的一行只替换三位数。\b[0-9]\{3\}\b是用于匹配三位数的正则表达式。[0-9]是数字的范围,即从 0 到 9。{3}用于匹配前面的字符三次。\\{3\}中用于给{}赋予特殊含义。\b是单词边界标记。

参见

  • 基本的 sed 入门,解释了 sed 命令

压缩或解压 JavaScript

JavaScript 在设计网站时被广泛使用。在编写 JavaScript 代码时,我们使用多个空格、注释和制表符来提高代码的可读性和维护性。但是在 JavaScript 中使用大量空格和制表符会导致文件大小增加。随着文件大小的增加,页面加载时间也会增加。因此,大多数专业网站都使用压缩的 JavaScript 来实现快速加载。压缩主要是挤压空格和换行字符。一旦 JavaScript 被压缩,可以通过添加足够的空格和换行字符来解压缩,从而使其可读。通常,混淆的代码也可以通过插入空格和换行符来实现可读。这个方法是在 shell 中尝试窃取类似功能的一种尝试。

准备工作

我们将编写一个 JavaScript 压缩器或混淆工具。也可以设计一个解压工具。我们将使用文本和字符替换工具trsed。让我们看看如何做到这一点。

如何做...

让我们按照逻辑顺序和所需的代码来压缩和解压 JavaScript。

$ cat sample.js
functionsign_out()
{ 

$("#loading").show(); 
$.get("log_in",{logout:"True"},

function(){ 

window.location="";

}); 

}

我们需要执行以下任务来压缩 JavaScript:

  1. 删除换行符和制表符。

  2. 挤压空格。

  3. 替换注释/内容/。

  4. 用替换替换以下内容:

  • 将"{ "替换为"{"

  • " }"替换为"}"

  • " ("替换为"("

  • ") "替换为")"

  • ", "替换为","

  • " ; "替换为";"(我们需要删除所有额外的空格)

要解压缩或使 JavaScript 更易读,我们可以使用以下任务:

  1. 将";"替换为";\n"。

  2. 将"{"替换为"{\n",将"}"替换为"\n}"。

它是如何工作的...

让我们通过执行以下任务来压缩 JavaScript:

  1. 删除'\n'和'\t'字符:
tr -d '\n\t' 
  1. 删除额外的空格:
tr -s ' ' or sed 's/[ ]\+/ /g'
  1. 删除注释:
sed 's:/\*.*\*/::g'
  • :被用作 sed 分隔符,以避免需要转义/,因为我们需要使用/**/

  • 在 sed 中,*被转义为\*

  • .*用于匹配/**/之间的所有文本

  1. 删除所有在{}();:和逗号之前和之后的空格。
sed 's/ \?\([{}();,:]\) \?/\1/g'

上述sed语句可以解析如下:

  • / \?\([{}();,:]\) \?/sed代码中是匹配部分,/\1 /g是替换部分。

  • \([{}();,:]\)用于匹配集合[ { }( ) ; , : ]中的任意一个字符(为了可读性插入了空格)。\(\)是用于在替换部分中记忆匹配和回溯引用的组操作符。()被转义以赋予它们作为组操作符的特殊含义。\?在组操作符之前和之后。它是为了匹配可能在集合中的任何字符之前或之后的空格字符。

  • 在替换部分,匹配字符串(即:一个空格(可选)、来自集合的字符,再次是可选空格)被替换为匹配的字符。它使用了一个回溯引用来匹配和记忆使用组操作符()的字符。通过使用\1符号,回溯引用的字符指的是组匹配。

使用管道结合上述任务如下:

$ catsample.js |  \
tr -d '\n\t' |  tr -s ' ' \
| sed 's:/\*.*\*/::g' \
| sed 's/ \?\([{}();,:]\) \?/\1/g' 

输出如下:

functionsign_out(){$("#loading").show();$.get("log_in",{logout:"True"},function(){window.location="";});}

让我们编写一个解压缩脚本,使混淆的代码可读,如下所示:

$ cat obfuscated.txt | sed 's/;/;\n/g; s/{/{\n\n/g; s/}/\n\n}/g' 

或:

$ cat obfuscated.txt | sed 's/;/;\n/g' | sed 's/{/{\n\n/g' | sed 's/}/\n\n}/g'

在上一个命令中:

  • s/;/;\n/g;替换为\n;

  • s/{/{\n\n/g{替换为{\n\n

  • s/}/\n\n}/g}替换为\n\n}

另请参阅

  • 使用 tr 进行翻译 第二章 ,解释了 tr 命令

  • 基本 sed 入门,解释了 sed 命令

在文件中迭代行、单词和字符

在编写不同的文本处理和文件操作脚本时,经常需要对文件中的字符、单词和行进行迭代。尽管这很简单,但我们会犯一些错误,而且没有得到预期的输出。这个方法将帮助你学会如何做到这一点。

准备工作

使用简单循环和从stdin或文件重定向进行迭代是执行上述任务的基本组件。

如何做...

在这个配方中,我们讨论了执行遍历行、单词和字符的三个任务。让我们看看如何执行这些任务中的每一个。

  1. 遍历文件中的每一行:

我们可以使用while循环从标准输入中读取。因此,它将在每次迭代中读取一行。

使用文件重定向到stdin如下:

while read line;
do
echo $line;
done < file.txt

如下使用子 shell:

cat file.txt | (  while read line; do echo $line; done )

这里cat file.txt可以替换为任何命令序列的输出。

  1. 遍历每个单词中的每个单词

我们可以使用while循环来遍历行中的单词,如下所示:

for word in $line;
do
echo $word;
done

  1. 遍历单词中的每个字符

我们可以使用for循环来迭代变量i0到字符串的长度。在每次迭代中,可以使用特殊符号${string:start_position:No_of_characters}从字符串中提取一个字符。

for((i=0;i<${#word};i++))
do
echo ${word:i:1} ;
done

工作原理…

读取文件的行和读取行中的单词是直接的方法。但是读取单词的字符有点技巧。我们使用子字符串提取技术。

${word:start_position:no_of_characters}返回变量word中字符串的子字符串。

${#word}返回变量word的长度。

另请参阅

  • 字段分隔符和迭代器 第一章,解释 Bash 中的不同循环。

  • 文本切片和参数操作,解释从字符串中提取字符。

合并多个文件作为列

有不同的情况需要我们在列中连接文件。我们可能需要使每个文件的内容出现在单独的列中。通常,cat命令以行或行的方式连接。

如何做…

paste是可用于按列连接的命令。paste命令可使用以下语法:

$ paste file1 file2 file3 …

让我们尝试以下示例:

$ cat paste1.txt
1
2
3
4
5
$ cat paste2.txt
slynux
gnu
bash
hack
$ paste paste1.txt paste2.txt
1slynux
2gnu
3bash
4hack
5

默认分隔符是制表符。我们也可以使用-d显式指定分隔符。例如:

$ paste paste1.txt paste2.txt -d ","
1,slynux
2,gnu
3,bash
4,hack
5,

另请参阅

  • 使用 cut 按列切割文件,解释从文本文件中提取数据

在文件或行中打印第 n 个单词或列

我们可能会得到一个具有许多列的文件,实际上只有少数列是有用的。为了仅打印相关列或字段,我们对其进行过滤。

准备工作

最广泛使用的方法是使用awk来执行此任务。也可以使用cut来完成。

如何做…

要打印第五列,请使用以下命令:

$ awk '{ print $5 }' filename

我们还可以打印多个列,并且可以在列之间插入自定义字符串。

例如,要打印当前目录中每个文件的权限和文件名,请使用:

$ ls -l | awk '{ print $1" :  " $8 }'
-rw-r--r-- :  delimited_data.txt
-rw-r--r-- :  obfuscated.txt
-rw-r--r-- :  paste1.txt
-rw-r--r-- :  paste2.txt

另请参阅

  • 基本 awk 入门,解释 awk 命令

  • 使用 cut 按列切割文件,解释从文本文件中提取数据

打印行号或模式之间的文本

我们可能需要根据条件打印文本行的某些部分,例如行号范围,开始和结束模式匹配的范围等。让我们看看如何做到这一点。

准备工作

我们可以使用诸如 awk、grep 和 sed 之类的实用程序根据条件执行部分打印。但我发现awk是最容易理解的。让我们使用awk来做。

如何做…

为了打印文本行的行号范围,从 M 到 N,使用以下语法:

$ awk 'NR==M, NR==N' filename

或者,它可以接受stdin输入如下:

$ cat filename | awk 'NR==M, NR==N'

MN替换为以下数字:

$ seq 100 | awk 'NR==4,NR==6'
4
5
6

要打印文本部分的行,使用以下语法:start_patternend_pattern

$ awk '/start_pattern/, /end _pattern/' filename

例如:

$ cat section.txt 
line with pattern1 
line with pattern2 
line with pattern3 
line end with pattern4 
line with pattern5 

$ awk '/pa.*3/, /end/' section.txt 
line with pattern3 
line end with pattern4

awk中使用的模式是正则表达式。

另请参阅

  • 基本 awk 入门,解释 awk 命令

使用脚本检查回文字符串

检查字符串是否回文是 C 编程课程中的第一个实验。但是,在这里,我们包含了这个配方,以便让您了解如何解决类似的问题,其中模式匹配可以扩展为以前出现的模式在文本中重复。

准备工作

sed命令具有记住先前匹配的子模式的能力。这被称为反向引用。我们可以通过使用反向引用来解决回文问题。我们可以在 Bash 中使用多种方法来解决这个问题。

如何做...

sed可以记住先前匹配的正则表达式模式,因此我们可以确定字符串中是否存在字符的重复。这种记住和引用先前匹配模式的能力称为反向引用。

让我们看看如何以更简单的方式应用反向引用来解决问题。例如:

$ sed -n '/\(.\)\1/p' filename

\(.\)对应于记住( )内的一个子字符串。这里是 . (句号),它也是sed的单个字符通配符。

\1对应于()内的第一个匹配的记忆。\2对应于第二个匹配。因此,我们可以记住许多被()包围的块。()显示为\( \),以赋予()特殊含义,而不仅仅是一个字符。

前面的sed语句将打印任何匹配两个完全相同的模式。

所有回文单词的结构如下:

  • 偶数个字符和一个字符序列,与其反向序列连接

  • 奇数个字符,带有字符序列,与相同字符的反向连接,但在第一个序列和其反向之间有一个公共字符

因此,为了匹配两者,我们可以在写正则表达式时在中间保留一个可选字符。

匹配三个字母回文单词的sed正则表达式将如下所示:

'/\(.\).\1/p'

我们可以在字符序列和其反向序列之间放置一个额外的字符(.)。

让我们编写一个可以匹配任意长度的回文字符串的脚本,如下所示:

#!/bin/bash
#Filename: match_palindrome.sh
#Description: Find out palindrome strings from a given file

if [ $# -ne 2 ];
then
echo "Usage: $0 filename string_length"
exit -1
fi

filename=$1 ;

basepattern='/^\(.\)'

count=$(( $2 / 2 ))

for((i=1;i<$count;i++))
do
basepattern=$basepattern'\(.\)' ;
done

if [ $(( $2 % 2 )) -ne 0 ];
then
basepattern=$basepattern'.' ;
fi

for((count;count>0;count--))
do
basepattern=$basepattern'\'"$count" ;
done

basepattern=$basepattern'$/p'
sed -n "$basepattern" $filename

使用字典文件作为输入文件,以获取给定字符串长度的回文单词列表。例如:

$ ./match_palindrome.sh /usr/share/dict/british-english 4
noon
peep
poop
sees

它是如何工作的...

上述脚本的工作很简单。大部分工作是为正则表达式和反向引用字符串生成sed脚本。

让我们通过一些示例来看看它的工作原理。

  • 如果要匹配字符并进行反向引用,我们使用\(.\)来匹配一个字符,\1来引用它。因此,为了匹配并打印两个字母的回文,我们使用:
sed '/\(.\)\1/p'

现在,为了指定从行的开头匹配字符串,我们添加行开始标记^,这样它将变成sed'/^\(.\)\1/p'/p用于打印匹配。

  • 如果我们想匹配四个字符的回文,我们使用:
sed '/^\(.\)\(.\)\2\1/p'

我们使用了两个\(.\)来匹配两个字符并记住它们。任何在\(\)之间的内容都将被sed记住并可以被反向引用。\2\1用于以匹配字符的相反顺序进行反向引用。

在上面的脚本中,我们有一个名为basepattern的变量,其中包含sed脚本。

该模式是根据回文字符串中的字符数使用for循环生成的。

最初,basepattern被初始化为basepattern='/^\(.\)',它对应于一个字符匹配。使用for循环将\(.\)basepattern连接起来,连接次数为回文字符串长度的一半。再次使用for循环以与回文字符串长度的一半相同的次数连接反向引用。最后,为了支持奇数长度的回文字符串,在匹配正则表达式和反向引用之间加入了一个可选字符(.)。

因此,sed回文匹配模式是精心制作的。这个精心制作的字符串用于从字典文件中找出回文字符串。

在上面的脚本中,我们使用了for循环生成sed模式。实际上没有必要单独生成模式。sed命令有自己的循环实现,使用标签和 goto。sed是一种广泛的语言。可以使用复杂的sed脚本在一行中进行回文检查。很难从头开始解释它。只需尝试以下脚本:

$ word="malayalam"
$ echo $word | sed ':loop ; s/^\(.\)\(.*\)\1/\2/; t loop; /^.\?$/{ s/.*/PALINDROME/ ; q; };  s/.*/NOT PALINDROME/ '
PALINDROME

如果您对使用sed进行深入脚本编写感兴趣,请参考完整的sedawk参考书:sed & awk,作者 Dale Dougherty 和 Arnold Robbins 的第二版。

尝试解析上面的一行sed脚本以测试回文是否使用该书。

还有更多...

现在让我们看看其他选项,或者可能与此任务相关的一些一般信息片段。

最简单直接的方法

检查字符串是否是回文的最简单方法是使用 rev 命令。

rev命令接受文件或stdin作为输入,并打印每行的反转字符串。

让我们来做一下:

string="malayalam"
if [[ "$string" == "$(echo $string | rev )" ]];
then
echo "Palindrome"
else
echo "Not palindrome"
fi

rev命令可以与其他命令一起使用来解决不同的问题。让我们看一个有趣的例子,将句子中的单词反转:

sentence='this is line from sentence'
echo $sentence | rev | tr ' ' '\n' | tac | tr '\n' ' ' | rev

输出如下:

sentence from line is this

在上面的一行代码中,首先使用rev命令反转字符。然后通过使用tr命令将空格替换为\n字符,将单词分隔成每行一个单词。现在使用tac命令按顺序反转行。再次使用tr将行合并为一行。现在再次应用rev,使得带有单词的行以相反的顺序排列。

另请参阅

  • 基本的 sed 入门,解释了 sed 命令

  • 比较和测试 第一章的[比较和测试],解释了字符串比较运算符

以相反的顺序打印行

这是一个简单的方法。它可能看起来并不是很有用,但它可以用来模拟 Bash 中的堆栈数据结构。这是一些有趣的东西。让我们以相反的顺序打印文件中的文本行。

准备工作

使用awk的小技巧可以完成任务。但是,也有一个直接的命令tac可以做同样的事情。taccat的反向。

如何做...

首先让我们用tac来做。语法如下:

tac file1 file2 …

它也可以按以下方式从stdin读取:

$ seq 5 | tac
5 
4 
3 
2 
1

tac中,\n是行分隔符。但我们也可以使用-s "separator"选项指定自己的分隔符。

让我们用awk来做:

$ seq 9 | \
awk '{ lifo[NR]=$0; lno=NR } 
END{ for(;lno>-1;lno--){ print lifo[lno]; } 
}'

在 shell 脚本中,\用于方便地将单行命令序列分解为多行。

它是如何工作的...

awk脚本非常简单。我们将每行存储到一个关联数组中,行号作为数组索引(NR 给出行号)。最后,awk执行END块。为了获得最后一行行号,lno=NR在{ }块中使用。因此,它从最后一行号迭代到0,并以相反的顺序打印数组中存储的行。

另请参阅

  • 使用 awk 实现 head、tail 和 tac,解释了使用 awk 编写 tac

从文本中解析电子邮件地址和 URL

从给定文件中解析所需的文本是我们在文本处理中经常遇到的常见任务。通过正确的正则表达式序列可以找到诸如电子邮件、URL 等项目。大多数情况下,我们需要从由许多不需要的字符和单词组成的电子邮件客户端的联系人列表或 HTML 网页中解析电子邮件地址。

准备工作

这个问题可以用 egrep 工具解决。

如何做...

匹配电子邮件地址的正则表达式模式是:

egrep 正则表达式:[A-Za-z0-9.]+@[A-Za-z0-9.]+\.[a-zA-Z]{2,4}

例如:

$ cat url_email.txt 
this is a line of text contains,<email> #slynux@slynux.com. </email> and email address, blog "http://www.google.com", test@yahoo.com dfdfdfdddfdf;cool.hacks@gmail.com<br />
<ahref="http://code.google.com"><h1>Heading</h1>

$ egrep -o '[A-Za-z0-9.]+@[A-Za-z0-9.]+\.[a-zA-Z]{2,4}'  url_email.txt
slynux@slynux.com 
test@yahoo.com 
cool.hacks@gmail.com

用于 HTTP URL 的egrep regex模式是:

http://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,4}

例如:

$ egrep -o "http://[a-zA-Z0-9.]+\.[a-zA-Z]{2,3}" url_email.txt
http://www.google.com 
http://code.google.com

它是如何工作的...

设计正则表达式真的很容易。在电子邮件正则表达式中,我们都知道电子邮件地址的形式是name@domain.some_2-4_letter。在这里,相同的内容以正则表达式语言写成如下:

[A-Za-z0-9.]+@[A-Za-z0-9.]+\.[a-zA-Z]{2,4}

[A-Za-z0-9.]+表示在[]块中的一些字符组合应该在字面@字符出现之前出现一次或多次(这是+的含义)。然后[A-Za-z0-9.]也应该出现一次或多次(+)。模式\.表示应该出现一个字面上的句号,最后一部分应该是长度为 2 到 4 个字母字符。

HTTP URL 的情况类似于电子邮件地址,但没有电子邮件正则表达式的name@匹配部分。

http://[a-zA-Z0-9.]+\.[a-zA-Z]{2,3}

另请参阅

  • 基本 sed 入门,解释了 sed 命令

  • 基本正则表达式入门,解释了如何使用正则表达式

在文件中打印模式之前或之后的 n 行

通过模式匹配打印文本部分在文本处理中经常使用。有时,我们可能需要在文本中出现模式之前或之后的文本行。例如,考虑一个包含电影演员评分的文件,其中每行对应于电影演员的详细信息,我们需要找出演员的评分以及评分最接近他们的演员的详细信息。让我们看看如何做到这一点。

准备工作

grep是在文件中搜索和查找文本的最佳工具。通常,grep打印给定模式的匹配行或匹配文本。但是grep中的上下文行控制选项使其能够打印模式匹配行周围的行之前,之后和之前后。

如何做...

这种技术可以通过电影演员列表更好地解释。例如:

$ cat actress_rankings.txt | head -n 20
1 Keira Knightley
2 Natalie Portman 
3 Monica Bellucci
4 Bonnie Hunt 
5 Cameron Diaz 
6 Annie Potts 
7 Liv Tyler 
8 Julie Andrews 
9 Lindsay Lohan
10 Catherine Zeta-Jones 
11 CateBlanchett
12 Sarah Michelle Gellar 
13 Carrie Fisher 
14 Shannon Elizabeth 
15 Julia Roberts 
16 Sally Field 
17 TéaLeoni
18 Kirsten Dunst
19 Rene Russo 
20 JadaPinkett

为了打印匹配“卡梅隆·迪亚兹”之后的三行文本以及匹配行,请使用以下命令:

$ grep -A 3 "Cameron Diaz" actress_rankings.txt
5 Cameron Diaz
6 Annie Potts
7 Liv Tyler
8 Julie Andrews

为了打印匹配的行和前面的三行,请使用以下命令:

$ grep -B 3 "Cameron Diaz" actress_rankings.txt 
2 Natalie Portman 
3 Monica Bellucci
4 Bonnie Hunt 
5 Cameron Diaz

打印匹配的行以及匹配行之前和之后的两行如下:

$ grep -C 2 "Cameron Diaz" actress_rankings.txt 
3 Monica Bellucci
4 Bonnie Hunt 
5 Cameron Diaz
6 Annie Potts 
7 Liv Tyler

你想知道我从哪里得到这个排名吗?

我使用基本的 sed,awk 和 grep 命令解析了一个充满图像和 HTML 内容的网站。请参阅章节:纷乱的网络?一点也不。

另请参阅

  • 使用 grep 在文件中搜索和挖掘“文本”解释了 grep 命令。

从包含单词的文件中删除句子

当确定了正确的正则表达式时,删除包含单词的句子是一个简单的任务。这只是解决类似问题的练习。

准备工作

sed是进行替换的最佳实用工具。因此让我们使用sed将匹配的句子替换为空白。

如何做...

让我们创建一个带有一些文本的文件进行替换。例如:

$ cat sentence.txt 
Linux refers to the family of Unix-like computer operating systems that use the Linux kernel. Linux can be installed on a wide variety of computer hardware, ranging from mobile phones, tablet computers and video game consoles, to mainframes and supercomputers. Linux is predominantly known for its use in servers. It has a server market share ranging between 20–40%. Most desktop computers run either Microsoft Windows or Mac OS X, with Linux having anywhere from a low of an estimated 1–2% of the desktop market to a high of an estimated 4.8%. However, desktop use of Linux has become increasingly popular in recent years, partly owing to the popular Ubuntu, Fedora, Mint, and openSUSE distributions and the emergence of netbooks and smart phones running an embedded Linux.

我们将删除包含“移动电话”一词的句子。使用以下sed表达式执行此任务:

$ sed 's/ [^.]*mobile phones[^.]*\.//g' sentence.txt
Linux refers to the family of Unix-like computer operating systems that use the Linux kernel. Linux is predominantly known for its use in servers. It has a server market share ranging between 20–40%. Most desktop computers run either Microsoft Windows or Mac OS X, with Linux having anywhere from a low of an estimated 1–2% of the desktop market to a high of an estimated 4.8%. However, desktop use of Linux has become increasingly popular in recent years, partly owing to the popular Ubuntu, Fedora, Mint, and openSUSE distributions and the emergence of netbooks and smart phones running an embedded Linux.

它是如何工作的...

让我们评估sed正则表达式's/ [^.]*mobile phones[^.]*\.//g'

它的格式是s/替换模式/替换字符串/g

它用替换字符串替换每个替换模式的出现。

这里替换模式是句子的正则表达式。每个句子由“。”分隔,第一个字符是空格。因此,我们需要匹配格式为“空格”一些文本 MATCH_STRING 一些文本“点”的文本。句子可以包含除“点”之外的任何字符,这是分隔符。因此,我们使用了[.]。[.]*匹配除点之外的任何字符的组合。在文本匹配字符串“移动电话”之间放置。每个匹配句子都被替换为//(无)。

另请参阅

  • 基本 sed 入门,解释了 sed 命令

  • 基本正则表达式入门,解释了如何使用正则表达式

使用 awk 实现 head,tail 和 tac

掌握文本处理操作需要实践。这个配方将帮助我们练习结合我们刚刚学到的一些命令和我们已经知道的一些命令。

准备工作

命令headtailuniqtac逐行操作。每当我们需要逐行处理时,我们总是可以使用awk。让我们用awk模拟这些命令。

如何做...

让我们看看如何用不同的基本文本处理命令来模拟不同的命令,比如 head、tail 和 tac。

head命令读取文件的前十行并将它们打印出来:

$ awk 'NR <=10' filename

tail命令打印文件的最后十行:

$ awk '{ buffer[NR % 10] = $0; } END { for(i=1;i<11;i++) { print buffer[i%10] } }' filename

tac命令以相反的顺序打印输入文件的行:

$ awk '{ buffer[NR] = $0; } END { for(i=NR; i>0; i--) { print buffer[i] } }' filename

它是如何工作的...

在使用awk实现head时,我们打印输入流中行号小于或等于10的行。行号可以使用特殊变量NR获得。

tail命令的实现中使用了一种哈希技术。缓冲区数组索引由哈希函数NR % 10确定,其中NR是包含当前执行的 Linux 编号的变量。$0是文本变量中的行。因此%将哈希函数中具有相同余数的所有行映射到数组的特定索引。在END{}块中,它可以遍历数组的十个索引值并打印缓冲区中存储的行。

tac命令的模拟中,它简单地将所有行存储在一个数组中。当它出现在END{}块中时,NR将保存最后一行的行号。然后它在for循环中递减,直到达到1,然后打印每个迭代语句中存储的行。

另请参阅

  • 基本 awk 入门,解释了 awk 命令

  • head 和 tail - 打印最后或前 10 行 of 第三章,解释了 head 和 tail 命令

  • 排序、唯一和重复 of 第二章, 解释了 uniq 命令

  • 以相反的顺序打印行,解释了 tac 命令

文本切片和参数操作

这个教程介绍了 Bash 中一些简单的文本替换技术和参数扩展简写。一些简单的技巧通常可以帮助我们避免编写多行代码。

如何做...

让我们开始任务吧。

从变量中替换一些文本可以这样做:

$ var="This is a line of text"
$ echo ${var/line/REPLACED}
This is a REPLACED of text"

line被替换为REPLACED

我们可以通过指定起始位置和字符串长度来生成子字符串,使用以下语法:

${variable_name:start_position:length}

要从第五个字符开始打印,请使用以下命令:

$ string=abcdefghijklmnopqrstuvwxyz
$ echo ${string:4}
efghijklmnopqrstuvwxyz

要从第五个字符开始打印八个字符,请使用:

$ echo ${string:4:8}
efghijkl

索引是通过将起始字母计为0来指定的。我们也可以指定从最后一个字母开始计数为-1。但它是在括号内使用的。(-1)是最后一个字母的索引。

echo ${string:(-1)}
z
$ echo ${string:(-2):2}
yz

另请参阅

  • 在文件中迭代行、单词和字符,解释了从单词中切片一个字符

第五章:纠缠的网络?一点也不!

在本章中,我们将涵盖:

  • 从网页下载

  • 将网页下载为格式化的纯文本

  • cURL 入门

  • 从命令行访问未读的 Gmail 邮件

  • 从网站解析数据

  • 创建图像爬虫和下载器

  • 创建网页相册生成器

  • 构建 Twitter 命令行客户端

  • 定义具有 Web 后端的实用程序

  • 在网站中查找损坏的链接

  • 跟踪网站的更改

  • 发布到网页并读取响应

介绍

网络正在成为技术的面孔。这是数据处理的中心访问点。虽然 shell 脚本不能像 PHP 等语言在 Web 上做的那样,但仍然有许多任务适合使用 shell 脚本。在本章中,我们将探讨一些可以用于解析网站内容、下载和获取数据、发送数据到表单以及自动执行网站使用任务和类似活动的方法。我们可以用几行脚本自动执行许多我们通过浏览器交互执行的活动。通过命令行实用程序访问 HTTP 协议提供的功能,我们可以编写适合解决大多数 Web 自动化实用程序的脚本。在阅读本章的食谱时玩得开心。

从网页下载

从给定 URL 下载文件或网页很简单。有几个命令行下载实用程序可用于执行此任务。

准备就绪

wget是一个文件下载命令行实用程序。它非常灵活,可以配置许多选项。

如何做...

可以使用wget下载网页或远程文件,如下所示:

$ wget URL

例如:

$ wget http://slynux.org 
--2010-08-01 07:51:20--  http://slynux.org/ 
Resolving slynux.org... 174.37.207.60 
Connecting to slynux.org|174.37.207.60|:80... connected. 
HTTP request sent, awaiting response... 200 OK 
Length: 15280 (15K) [text/html] 
Saving to: "index.html" 

100%[======================================>] 15,280      75.3K/s   in 0.2s 

2010-08-01 07:51:21 (75.3 KB/s) - "index.html" saved [15280/15280]

也可以指定多个下载 URL,如下所示:

$ wget URL1 URL2 URL3 ..

可以使用 URL 下载文件,如下所示:

$ wget ftp://example_domain.com/somefile.img

通常,文件以与 URL 相同的文件名下载,并且下载日志信息或进度被写入stdout

您可以使用-O选项指定输出文件名。如果指定的文件名已经存在,它将首先被截断,然后下载的文件将被写入指定的文件。

您还可以使用-o选项指定不同的日志文件路径,而不是将日志打印到stdout,如下所示:

$ wget ftp://example_domain.com/somefile.img -O dloaded_file.img -o log

通过使用上述命令,屏幕上将不会打印任何内容。日志或进度将被写入log,输出文件将是dloaded_file.img

由于不稳定的互联网连接可能会导致下载中断,因此我们可以使用尝试次数作为参数,以便一旦中断,实用程序将在放弃之前重试下载那么多次。

为了指定尝试次数,请使用-t标志,如下所示:

$ wget -t 5 URL

还有更多...

wget实用程序有几个额外的选项,可以在不同的问题领域下使用。让我们来看看其中的一些。

限速下载

当我们有限的互联网下行带宽和许多应用程序共享互联网连接时,如果给定一个大文件进行下载,它将吸取所有带宽,可能导致其他进程因带宽不足而挨饿。wget命令带有一个内置选项,可以指定下载作业可以拥有的最大带宽限制。因此,所有应用程序可以同时平稳运行。

我们可以使用--limit-rate参数限制wget的速度,如下所示:

$ wget  --limit-rate 20k http://example.com/file.iso

在这个命令中,k(千字节)和m(兆字节)指定了速度限制。

我们还可以指定下载的最大配额。当配额超过时,它将停止。在下载多个受总下载大小限制的文件时很有用。这对于防止下载意外使用太多磁盘空间很有用。

使用--quota-Q如下:

$ wget -Q 100m http://example.com/file1 http://example.com/file2

恢复下载并继续

如果使用wget下载在完成之前中断,我们可以使用-c选项恢复下载,从我们离开的地方继续下载,如下所示:

$ wget -c URL

使用 cURL 进行下载

cURL 是另一个高级命令行实用程序。它比 wget 更强大。

cURL 可以用于下载如下:

$ curl http://slynux.org > index.html

与 wget 不同,curl 将下载的数据写入标准输出(stdout)而不是文件。因此,我们必须使用重定向运算符将数据从 stdout 重定向到文件。

复制完整的网站(镜像)

wget 有一个选项,可以通过递归收集网页中的所有 URL 链接并像爬虫一样下载所有网页,从而下载完整的网站。因此,我们可以完全下载网站的所有页面。

为了下载页面,请使用--mirror 选项如下:

$ wget --mirror exampledomain.com

或使用:

$ wget -r -N -l DEPTH URL

-l 指定网页的深度为级别。这意味着它只会遍历那么多级别。它与-r(递归)一起使用。-N 参数用于为文件启用时间戳。URL 是需要启动下载的网站的基本 URL。

使用 HTTP 或 FTP 身份验证访问页面

有些网页需要对 HTTP 或 FTP URL 进行身份验证。这可以通过使用--user 和--password 参数来提供:

$ wget –-user username –-password pass URL

还可以在不指定内联密码的情况下请求密码。为此,请使用--ask-password 而不是--password 参数。

将网页作为格式化纯文本下载

网页是包含一系列 HTML 标记以及其他元素(如 JavaScript、CSS 等)的 HTML 页面。但是 HTML 标记定义了网页的基础。在查找特定内容时,我们可能需要解析网页中的数据,这是 Bash 脚本可以帮助我们的地方。当我们下载一个网页时,我们会收到一个 HTML 文件。为了查看格式化的数据,它应该在 Web 浏览器中查看。然而,在大多数情况下,解析格式化的文本文档将比解析 HTML 数据更容易。因此,如果我们可以获得一个与在 Web 浏览器上看到的网页类似的格式化文本的文本文件,那将更有用,并且可以节省大量去除 HTML 标记所需的工作。Lynx 是一个有趣的命令行 Web 浏览器。我们实际上可以从 Lynx 获取网页作为纯文本格式化输出。让我们看看如何做到这一点。

如何做…

让我们使用 lynx 命令的--dump 标志将网页视图以 ASCII 字符表示形式下载到文本文件中:

$ lynx -dump URL > webpage_as_text.txt

该命令还将所有超链接()单独列在文本输出的页脚下的References标题下。这将帮助我们避免使用正则表达式单独解析链接。

例如:

$ lynx -dump http://google.com > plain_text_page.txt

您可以使用 cat 命令查看文本的纯文本版本如下:

$ cat plain_text_page.txt

cURL 入门

cURL 是一个强大的实用程序,支持包括 HTTP、HTTPS、FTP 等在内的许多协议。它支持许多功能,包括 POST、cookie、身份验证、从指定偏移量下载部分文件、引用、用户代理字符串、额外标头、限制速度、最大文件大小、进度条等。cURL 对于我们想要玩转自动化网页使用序列并检索数据时非常有用。这个配方是 cURL 最重要的功能列表。

准备工作

cURL 不会默认随主要 Linux 发行版一起提供,因此您可能需要使用软件包管理器安装它。默认情况下,大多数发行版都附带 wget。

cURL 通常将下载的文件转储到 stdout,并将进度信息转储到 stderr。为了避免显示进度信息,我们总是使用--silent 选项。

如何做…

curl 命令可用于执行不同的活动,如下载、发送不同的 HTTP 请求、指定 HTTP 标头等。让我们看看如何使用 cURL 执行不同的任务。

$ curl URL --silent

上述命令将下载的文件转储到终端(下载的数据写入 stdout)。

--silent选项用于防止curl命令显示进度信息。如果需要进度信息,请删除--silent

$ curl URL –-silent -O

使用-O选项将下载的数据写入文件,文件名从 URL 中解析而不是写入标准输出。

例如:

$ curl http://slynux.org/index.html --silent -O

将创建index.html

它将网页或文件写入与 URL 中的文件名相同的文件,而不是写入stdout。如果 URL 中没有文件名,将产生错误。因此,请确保 URL 是指向远程文件的 URL。curl http://slynux.org -O --silent将显示错误,因为无法从 URL 中解析文件名。

$ curl URL –-silent -o new_filename

-o选项用于下载文件并写入指定的文件名。

为了在下载时显示#进度条,使用--progress而不是--silent

$ curl http://slynux.org -o index.html --progress
################################## 100.0% 

还有更多...

在前面的部分中,我们已经学习了如何下载文件并将 HTML 页面转储到终端。cURL 还有一些高级选项。让我们更深入地了解 cURL。

继续/恢复下载

cURL 具有高级的恢复下载功能,可以在给定的偏移量继续下载,而wget不具备这个功能。它可以通过指定偏移量来下载文件的部分。

$ curl URL/file -C offset

偏移是以字节为单位的整数值。

如果我们想要恢复下载文件,cURL 不需要我们知道确切的字节偏移量。如果要 cURL 找出正确的恢复点,请使用-C -选项,就像这样:

$ curl -C - URL

cURL 将自动找出重新启动指定文件的下载位置。

使用 cURL 设置引用字符串

引用者是 HTTP 头中的一个字符串,用于标识用户到达当前网页的页面。当用户从网页 A 点击链接到达网页 B 时,页面 B 中的引用头字符串将包含页面 A 的 URL。

一些动态页面在返回 HTML 数据之前会检查引用字符串。例如,当用户通过在 Google 上搜索导航到网站时,网页会显示一个附加了 Google 标志的页面,当他们通过手动输入 URL 导航到网页时,会显示不同的页面。

网页可以编写一个条件,如果引用者是www.google.com,则返回一个 Google 页面,否则返回一个不同的页面。

您可以使用curl命令的--referer选项指定引用字符串如下:

$ curl –-referer Referer_URL target_URL

例如:

$ curl –-referer http://google.com http://slynux.org

使用 cURL 的 cookies

使用curl我们可以指定并存储在 HTTP 操作期间遇到的 cookies。

为了指定 cookies,使用--cookie "COOKIES"选项。

Cookies 应该提供为name=value。多个 cookies 应该用分号“;”分隔。例如:

$ curl http://example.com –-cookie "user=slynux;pass=hack"

为了指定存储遇到的 cookies 的文件,使用--cookie-jar选项。例如:

$ curl URL –-cookie-jar cookie_file

使用 cURL 设置用户代理字符串

一些检查用户代理的网页如果没有指定用户代理就无法工作。您可能已经注意到,某些网站只在 Internet Explorer(IE)中运行良好。如果使用不同的浏览器,网站将显示一个消息,表示只能在 IE 上运行。这是因为网站检查用户代理。您可以使用curl将用户代理设置为 IE,并查看在这种情况下返回不同的网页。

使用 cURL 可以使用--user-agent-A来设置如下:

$ curl URL –-user-agent "Mozilla/5.0"

可以使用 cURL 传递附加的标头。使用-H "Header"传递多个附加标头。例如:

$ curl -H "Host: www.slynux.org" -H "Accept-language: en" URL

在 cURL 上指定带宽限制

当可用带宽有限且多个用户共享互联网时,为了平稳地共享带宽,我们可以通过使用--limit-rate选项从curl限制下载速率到指定的限制。

$ curl URL --limit-rate 20k

在这个命令中,k(千字节)和m(兆字节)指定了下载速率限制。

指定最大下载大小

可以使用--max-filesize选项指定 cURL 的最大下载文件大小如下:

$ curl URL --max-filesize bytes

如果文件大小超过,则返回非零退出代码。如果成功,则返回零。

使用 cURL 进行身份验证

可以使用 cURL 和-u参数进行 HTTP 身份验证或 FTP 身份验证。

可以使用-u username:password指定用户名和密码。也可以不提供密码,这样在执行时会提示输入密码。

如果您希望提示输入密码,可以仅使用-u username。例如:

$ curl -u user:pass http://test_auth.com

为了提示输入密码,请使用:

$ curl -u user http://test_auth.com 

打印响应标头,不包括数据

仅打印响应标头非常有用,可以应用许多检查或统计。例如,要检查页面是否可访问,我们不需要下载整个页面内容。只需读取 HTTP 响应标头即可用于识别页面是否可用。

检查 HTTP 标头的一个示例用例是在下载之前检查文件大小。我们可以检查 HTTP 标头中的Content-Length参数以找出文件的长度。还可以从标头中检索到几个有用的参数。Last-Modified参数使我们能够知道远程文件的最后修改时间。

使用curl–I–head选项仅转储 HTTP 标头而不下载远程文件。例如:

$ curl -I http://slynux.org
HTTP/1.1 200 OK 
Date: Sun, 01 Aug 2010 05:08:09 GMT 
Server: Apache/1.3.42 (Unix) mod_gzip/1.3.26.1a mod_log_bytes/1.2 mod_bwlimited/1.4 mod_auth_passthrough/1.8 FrontPage/5.0.2.2635 mod_ssl/2.8.31 OpenSSL/0.9.7a 
Last-Modified: Thu, 19 Jul 2007 09:00:58 GMT 
ETag: "17787f3-3bb0-469f284a" 
Accept-Ranges: bytes 
Content-Length: 15280 
Connection: close 
Content-Type: text/html

参见

  • 发布到网页并读取响应

从命令行访问 Gmail

Gmail 是谷歌提供的广泛使用的免费电子邮件服务[: http://mail.google.com/](http://: http://mail.google.com/)。 Gmail 允许您通过经过身份验证的 RSS 订阅来阅读邮件。我们可以解析 RSS 订阅,其中包括发件人的姓名和主题为电子邮件。这将有助于在不打开网络浏览器的情况下查看收件箱中的未读邮件。

如何做...

让我们通过 shell 脚本来解析 Gmail 的 RSS 订阅以显示未读邮件:

#!/bin/bash
Filename: fetch_gmail.sh
#Description: Fetch gmail tool

username="PUT_USERNAME_HERE"
password="PUT_PASSWORD_HERE"

SHOW_COUNT=5 # No of recent unread mails to be shown

echo

curl  -u $username:$password --silent "https://mail.google.com/mail/feed/atom" | \
tr -d '\n' | sed 's:</entry>:\n:g' |\
 sed 's/.*<title>\(.*\)<\/title.*<author><name>\([^<]*\)<\/name><email>\([^<]*\).*/Author: \2 [\3] \nSubject: \1\n/' | \
head -n $(( $SHOW_COUNT * 3 ))

输出将如下所示:

$ ./fetch_gmail.sh
Author: SLYNUX [ slynux@slynux.com ]
Subject: Book release - 2

Author: SLYNUX [ slynux@slynux.com ]
Subject: Book release - 1
.
… 5 entries

它是如何工作的...

该脚本使用 cURL 通过用户身份验证下载 RSS 订阅。用户身份验证由-u username:password参数提供。您可以使用-u user而不提供密码。然后在执行 cURL 时,它将交互式地要求输入密码。

在这里,我们可以将管道命令拆分为不同的块,以说明它们的工作原理。

tr -d '\n'删除换行符,以便我们使用\n作为分隔符重构每个邮件条目。sed 's:</entry>:\n:g'将每个</entry>替换为换行符,以便每个邮件条目都由换行符分隔,因此可以逐个解析邮件。查看mail.google.com/mail/feed/atom的源代码,了解 RSS 订阅中使用的 XML 标记。<entry> TAGS </entry>对应于单个邮件条目。

下一个脚本块如下:

 sed 's/.*<title>\(.*\)<\/title.*<author><name>\([^<]*\)<\/name><email>\([^<]*\).*/Author: \2 [\3] \nSubject: \1\n/'

此脚本使用<title>\(.*\)<\/title匹配子字符串标题,使用<author><name>\([^<]*\)<\/name>匹配发件人姓名,使用<email>\([^<]*\)匹配电子邮件。然后使用反向引用如下:

  • Author: \2 [\3] \nSubject: \1\n用于以易于阅读的格式替换邮件的条目。\1对应于第一个子字符串匹配,\2对应于第二个子字符串匹配,依此类推。

  • SHOW_COUNT=5变量用于在终端上打印未读邮件条目的数量。

  • head用于仅显示来自第一行的SHOW_COUNT*3行。 SHOW_COUNT被使用三次,以便显示输出的三行。

参见

  • cURL 入门,解释了 curl 命令

  • 基本的 sed 入门第四章,解释了 sed 命令

从网站解析数据

通过消除不必要的细节,从网页中解析数据通常很有用。sedawk是我们将用于此任务的主要工具。您可能已经在上一章的 grep 示例中看到了一个访问排名列表;它是通过解析网站页面www.johntorres.net/BoxOfficefemaleList.html生成的。

让我们看看如何使用文本处理工具解析相同的数据。

如何做...

让我们通过用于解析女演员详情的命令序列:

$ lynx -dump http://www.johntorres.net/BoxOfficefemaleList.html  | \ grep -o "Rank-.*" | \
sed 's/Rank-//; s/\[[0-9]\+\]//' | \
sort -nk 1 |\
 awk ' 
{
 for(i=3;i<=NF;i++){ $2=$2" "$i } 
 printf "%-4s %s\n", $1,$2 ; 
}' > actresslist.txt

输出将如下所示:

# Only 3 entries shown. All others omitted due to space limits
1   Keira Knightley 
2   Natalie Portman 
3   Monica Bellucci 

它是如何工作的...

Lynx 是一个命令行网页浏览器;它可以转储网站的文本版本,就像我们在网页浏览器中看到的那样,而不是显示原始代码。因此,它避免了删除 HTML 标记的工作。我们使用sed解析以 Rank 开头的行,如下所示:

sed 's/Rank-//; s/\[[0-9]\+\]//'

然后可以根据排名对这些行进行排序。这里使用awk来保持排名和名称之间的间距,通过指定宽度来使其统一。%-4s指定四个字符的宽度。除了第一个字段之外的所有字段都被连接在一起形成一个单个字符串$2

另请参阅

  • 第四章的基本 sed 入门,解释了 sed 命令

  • 第四章的基本 awk 入门,解释了 awk 命令

  • 以格式化纯文本形式下载网页,解释了 lynx 命令

图像爬虫和下载器

当我们需要下载出现在网页中的所有图像时,图像爬虫非常有用。我们可以使用脚本来解析图像文件并自动下载,而不是查看 HTML 源并选择所有图像。让我们看看如何做到这一点。

如何做...

让我们编写一个 Bash 脚本来爬取并从网页下载图像,如下所示:

#!/bin/bash
#Description: Images downloader
#Filename: img_downloader.sh

if [ $# -ne 3 ];
then
  echo "Usage: $0 URL -d DIRECTORY"
  exit -1
fi

for i in {1..4}
do
  case $1 in
  -d) shift; directory=$1; shift ;;
   *) url=${url:-$1}; shift;;
esac
done

mkdir -p $directory;
baseurl=$(echo $url | egrep -o "https?://[a-z.]+")

curl –s $url | egrep -o "<img src=[^>]*>" | sed 's/<img src=\"\([^"]*\).*/\1/g' > /tmp/$$.list

sed -i "s|^/|$baseurl/|" /tmp/$$.list

cd $directory;

while read filename;
do
  curl –s -O "$filename" --silent

done < /tmp/$$.list

一个示例用法如下:

$ ./img_downloader.sh http://www.flickr.com/search/?q=linux -d images

它是如何工作的...

上述图像下载器脚本解析 HTML 页面,除了<img>之外剥离所有标记,然后从<img>标记中解析src="img/URL"并将其下载到指定目录。此脚本接受网页 URL 和目标目录路径作为命令行参数。脚本的第一部分是解析命令行参数的一种巧妙方法。[ $# -ne 3 ]语句检查脚本的参数总数是否为三,否则退出并返回一个使用示例。

如果有 3 个参数,那么解析 URL 和目标目录。为了做到这一点,使用了一个巧妙的技巧:

for i in {1..4}
do 
 case $1 in
 -d) shift; directory=$1; shift ;;
 *) url=${url:-$1}; shift;;
esac
done

for循环迭代了四次(数字四没有特殊意义,只是为了运行case语句几次)。

case语句将评估第一个参数($1),并匹配-d或任何其他检查的字符串参数。我们可以在格式中的任何位置放置-d参数,如下所示:

$ ./img_downloader.sh -d DIR URL

或:

$ ./img_downloader.sh URL -d DIR

shift用于移动参数,这样当调用shift时,$1将被赋值为$2,再次调用时,$1=$3,依此类推,因为它将$1移动到下一个参数。因此,我们可以通过$1本身评估所有参数。

当匹配-d-d))时,很明显下一个参数是目标目录的值。*)对应默认匹配。它将匹配除-d之外的任何内容。因此,在迭代时,$1=""$1=URL在默认匹配中,我们需要取$1=URL避免""覆盖。因此我们使用url=${url:-$1}技巧。如果已经不是"",它将返回一个 URL 值,否则它将分配$1

egrep -o "<img src=[^>]*>"将仅打印匹配的字符串,即包括其属性的<img>标记。[^>]*用于匹配除了结束>之外的所有字符,即<img src="img/image.jpg" …. >

sed 's/<img src=\"\([^"]*\).*/\1/g'解析src="img/url",以便可以从已解析的<img>标记中解析所有图像 URL。

有两种类型的图像源路径:相对和绝对。绝对路径包含以http://https://开头的完整 URL。相对 URL 以/image_name本身开头。

绝对 URL 的示例是:example.com/image.jpg

相对 URL 的示例是:/image.jpg

对于相对 URL,起始的/应该被替换为基本 URL,以将其转换为example.com/image.jpg

为了进行转换,我们首先通过解析找出baseurl sed

然后用sed -i "s|^/|$baseurl/|" /tmp/$$.list将起始的/替换为baseurl sed

然后使用while循环逐行迭代列表,并使用curl下载 URL。使用--silent参数与curl一起,以避免在屏幕上打印其他进度消息。

另请参阅

  • cURL 入门,解释了 curl 命令

  • 基本的 sed 入门 第四章 ,解释 sed 命令

  • 使用 grep 在文件中搜索和挖掘“文本” 第四章 ,解释 grep 命令

Web 相册生成器

Web 开发人员通常为网站设计照片相册页面,该页面包含页面上的许多图像缩略图。单击缩略图时,将显示图片的大版本。但是,当需要许多图像时,每次复制<img>标签,调整图像以创建缩略图,将它们放在 thumbs 目录中,测试链接等都是真正的障碍。这需要很多时间并且重复相同的任务。通过编写一个简单的 Bash 脚本,可以轻松自动化。通过编写脚本,我们可以在几秒钟内自动创建缩略图,将它们放在确切的目录中,并自动生成<img>标签的代码片段。这个配方将教你如何做到这一点。

准备工作

我们可以使用for循环执行此任务,该循环遍历当前目录中的每个图像。通常使用 Bash 实用程序,如catconvert(image magick)。这些将生成一个 HTML 相册,使用所有图像,放在index.html中。为了使用convert,请确保已安装 Imagemagick。

如何做...

让我们编写一个 Bash 脚本来生成 HTML 相册页面:

#!/bin/bash
#Filename: generate_album.sh
#Description: Create a photo album using images in current directory

echo "Creating album.."
mkdir -p thumbs
cat <<EOF > index.html
<html>
<head>
<style>

body 
{ 
  width:470px;
  margin:auto;
  border: 1px dashed grey;
  padding:10px; 
} 

img
{ 
  margin:5px;
  border: 1px solid black;

} 
</style>
</head>
<body>
<center><h1> #Album title </h1></center>
<p>
EOF

for img in *.jpg;
do 
  convert "$img" -resize "100x" "thumbs/$img"
  echo "<a href=\"$img\" ><img src=\"thumbs/$img\" title=\"$img\" /></a>" >> index.html
done

cat <<EOF >> index.html

</p>
</body>
</html>
EOF 

echo Album generated to index.html

按以下方式运行脚本:

$ ./generate_album.sh
Creating album..
Album generated to index.html

工作原理...

脚本的初始部分是编写 HTML 页面的标题部分。

以下脚本将所有内容重定向到 EOF(不包括)到index.html

cat <<EOF > index.html
contents...
EOF

标题包括 HTML 和样式表。

for img in *.jpg;将遍历每个文件的名称并执行操作。

convert "$img" -resize "100x" "thumbs/$img"将创建宽度为 100px 的图像作为缩略图。

以下语句将生成所需的<img>标签并将其附加到index.html

echo "<a href=\"$img\" ><img src=\"thumbs/$img\" title=\"$img\" /></a>" >> index.html

最后,使用cat附加页脚 HTML 标记。

另请参阅

  • 玩转文件描述符和重定向 第一章 ,解释 EOF 和 stdin 重定向。

Twitter 命令行客户端

Twitter 是最热门的微博平台,也是在线社交媒体的最新热点。发推文和阅读推文很有趣。如果我们可以从命令行做这两件事呢?编写命令行 Twitter 客户端非常简单。Twitter 有 RSS feeds,因此我们可以利用它们。让我们看看如何做到这一点。

准备工作

我们可以使用 cURL 进行身份验证并发送 twitter 更新,以及下载 RSS feed 页面以解析 tweets。只需四行代码就可以做到。让我们来做吧。

如何做...

让我们编写一个 Bash 脚本,使用curl命令来操作 twitter API:

#!/bin/bash
#Filename: tweets.sh
#Description: Basic twitter client

USERNAME="PUT_USERNAME_HERE"
PASSWORD="PUT_PASSWORD_HERE"
COUNT="PUT_NO_OF_TWEETS"

if [[ "$1" != "read" ]] && [[ "$1" != "tweet" ]];
then 
  echo -e "Usage: $0 send status_message\n   OR\n      $0 read\n"
  exit -1;
fi

if [[ "$1" = "read" ]];
then 
  curl --silent -u $USERNAME:$PASSWORD  http://twitter.com/statuses/friends_timeline.rss | \
grep title | \
tail -n +2 | \
head -n $COUNT | \
  sed 's:.*<title>\([^<]*\).*:\n\1:'

elif [[ "$1" = "tweet" ]];
then 
  status=$( echo $@ | tr -d '"' | sed 's/.*tweet //')
  curl --silent -u $USERNAME:$PASSWORD -d status="$status" http://twitter.com/statuses/update.xml > /dev/null
  echo 'Tweeted :)'
fi

运行以下脚本:

$ ./tweets.sh tweet Thinking of writing a X version of wall command "#bash"
Tweeted :)

$ ./tweets.sh read
bot: A tweet line
t3rm1n4l: Thinking of writing a X version of wall command #bash

工作原理...

让我们通过将上述脚本分成两部分来看看它的工作。第一部分是关于阅读推文的。要阅读推文,脚本会从twitter.com/statuses/friends_timeline.rss下载 RSS 信息,并解析包含<title>标签的行。然后,它使用sed剥离<title></title>标签,以形成所需的推文文本。然后使用COUNT变量来使用head命令除了最近推文的数量之外的所有其他文本。使用tail -n +2来删除不必要的标题文本“Twitter: Timeline of friends”。

在发送推文部分,curl-d状态参数用于使用 Twitter 的 API 发布数据到 Twitter:twitter.com/statuses/update.xml

在发送推文的情况下,脚本的$1将是推文。然后,为了获取状态,我们使用$@(脚本的所有参数的列表)并从中删除单词“tweet”。

另请参阅

  • cURL 入门,解释了 curl 命令

  • 头和尾-打印最后或前 10 行 第三章,解释了头和尾命令

具有 Web 后端的定义实用程序

谷歌通过使用搜索查询define:WORD为任何单词提供 Web 定义。我们需要一个 GUI 网页浏览器来获取定义。但是,我们可以通过使用脚本来自动化并解析所需的定义。让我们看看如何做到这一点。

准备工作

我们可以使用lynxsedawkgrep来编写定义实用程序。

如何做...

让我们看看从 Google 搜索中获取定义的定义实用程序脚本的核心部分:

#!/bin/bash
#Filename: define.sh
#Description: A Google define: frontend

limit=0
if  [ ! $# -ge 1 ];
then
  echo -e "Usage: $0 WORD [-n No_of_definitions]\n"
  exit -1;
fi

if [ "$2" = "-n" ];
then
  limit=$3;
  let limit++
fi

word=$1

lynx -dump http://www.google.co.in/search?q=define:$word | \
awk '/Defini/,/Find defini/' | head -n -1 | sed 's:*:\n*:; s:^[ ]*::' | \
grep -v "[[0-9]]" | \
awk '{
if ( substr($0,1,1) == "*" )
{ sub("*",++count".") } ;
print
} ' >  /tmp/$$.txt

echo

if [ $limit -ge 1 ];
then

cat /tmp/$$.txt | sed -n "/¹\./, /${limit}/p" | head -n -1

else

cat /tmp/$$.txt;

fi

按以下方式运行脚本:

$ ./define.sh hack -n 2
1\. chop: cut with a hacking tool
2\. one who works hard at boring tasks

它是如何工作的...

我们将研究定义解析器的核心部分。Lynx 用于获取网页的纯文本版本。www.google.co.in/search?q=define:$word是网页定义网页的 URL。然后我们缩小“网页上的定义”和“查找定义”之间的文本。所有的定义都出现在这些文本行之间(awk '/Defini/,/Find defini/')。

's:*:\n*:'用于将替换为和换行符,以便在每个定义之间插入换行符,s:^[ ]*::用于删除行首的额外空格。在 lynx 输出中,超链接标记为[数字]。这些行通过grep -v(反向匹配行选项)被移除。然后使用awk将出现在行首的替换为数字,以便为每个定义分配一个序号。如果我们在脚本中读取了一个-n计数,它必须根据计数输出一些定义。因此,使用awk打印序号 1 到计数的定义(这样做更容易,因为我们用序号替换了)。

另请参阅

  • 基本的 sed 入门 第四章,解释了 sed 命令

  • 基本的 awk 入门 第四章,解释了 awk 命令

  • 使用 grep 在文件中搜索和挖掘“文本” 第四章,解释了 grep 命令

  • 将网页下载为格式化的纯文本,解释了 lynx 命令

在网站中查找损坏的链接

我看到人们手动检查网站上的每个页面以查找损坏的链接。这仅适用于页面非常少的网站。当页面数量变得很多时,这将变得不可能。如果我们可以自动查找损坏的链接,那将变得非常容易。我们可以使用 HTTP 操作工具来查找损坏的链接。让我们看看如何做到这一点。

准备工作

为了识别链接并从链接中找到损坏的链接,我们可以使用lynxcurl。它有一个-traversal选项,它将递归访问网站中的页面,并构建网站中所有超链接的列表。我们可以使用 cURL 来验证每个链接是否损坏。

如何做...

让我们通过curl命令编写一个 Bash 脚本来查找网页上的损坏链接:

#!/bin/bash 
#Filename: find_broken.sh
#Description: Find broken links in a website

if [ $# -eq 2 ]; 
then 
  echo -e "$Usage $0 URL\n" 
  exit -1; 
fi 

echo Broken links: 

mkdir /tmp/$$.lynx 

cd /tmp/$$.lynx 

lynx -traversal $1 > /dev/null 
count=0; 

sort -u reject.dat > links.txt 

while read link; 
do 
  output=`curl -I $link -s | grep "HTTP/.*OK"`; 
  if [[ -z $output ]]; 
  then 
    echo $link; 
    let count++ 
  fi 

done < links.txt 

[ $count -eq 0 ] && echo No broken links found.

工作原理...

lynx -traversal URL将在工作目录中生成多个文件。其中包括一个名为reject.dat的文件,其中包含网站中的所有链接。使用sort -u来避免重复构建列表。然后我们遍历每个链接,并使用curl -I检查标题响应。如果标题包含第一行HTTP/1.0 200 OK作为响应,这意味着目标不是损坏的。所有其他响应对应于损坏的链接,并打印到stdout

另请参阅

  • 以格式化纯文本形式下载网页,解释了 lynx 命令

  • cURL 入门,解释了 curl 命令

跟踪网站的变化

跟踪网站的变化对于网页开发人员和用户非常有帮助。在间隔时间内手动检查网站非常困难和不切实际。因此,我们可以编写一个在重复间隔时间内运行的变化跟踪器。当发生变化时,它可以播放声音或发送通知。让我们看看如何编写一个基本的网站变化跟踪器。

准备工作

在 Bash 脚本中跟踪网页变化意味着在不同时间获取网站并使用diff命令进行差异。我们可以使用curldiff来做到这一点。

如何做...

让我们通过组合不同的命令来编写一个 Bash 脚本来跟踪网页中的变化:

#!/bin/bash
#Filename: change_track.sh
#Desc: Script to track changes to webpage

if [ $# -eq 2 ];
then 
  echo -e "$Usage $0 URL\n"
  exit -1;
fi

first_time=0
# Not first time

if [ ! -e "last.html" ];
then
  first_time=1
  # Set it is first time run
fi

curl --silent $1 -o recent.html

if [ $first_time -ne 1 ];
then
  changes=$(diff -u last.html recent.html)
  if [ -n "$changes" ];
  then
    echo -e "Changes:\n"
    echo "$changes"
  else
    echo -e "\nWebsite has no changes"
  fi
else
  echo "[First run] Archiving.."

fi

cp recent.html last.html

让我们看看track_changes.sh脚本在网页发生变化和网页未发生变化时的输出:

  • 首次运行:
$ ./track_changes.sh http://web.sarathlakshman.info/test.html
[First run] Archiving..

  • 第二次运行:
$ ./track_changes.sh http://web.sarathlakshman.info/test.html
Website has no changes 

  • 对网页进行更改后的第三次运行:
$ ./test.sh http://web.sarathlakshman.info/test_change/test.html 
Changes: 

--- last.html	2010-08-01 07:29:15.000000000 +0200 
+++ recent.html	2010-08-01 07:29:43.000000000 +0200 
@@ -1,3 +1,4 @@ 
<html>
+added line :)
<p>data</p>
</html>

工作原理...

该脚本通过[!-elast.html]检查脚本是否是第一次运行。如果last.html不存在,这意味着这是第一次,因此必须下载网页并将其复制为last.html

如果不是第一次,它应该下载新副本(recent.html)并使用diff实用程序检查差异。如果有变化,它应该打印出变化,最后应该将recent.html复制到last.html

另请参阅

  • cURL 入门,解释了 curl 命令

向网页提交并读取响应

POST 和 GET 是 HTTP 中用于向网站发送信息或检索信息的两种请求类型。在 GET 请求中,我们通过网页 URL 本身发送参数(名称-值对)。在 POST 的情况下,它不会附加在 URL 上。当需要提交表单时使用 POST。例如,需要提交用户名、密码和检索登录页面。

在编写基于网页检索的脚本时,POST 到页面的使用频率很高。让我们看看如何使用 POST。通过发送 POST 数据和检索输出来自动执行 HTTP GET 和 POST 请求是我们在编写从网站解析数据的 shell 脚本时练习的非常重要的任务。

准备工作

cURL 和wget都可以通过参数处理 POST 请求。它们作为名称-值对传递。

如何做...

让我们看看如何使用curl从真实网站进行 POST 和读取 HTML 响应:

$ curl URL -d "postvar=postdata2&postvar2=postdata2"

我们有一个网站(book.sarathlakshman.com/lsc/mlogs/),用于提交当前用户信息,如主机名和用户名。假设在网站的主页上有两个字段 HOSTNAME 和 USER,以及一个 SUBMIT 按钮。当用户输入主机名、用户名并单击 SUBMIT 按钮时,详细信息将存储在网站中。可以使用一行curl命令自动化此过程,通过自动化 POST 请求。如果查看网站源代码(使用 Web 浏览器的查看源代码选项),可以看到类似于以下代码的 HTML 表单定义:

<form action="http://book.sarathlakshman.com/lsc/mlogs/submit.php" method="post" >

<input type="text" name="host" value="HOSTNAME" >
<input type="text" name="user" value="USER" >
<input type="submit" >
</form>

在这里,book.sarathlakshman.com/lsc/mlogs/submit.php是目标 URL。当用户输入详细信息并单击提交按钮时,主机和用户输入将作为 POST 请求发送到submit.php,并且响应页面将返回到浏览器。

我们可以按照以下方式自动化 POST 请求:

$ curl http://book.sarathlakshman.com/lsc/mlogs/submit.php -d "host=test-host&user=slynux"
<html>
You have entered :
<p>HOST : test-host</p>
<p>USER : slynux</p>
<html>

现在curl返回响应页面。

-d是用于发布的参数。 -d的字符串参数类似于 GET 请求语义。 var=value对应关系应该由&分隔。

注意

-d参数应该总是用引号括起来。如果不使用引号,&会被 shell 解释为表示这应该是一个后台进程。

还有更多

让我们看看如何使用 cURL 和wget执行 POST。

在 curl 中进行 POST

您可以使用-d–datacurl中发送 POST 数据,如下所示:

$ curl –-data "name=value" URL -o output.html

如果要发送多个变量,请用&分隔它们。请注意,当使用&时,名称-值对应该用引号括起来,否则 shell 将把&视为后台进程的特殊字符。例如:

$ curl -d "name1=val1&name2=val2" URL -o output.html

使用 wget 发送 POST 数据

您可以使用wget通过使用-–post-data "string"来发送 POST 数据。例如:

$ wget URL –post-data "name=value" -O output.html

使用与 cURL 相同的格式进行名称-值对。

另请参阅

  • 关于 cURL 的入门,解释了 curl 命令

  • 从网页下载解释了 wget 命令

第六章:备份计划

在本章中,我们将涵盖:

  • 使用 tar 进行存档

  • 使用 cpio 进行存档

  • 使用 gunzip(gzip)进行压缩

  • 使用 bunzip(bzip)进行压缩

  • 使用 lzma 进行压缩

  • 使用 zip 进行存档和压缩

  • 重压缩 squashfs 文件系统

  • 使用标准算法对文件和文件夹进行加密

  • 使用 rsync 备份快照

  • 使用 git 进行版本控制备份

  • 使用 dd 进行克隆磁盘

介绍

数据的快照和备份是我们经常遇到的常规任务。当涉及到服务器或大型数据存储系统时,定期备份非常重要。可以通过 shell 脚本自动化备份。存档和压缩似乎在系统管理员或普通用户的日常生活中找到了用途。有各种压缩格式可以以各种方式使用,以便获得最佳结果。加密是另一个经常使用的任务,用于保护数据。为了减小加密数据的大小,通常在加密之前对文件进行存档和压缩。有许多标准的加密算法可供使用,并且可以使用 shell 实用程序进行处理。本章将介绍使用 shell 创建和维护文件或文件夹存档、压缩格式和加密技术的不同方法。让我们来看看这些方法。

使用 tar 进行存档

tar命令可用于存档文件。它最初是为了在磁带存档上存储数据而设计的。它允许您将多个文件和目录存储为单个文件。它可以保留所有文件属性,如所有者、权限等。tar命令创建的文件通常被称为 tarball。

准备就绪

tar命令默认随所有类 UNIX 操作系统一起提供。它具有简单的语法,并且是一种可移植的文件格式。让我们看看如何做到这一点。

tar有一系列参数:Acdrtuxfv。每个字母都可以独立使用,用于相应的不同目的。

如何做到…

要使用 tar 存档文件,请使用以下语法:

$ tar -cf output.tar [SOURCES]

例如:

$ tar -cf output.tar file1 file2 file3 folder1 ..

在这个命令中,-c代表“创建文件”,–f代表“指定文件名”。

我们可以将文件夹和文件名指定为SOURCES。我们可以使用文件名列表或通配符,如*.txt来指定源。

它将源文件存档到名为output.tar的文件中。

文件名必须立即出现在–f之后,并且应该是参数组中的最后一个选项(例如,-cvvf filename.tar-tvvf filename.tar)。

我们不能将数百个文件或文件夹作为命令行参数传递,因为有限制。因此,如果要存档许多文件,最好使用附加选项。

还有更多…

让我们看看tar命令提供的其他功能。

将文件附加到存档中

有时我们可能需要将文件添加到已经存在的存档中(一个示例用法是当需要存档数千个文件时,无法将它们作为命令行参数在一行中指定)。

附加选项:-r

为了将文件附加到已经存在的存档中使用:

$ tar -rvf original.tar new_file

按如下方式列出存档中的文件:

$ tar -tf archive.tar
yy/lib64/
yy/lib64/libfakeroot/
yy/sbin/

为了在存档或列表时打印更多细节,使用-v–vv标志。这些标志称为详细(v),它将在终端上打印更多细节。例如,通过使用详细,您可以打印更多细节,如文件权限、所有者组、修改日期等。

例如:

$ tar -tvvf archive.tar
drwxr-xr-x slynux/slynux     0 2010-08-06 09:31 yy/
drwxr-xr-x slynux/slynux     0 2010-08-06 09:39 yy/usr/
drwxr-xr-x slynux/slynux     0 2010-08-06 09:31 yy/usr/lib64/

从存档中提取文件和文件夹

以下命令将存档的内容提取到当前目录:

$ tar -xf archive.tar

-x选项代表提取。

当使用–x时,tar命令将存档的内容提取到当前目录。我们还可以使用–C标志指定需要提取文件的目录,如下所示:

$ tar -xf archive.tar -C /path/to/extraction_directory

该命令将归档的内容提取到指定目录中。它提取归档的全部内容。我们也可以通过将它们指定为命令参数来仅提取一些文件:

$ tar -xvf file.tar file1 file4

上面的命令仅提取file1file4,并忽略归档中的其他文件。

使用 tar 的 stdin 和 stdout

在归档时,我们可以指定stdout作为输出文件,以便通过管道出现的另一个命令可以将其读取为stdin,然后执行一些处理或提取归档。

这对于通过安全外壳(SSH)连接传输数据很有帮助(在网络上)。例如:

$ mkdir ~/destination
$ tar -cf - file1 file2 file3 | tar -xvf -  -C ~/destination

在上面的例子中,file1file2file3被合并成一个 tarball,然后提取到~/destination。在这个命令中:

  • -f指定stdout作为归档文件(当使用-c选项时)

  • -f指定stdin作为提取文件(当使用-x选项时)

连接两个归档

我们可以使用-A选项轻松合并多个 tar 文件。

假设我们有两个 tarballs:file1.tarfile2.tar。我们可以将file2.tar的内容合并到file1.tar中,如下所示:

$ tar -Af file1.tar file2.tar

通过列出内容来验证它:

$ tar -tvf file1.tar

使用时间戳检查更新归档中的文件

追加选项将任何给定的文件追加到归档中。如果在归档中有相同的文件要追加,它将追加该文件,并且归档将包含重复文件。我们可以使用更新选项-u来指定仅追加比归档中具有相同名称的文件更新的文件。

$ tar -tf archive.tar
filea
fileb
filec

该命令列出归档中的文件。

为了仅在filea的修改时间比archive.tar中的filea更新时追加filea,使用:

$ tar -uvvf archive.tar filea

如果归档外的filea的版本和archive.tar中的filea具有相同的时间戳,则不会发生任何事情。

使用touch命令修改文件时间戳,然后再次尝试tar命令:

$ tar -uvvf archive.tar filea
-rw-r--r-- slynux/slynux     0 2010-08-14 17:53 filea

由于其时间戳比归档中的时间戳更新,因此文件被追加。

比较归档和文件系统中的文件

有时候知道归档中的文件和文件系统中具有相同文件名的文件是否相同或包含任何差异是有用的。–d标志可用于打印差异:

$ tar -df archive.tar filename1 filename2 ...

例如:

$ tar -df archive.tar afile bfile
afile: Mod time differs
afile: Size differs

从归档中删除文件

我们可以使用–delete选项从给定的归档中删除文件。例如:

$ tar -f archive.tar --delete file1 file2 ..

让我们看另一个例子:

$ tar -tf archive.tar
filea
fileb
filec

或者,我们也可以使用以下语法:

$ tar --delete --file archive.tar [FILE LIST]

例如:

$ tar --delete --file archive.tar filea
$ tar -tf archive.tar
fileb
filec

使用 tar 归档进行压缩

tar命令只对文件进行归档,不对其进行压缩。因此,大多数人在处理 tarballs 时通常会添加某种形式的压缩。这显着减小了文件的大小。Tarballs 通常被压缩成以下格式之一:

  • file.tar.gz

  • file.tar.bz2

  • file.tar.lzma

  • file.tar.lzo

不同的tar标志用于指定不同的压缩格式。

  • -j用于 bunzip2

  • -z用于 gzip

  • --lzma用于 lzma

它们在以下特定于压缩的配方中有解释。

可以使用压缩格式而不明确指定特殊选项。tar可以通过查看输出或输入文件名的给定扩展名来进行压缩。为了使tar通过查看扩展名自动支持压缩,请使用-a--auto-compresstar

从归档中排除一组文件

可以通过指定模式来排除一组文件不进行归档。使用--exclude [PATTERN]来排除与通配符模式匹配的文件。

例如,要排除所有.txt文件不进行归档,请使用:

$ tar -cf arch.tar * --exclude "*.txt"

提示

请注意,模式应该用双引号括起来。

还可以使用-X标志排除列表文件中提供的文件列表,如下所示:

$ cat list
filea
fileb

$ tar -cf arch.tar * -X list

现在排除了fileafileb不进行归档。

排除版本控制目录

我们通常使用 tarballs 来分发源代码。大多数源代码是使用诸如 subversion、Git、mercurial、cvs 等版本控制系统进行维护的。版本控制下的代码目录将包含用于管理版本的特殊目录,如.svn.git。但是,这些目录对代码本身并不需要,因此应该从源代码的 tarball 中删除。

为了在归档时排除与版本控制相关的文件和目录,请使用tar--exclude-vcs选项。例如:

$ tar --exclude-vcs -czvvf source_code.tar.gz eye_of_gnome_svn

打印总字节数

如果我们可以打印复制到归档中的总字节数,有时会很有用。通过使用--totals选项在归档后打印复制的总字节数,如下所示:

$ tar -cf arc.tar * --exclude "*.txt" --totals
Total bytes written: 20480 (20KiB, 12MiB/s)

另请参阅

  • 使用 gunzip(gzip)进行压缩,解释了 gzip 命令

  • 使用 bunzip(bzip2)进行压缩,解释了 bzip2 命令

  • 使用 lzma 进行压缩,解释了 lzma 命令

使用 cpio 进行归档

cpio是另一种类似于tar的归档格式。它用于将文件和目录存储在具有权限、所有权等属性的文件中。但是它并不像tar那样常用。然而,cpio似乎被用于 RPM 软件包归档、Linux 内核的 initramfs 文件等。本文将提供cpio的最小使用示例。

如何做...

cpio通过stdin接受输入文件名,并将归档写入stdout。我们必须将stdout重定向到文件以接收输出的cpio文件,如下所示:

创建测试文件:

$ touch file1 file2 file3

我们可以将测试文件归档如下:

$ echo file1 file2 file3 | cpio -ov > archive.cpio

在此命令中:

  • -o指定输出

  • -v用于打印已归档文件的列表

注意

通过使用cpio,我们还可以使用文件的绝对路径进行归档。/usr/somedir是一个绝对路径,因为它包含了从根目录(/)开始的完整路径。

相对路径不会以/开头,但它从当前目录开始。例如,test/file表示有一个名为test的目录,filetest目录内。

在提取时,cpio提取到绝对路径本身。但是在tar中,它会删除绝对路径中的/,并将其转换为相对路径。

为了列出cpio归档中的文件,请使用以下命令:

$ cpio -it < archive.cpio

此命令将列出给定cpio归档中的所有文件。它从stdin读取文件。在此命令中:

  • -i用于指定输入

  • -t用于列出

为了从cpio归档中提取文件,请使用:

$ cpio -id < archive.cpio

这里,-d用于提取。

它会在不提示的情况下覆盖文件。如果归档中存在绝对路径文件,它将替换该路径下的文件。它不会像tar那样在当前目录中提取文件。

使用 gunzip(gzip)进行压缩

gzip是 GNU/Linux 平台上常用的压缩格式。可用的实用程序包括gzipgunzipzcat,用于处理 gzip 压缩文件类型。gzip只能应用于文件。它不能归档目录和多个文件。因此我们使用tar归档并用gzip压缩。当多个文件作为输入时,它将产生多个单独压缩(.gz)文件。让我们看看如何使用gzip

如何做...

为了使用gzip压缩文件,请使用以下命令:

$ gzip filename

$ ls
filename.gz

然后它将删除该文件并生成名为filename.gz的压缩文件。

提取gzip压缩文件如下:

$ gunzip filename.gz

它将删除filename.gz并生成filename.gz的未压缩版本。

为了列出压缩文件的属性,请使用:

$ gzip -l test.txt.gz
compressed        uncompressed  ratio uncompressed_name
 35                   6        -33.3% test.txt

gzip命令可以从stdin读取文件,并将压缩文件写入stdout

stdin读取并输出到stdout如下:

$ cat file | gzip -c > file.gz

-c选项用于指定输出到stdout

我们可以为gzip指定压缩级别。使用--fast--best选项分别提供低和高的压缩比。

还有更多...

gzip命令通常与其他命令一起使用。它还有高级选项来指定压缩比。让我们看看如何使用这些功能。

使用 tarball 的 gzip

我们通常使用gzip与 tarballs。可以通过在归档和提取时传递-z选项给tar命令来压缩 tarball。

您可以使用以下方法创建 gzipped tarballs:

  • 方法-1
$ tar -czvvf archive.tar.gz [FILES]

或:

$ tar -cavvf archive.tar.gz [FILES]

-a选项指定应自动从扩展名检测压缩格式。

  • 方法-2

首先,创建一个 tarball:

$ tar -cvvf archive.tar [FILES]

在打包后进行压缩如下:

$ gzip archive.tar

如果有许多文件(几百个)需要在一个 tarball 中进行归档并进行压缩,我们使用方法-2 并进行少量更改。使用tar将许多文件作为命令参数的问题是它只能从命令行接受有限数量的文件。为了解决这个问题,我们可以使用循环逐个添加文件并使用附加选项(-r)创建一个tar文件,如下所示:

FILE_LIST="file1  file2  file3  file4  file5"

for f in $FILE_LIST;
do
tar -rvf archive.tar $f 
done

gzip archive.tar

为了提取一个 gzipped tarball,使用以下命令:

  • -x 用于提取

  • -z用于 gzip 规范

或:

$ tar -xavvf archive.tar.gz -C extract_directory

在上述命令中,使用-a选项自动检测压缩格式。

zcat-读取 gzipped 文件而不解压

zcat是一个命令,可用于从.gz文件中转储提取的文件到stdout,而无需手动提取它。.gz文件仍然与以前一样,但它将提取的文件转储到stdout,如下所示:

$ ls
test.gz

$ zcat test.gz
A test file
# file test contains a line "A test file"

$ ls
test.gz

压缩比

我们可以指定压缩比,可在 1 到 9 的范围内使用,其中:

  • 1 是最低的,但速度最快

  • 9 是最好的,但速度最慢

您还可以在之间指定比率,如下所示:

$ gzip -9 test.img

这将将文件压缩到最大。

另请参阅

  • 使用 tar 进行归档,解释了 tar 命令

使用 bunzip(bzip)进行压缩

bunzip2是另一种与gzip非常相似的压缩技术。bzip2通常比gzip产生更小(更压缩)的文件。它随所有 Linux 发行版一起提供。让我们看看如何使用bzip2

如何做...

为了使用bzip2进行压缩,使用:

$ bzip2 filename
$ ls
filename.bz2

然后它将删除文件并生成一个名为filename.bzip2的压缩文件。

提取一个 bzipped 文件如下:

$ bunzip2 filename.bz2

它将删除filename.bz2并产生一个未压缩版本的filename

bzip2可以从stdin读取文件,并将压缩文件写入stdout

为了从stdin读取并作为stdout读取,请使用:

$ cat file | bzip2 -c > file.tar.bz2

-c用于指定输出到stdout

我们通常使用bzip2与 tarballs。可以通过在归档和提取时传递-j选项给tar命令来压缩 tarball。

可以通过以下方法创建一个 bzipped tarball:

  • 方法-1
$ tar -cjvvf archive.tar.bz2 [FILES]

或:

$ tar -cavvf archive.tar.bz2 [FILES]

-a选项指定自动从扩展名检测压缩格式。

  • 方法-2

首先创建 tarball:

$ tar -cvvf archive.tar [FILES]

在 tarball 后进行压缩:

$ bzip2 archive.tar

如果我们需要将数百个文件添加到存档中,则上述命令可能会失败。要解决此问题,请使用循环逐个使用-r选项将文件附加到存档中。请参阅食谱中的类似部分,使用 gunzip(gzip)进行压缩

提取一个 bzipped tarball 如下:

$ tar -xjvvf archive.tar.bz2 -C extract_directory

在这个命令中:

  • -x用于提取

  • -j是用于bzip2规范

  • -C用于指定要提取文件的目录

或者,您可以使用以下命令:

$ tar -xavvf archive.tar.bz2 -C extract_directory

-a将自动检测压缩格式。

还有更多...

bunzip 有几个附加选项来执行不同的功能。让我们浏览其中的一些。

保留输入文件而不删除它们

在使用bzip2bunzip2时,它将删除输入文件并生成一个压缩的输出文件。但是我们可以使用-k选项防止它删除输入文件。

例如:

$ bunzip2 test.bz2 -k
$ ls
test test.bz2

压缩比

我们可以指定压缩比,可在 1 到 9 的范围内使用(其中 1 是最少压缩,但速度快,9 是最高可能的压缩,但要慢得多)。

例如:

$ bzip2 -9 test.img

此命令提供最大压缩。

另请参阅

  • 使用 tar 进行归档,解释了 tar 命令

使用 lzma 进行压缩

lzmagzipbzip2相比相对较新。lzma的压缩率比gzipbzip2更高。由于大多数 Linux 发行版上没有预安装lzma,您可能需要使用软件包管理器进行安装。

如何做到...

为了使用lzma进行压缩,请使用以下命令:

$ lzma filename
$ ls
filename.lzma

这将删除文件并生成名为filename.lzma的压缩文件。

要提取lzma文件,请使用:

$ unlzma filename.lzma

这将删除filename.lzma并生成文件的未压缩版本。

lzma命令还可以从stdin读取文件并将压缩文件写入stdout

为了从stdin读取并作为stdout读取,请使用:

$ cat file | lzma -c > file.lzma

-c用于指定输出到stdout

我们通常使用lzma与 tarballs。可以通过在归档和提取时传递--lzma选项给tar命令来压缩 tarball。

有两种方法可以创建lzma tarball:

  • 方法-1
$ tar -cvvf --lzma archive.tar.lzma [FILES]

或者:

$ tar -cavvf archive.tar.lzma [FILES]

-a选项指定自动从扩展名中检测压缩格式。

  • 方法-2

首先,创建 tarball:

$ tar -cvvf archive.tar [FILES]

在 tarball 后进行压缩:

$ lzma archive.tar

如果我们需要将数百个文件添加到存档中,则上述命令可能会失败。为了解决这个问题,使用循环使用-r选项逐个将文件附加到存档中。请参阅配方中的类似部分,使用 gunzip(gzip)进行压缩

还有更多...

让我们看看与lzma实用程序相关的其他选项

提取 lzma tarball

为了将使用lzma压缩的 tarball 提取到指定目录,请使用:

$ tar -xvvf --lzma archive.tar.lzma -C extract_directory

在这个命令中,-x用于提取。--lzma指定使用lzma来解压缩生成的文件。

或者,我们也可以使用:

$ tar -xavvf archive.tar.lzma -C extract_directory

-a选项指定自动从扩展名中检测压缩格式。

保留输入文件而不删除它们

在使用lzmaunlzma时,它将删除输入文件并生成输出文件。但是我们可以使用-k选项防止删除输入文件并保留它们。例如:

$ lzma test.bz2 -k
$ ls
test.bz2.lzma

压缩比

我们可以指定压缩比,可在 1 到 9 的范围内选择(1 表示最小压缩,但速度快,9 表示最大可能的压缩,但速度慢)。

您还可以按照以下方式指定比率:

$ lzma -9 test.img

此命令将文件压缩到最大。

另请参阅

  • 使用 tar 进行归档,解释了 tar 命令

使用 zip 进行归档和压缩

ZIP 是许多平台上常用的压缩格式。在 Linux 平台上,它不像gzipbzip2那样常用,但是互联网上的文件通常以这种格式保存。

如何做到...

为了使用 ZIP 进行归档,使用以下语法:

$ zip archive_name.zip [SOURCE FILES/DIRS]

例如:

$ zip file.zip file

在这里,将生成file.zip文件。

递归存档目录和文件如下:

$ zip -r archive.zip folder1 file2

在这个命令中,-r用于指定递归。

lzmagzipbzip2不同,zip在归档后不会删除源文件。zip在这方面类似于tar,但zip可以压缩tar无法压缩的文件。但是,zip也添加了压缩。

为了提取 ZIP 文件中的文件和文件夹,请使用:

$ unzip file.zip

它将提取文件而不删除filename.zip(不像unlzmagunzip)。

为了使用文件系统中的新文件更新存档中的文件,请使用-u标志:

$ zip file.zip -u newfile

通过使用-d来从压缩存档中删除文件,如下所示:

$ zip -d arc.zip file.txt

为了列出存档中的文件,请使用:

$ unzip -l archive.zip

squashfs-重压缩文件系统

squashfs是一种基于重压缩的只读文件系统,能够将 2 到 3GB 的数据压缩到 700MB 的文件中。您是否曾经想过 Linux Live CD 是如何工作的?当启动 Live CD 时,它加载完整的 Linux 环境。Linux Live CD 使用名为 squashfs 的只读压缩文件系统。它将根文件系统保留在一个压缩的文件系统文件中。它可以进行回环挂载并访问文件。因此,当进程需要一些文件时,它们会被解压缩并加载到 RAM 中并被使用。当构建自定义的实时操作系统或需要保持文件高度压缩并且无需完全提取文件时,了解 squashfs 可能是有用的。对于提取大型压缩文件,需要很长时间。但是,如果文件进行回环挂载,它将非常快,因为只有在出现文件请求时才会解压缩压缩文件的所需部分。在常规解压缩中,首先解压缩所有数据。让我们看看如何使用 squashfs。

准备工作

如果您有 Ubuntu CD,只需在CDRom ROOT/casper/filesystem.squashfs中找到.squashfs文件。squashfs内部使用压缩算法,如gziplzmasquashfs支持在所有最新的 Linux 发行版中都可用。但是,为了创建squashfs文件,需要从软件包管理器安装额外的软件包squashfs-tools

如何做...

为了通过添加源目录和文件创建squashfs文件,请使用:

$ mksquashfs SOURCES compressedfs.squashfs

源可以是通配符、文件或文件夹路径。

例如:

$ sudo mksquashfs /etc test.squashfs
Parallel mksquashfs: Using 2 processors
Creating 4.0 filesystem on test.squashfs, block size 131072.
[=======================================] 1867/1867 100%

More details will be printed on terminal. They are limited to save space

为了将squashfs文件挂载到挂载点,使用回环挂载如下:

# mkdir /mnt/squash
# mount -o loop compressedfs.squashfs /mnt/squash

您可以通过访问/mnt/squashfs来复制内容。

还有更多...

可以通过指定附加参数来创建squashfs文件系统。让我们看看附加选项。

在创建 squashfs 文件时排除文件

在创建squashfs文件时,可以使用通配符指定要排除的文件或文件模式的列表。

通过使用-e选项,可以排除作为命令行参数指定的文件列表。例如:

$ sudo mksquashfs /etc test.squashfs -e /etc/passwd /etc/shadow

-e选项用于排除passwdshadow文件。

也可以使用-ef指定要排除的文件列表。

$ cat excludelist
/etc/passwd
/etc/shadow

$ sudo mksquashfs /etc test.squashfs -ef excludelist

如果我们想在排除列表中支持通配符,请使用-wildcard作为参数。

加密工具和哈希

加密技术主要用于保护数据免受未经授权的访问。有许多可用的算法,我们使用一组常见的标准算法。在 Linux 环境中有一些可用的工具用于执行加密和解密。有时我们使用加密算法哈希来验证数据完整性。本节将介绍一些常用的加密工具和这些工具可以处理的一般算法。

如何做...

让我们看看如何使用诸如 crypt、gpg、base64、md5sum、sha1sum 和 openssl 等工具:

  • crypt

crypt命令是一个简单的加密实用程序,它从stdin获取文件和密码作为输入,并将加密数据输出到stdout

$ crypt <input_file> output_file
Enter passphrase:

它将交互式地要求输入密码。我们也可以通过命令行参数提供密码。

$ crypt PASSPHRASE < input_file > encrypted_file

要解密文件,请使用:

$ crypt PASSPHRASE -d < encrypted_file > output_file

  • gpg(GNU 隐私保护)

gpg(GNU 隐私保护)是一种广泛使用的加密方案,用于通过密钥签名技术保护文件,使得只有认证目标才能访问数据。gpg 签名非常有名。gpg 的详细信息超出了本书的范围。在这里,我们可以学习如何加密和解密文件。

要使用gpg加密文件,请使用:

$ gpg -c filename

此命令交互式地读取密码并生成filename.gpg

要解密gpg文件,请使用:

$ gpg filename.gpg

此命令读取密码并解密文件。

  • Base64

Base64 是一组类似的编码方案,通过将二进制数据转换为基 64 表示形式的 ASCII 字符串格式来表示二进制数据。base64命令可用于编码和解码 Base64 字符串。

为了将二进制文件编码为 Base64 格式,请使用:

$ base64 filename > outputfile

或:

$ cat file | base64 > outputfile

它可以从stdin读取。

按照以下方式解码 Base64 数据:

$ base64 -d file > outputfile

或:

$ cat base64_file | base64 -d > outputfile

  • md5sumsha1sum

md5sumsha1sum 是单向哈希算法,无法逆转为原始数据。通常用于验证数据的完整性或从给定数据生成唯一密钥。对于每个文件,它通过分析其内容生成一个唯一密钥。

$ md5sum file
8503063d5488c3080d4800ff50850dc9  file

$ sha1sum file
1ba02b66e2e557fede8f61b7df282cd0a27b816b  file

这些类型的哈希适合于存储密码。密码以其哈希形式存储。当用户要进行身份验证时,密码被读取并转换为哈希。然后将哈希与已存储的哈希进行比较。如果它们相同,密码得到验证并提供访问权限,否则拒绝。存储原始密码字符串是有风险的,并会暴露密码的安全风险。

  • 类似影子的哈希(盐哈希)

让我们看看如何生成类似影子的盐哈希密码。

Linux 中的用户密码以其哈希形式存储在/etc/shadow文件中。/etc/shadow中的典型行看起来像这样:

test:$6$fG4eWdUi$ohTKOlEUzNk77.4S8MrYe07NTRV4M3LrJnZP9p.qc1bR5c.EcOruzPXfEu1uloBFUa18ENRH7F70zhodas3cR.:14790:0:99999:7:::

在这行中$6$fG4eWdUi$ohTKOlEUzNk77.4S8MrYe07NTRV4M3LrJnZP9p.qc1bR5c.EcOruzPXfEu1uloBFUa18ENRH7F70zhodas3cR是与其密码对应的影子哈希。

在某些情况下,我们可能需要编写关键的管理脚本,可能需要使用 shell 脚本手动编辑密码或添加用户。在这种情况下,我们必须生成一个影子密码字符串,并写一个类似上面的行到影子文件中。让我们看看如何使用openssl生成影子密码。

影子密码通常是盐密码。SALT是一个额外的字符串,用于混淆和加强加密。盐由随机位组成,用作生成密码的盐哈希的输入之一。

有关盐的更多细节,请参阅维基百科页面en.wikipedia.org/wiki/Salt_(cryptography)

$ openssl passwd -1 -salt SALT_STRING PASSWORD
$1$SALT_STRING$323VkWkSLHuhbt1zkSsUG.

用随机字符串替换SALT_STRING,用要使用的密码替换PASSWORD

使用 rsync 备份快照

备份数据是大多数系统管理员需要定期执行的操作。我们可能需要在 Web 服务器或远程位置备份数据。rsync是一个可以用于将文件和目录从一个位置同步到另一个位置的命令,同时最小化数据传输,使用文件差异计算和压缩。rsync相对于cp命令的优势在于rsync使用强大的差异算法。此外,它支持网络数据传输。在复制文件时,它会比较原始位置和目标位置的文件,并只复制更新的文件。它还支持压缩、加密等等。让我们看看如何使用rsync

如何做到...

为了将源目录复制到目的地(创建镜像),使用以下命令:

$ rsync -av source_path destination_path

在这个命令中:

  • -a代表存档

  • -v(详细)在stdout上打印详细信息或进度

上述命令将递归地将所有文件从源路径复制到目标路径。我们可以指定远程或本地主机路径。

它可以是格式为/home/slynux/dataslynux@192.168.0.6:/home/backups/data,等等。

/home/slynux/data指定了在执行rsync命令的机器上的绝对路径。slynux@192.168.0.6:/home/backups/data指定了 IP 地址为192.168.0.6的机器上的路径为/home/backups/data,并以用户slynux登录。

为了将数据备份到远程服务器或主机,请使用:

$ rsync -av source_dir username@host:PATH

为了在目的地保持镜像,以规律的间隔运行相同的rsync命令。它只会将更改的文件复制到目的地。

从远程主机恢复数据到localhost如下:

$ rsync -av username@host:PATH destination

rsync命令使用 SSH 连接到另一台远程计算机。在格式user@host中提供远程计算机地址,其中 user 是用户名,host 是附加到远程计算机的 IP 地址或域名。PATH是需要复制数据的绝对路径地址。rsync将像往常一样要求用户密码以进行 SSH 逻辑。这可以通过使用 SSH 密钥来自动化(避免用户密码探测)。

确保远程计算机上安装并运行 OpenSSH。

通过网络传输时压缩数据可以显著优化传输速度。我们可以使用rsync选项-z来指定在通过网络传输时压缩数据。例如:

$ rsync -avz source destination

注意

对于 PATH 格式,如果我们在源路径的末尾使用/rsync将把指定在source_path中的末尾目录的内容复制到目的地。

如果源路径末尾没有/rsync将复制该末尾目录本身到目的地。

例如,以下命令复制了test目录的内容:

$ rsync -av /home/test/ /home/backups

以下命令将test目录复制到目的地:

$ rsync -av /home/test /home/backups

注意

如果destination_path末尾有/rsync将把源复制到目的地目录。

如果目的地路径末尾没有使用/rsync将在目的地路径的末尾创建一个文件夹,该文件夹的名称与源目录类似,并将源复制到该目录中。

例如:

$ rsync -av /home/test /home/backups/

此命令将源(/home/test)复制到名为backups的现有文件夹。

$ rsync -av /home/test /home/backups

此命令通过创建目录将源(/home/test)复制到名为backups的目录中。

还有更多...

rsync命令具有几个额外的功能,可以使用其命令行选项来指定。让我们逐个了解。

在使用 rsync 进行归档时排除文件

有些文件在归档到远程位置时不需要更新。可以告诉rsync从当前操作中排除某些文件。文件可以通过两个选项来排除:

--exclude PATTERN

我们可以指定要排除的文件的通配符模式。例如:

$ rsync -avz /home/code/some_code /mnt/disk/backup/code --exclude "*.txt"

此命令排除了.txt文件的备份。

或者,我们可以通过提供一个文件列表来指定要排除的文件。

使用--exclude-from FILEPATH

在更新 rsync 备份时删除不存在的文件

我们将文件存档为 tarball 并将 tarball 传输到远程备份位置。当我们需要更新备份数据时,我们再次创建一个 TAR 文件并将文件传输到备份位置。默认情况下,如果目的地不存在源文件,rsync不会从目的地删除文件。为了从目的地删除在源文件中不存在的文件,请使用rsync --delete选项:

$ rsync -avz SOURCE DESTINATION --delete

在间隔时间安排备份

您可以创建一个 cron 作业以规律的间隔安排备份。

示例如下:

$ crontab -e

添加以下行:

0 */10 * * * rsync -avz /home/code user@IP_ADDRESS:/home/backups

上述crontab条目安排每 10 小时执行一次rsync

*/10crontab语法的小时位置。/10指定每 10 小时执行一次备份。如果*/10写在分钟位置,它将每 10 分钟执行一次。

查看第九章中的Scheduling with cron配方,了解如何配置crontab

基于 Git 的版本控制备份

人们在备份数据时使用不同的策略。差异备份比将整个源目录的副本复制到目标备份目录更有效,使用日期或当天时间的版本号。这会造成空间的浪费。我们只需要复制从第二次备份发生以来发生的文件更改。这称为增量备份。我们可以使用rsync等工具手动创建增量备份。但是恢复这种备份可能会很困难。保持和恢复更改的最佳方法是使用版本控制系统。它们在软件开发和代码维护中被广泛使用,因为编码经常发生变化。Git(GNU it)是一个非常著名且最有效的版本控制系统。让我们在非编程环境中使用 Git 备份常规文件。Git 可以通过您的发行版软件包管理器安装。它是由 Linus Torvalds 编写的。

准备工作

这是问题陈述:

我们有一个包含多个文件和子目录的目录。我们需要跟踪目录内容发生的更改并对其进行备份。如果数据损坏或丢失,我们必须能够恢复该数据的以前副本。我们需要定期将数据备份到远程机器。我们还需要在同一台机器(本地主机)的不同位置进行备份。让我们看看如何使用 Git 来实现它。

如何做…

在要备份的目录中使用:

$ cd /home/data/source

让源目录被跟踪。

设置并初始化远程备份目录。在远程机器上,创建备份目标目录:

$ mkdir -p /home/backups/backup.git

$ cd /home/backups/backup.git

$ git init --bare

以下步骤需要在源主机机器上执行:

  1. 在源主机机器上将用户详细信息添加到 Git 中:
$ git config --global user.name  "Sarath Lakshman"
#Set user name to "Sarath Lakshman"

$ git config --global user.email slynux@slynux.com
# Set email to slynux@slynux.com

从主机机器初始化要备份的源目录。在要备份其文件的主机机器中的源目录中,执行以下命令:

$ git init
Initialized empty Git repository in /home/backups/backup.git/
# Initialize git repository

$ git commit --allow-empty -am "Init"
[master (root-commit) b595488] Init

  1. 在源目录中,执行以下命令以添加远程 git 目录并同步备份:
$ git remote add origin user@remotehost:/home/backups/backup.git

$ git push origin master
Counting objects: 2, done.
Writing objects: 100% (2/2), 153 bytes, done.
Total 2 (delta 0), reused 0 (delta 0)
To user@remotehost:/home/backups/backup.git
 * [new branch]      master -> master

  1. 添加或删除 Git 跟踪的文件。

以下命令将当前目录中的所有文件和文件夹添加到备份列表中:

$ git add *

我们可以有条件地将某些文件添加到备份列表中,方法如下:

$ git add *.txt
$ git add *.py

通过使用以下命令,我们可以删除不需要跟踪的文件和文件夹:

$ git rm file

它可以是一个文件夹,甚至是一个通配符,如下所示:

$ git rm *.txt

  1. 检查点或标记备份点。

我们可以使用以下命令为备份标记检查点并附上消息:

$ git commit -m "Commit Message"

我们需要定期更新远程位置的备份。因此,设置一个 cron 作业(例如,每五个小时备份一次)。

创建包含以下行的 crontab 条目文件:

0 */5 * * *  /home/data/backup.sh

创建脚本/home/data/backup.sh如下:

#!/bin/ bash
cd /home/data/source
git add .
git commit -am "Commit - @ $(date)"
git push

现在我们已经设置好了备份系统。

  1. 使用 Git 恢复数据。

为了查看所有备份版本,请使用:

$ git log

通过忽略任何最近的更改,将当前目录更新到上次备份。

  • 要恢复到任何以前的状态或版本,请查看提交 ID,这是一个 32 个字符的十六进制字符串。使用提交 ID 和git checkout

  • 对于提交 ID 3131f9661ec1739f72c213ec5769bc0abefa85a9,它将是:

$ git checkout 3131f9661ec1739f72c213ec5769bc0abefa85a9
$ git commit -am "Restore @ $(date) commit ID: 3131f9661ec1739f72c213ec5769bc0abefa85a9"
$ git push

  • 为了再次查看版本的详细信息,请使用:
$ git log

如果工作目录由于某些问题而损坏,我们需要使用远程位置的备份来修复目录。

然后我们可以按以下方式从远程位置重新创建内容:

$ git clone user@remotehost:/home/backups/backup.git

这将创建一个包含所有内容的目录备份。

使用 dd 克隆硬盘和磁盘

在处理硬盘和分区时,我们可能需要创建副本或备份完整分区,而不是复制所有内容(不仅是硬盘分区,还包括复制整个硬盘而不丢失任何信息,如引导记录、分区表等)。在这种情况下,我们可以使用dd命令。它可以用于克隆任何类型的磁盘,如硬盘、闪存驱动器、CD、DVD、软盘等。

准备工作

dd命令扩展到数据定义。由于不正确的使用会导致数据丢失,因此它被昵称为“数据销毁者”。在使用参数顺序时要小心。错误的参数可能导致整个数据丢失或变得无用。dd基本上是一个比特流复制器,它将整个比特流从磁盘复制到文件或从文件复制到磁盘。让我们看看如何使用dd

如何做...

dd的语法如下:

$ dd if=SOURCE of=TARGET bs=BLOCK_SIZE count=COUNT

在这个命令中:

  • if代表输入文件或输入设备路径

  • of代表目标文件或目标设备路径

  • bs代表块大小(通常以 2 的幂给出,例如 512、1024、2048 等)。COUNT是要复制的块数(一个整数)。

总字节数=块大小*计数

bscount是可选的。

通过指定COUNT,我们可以限制从输入文件复制到目标的字节数。如果未指定COUNTdd将从输入文件复制,直到达到文件的结尾(EOF)标记。

为了将分区复制到文件中使用:

# dd if=/dev/sda1 of=sda1_partition.img

这里/dev/sda1是分区的设备路径。

使用备份还原分区如下:

# dd if=sda1_partition.img of=/dev/sda1

您应该小心处理参数ifof。不正确的使用可能会导致数据丢失。

通过将设备路径/dev/sda1更改为适当的设备路径,可以复制或还原任何磁盘。

为了永久删除分区中的所有数据,我们可以使用以下命令使dd将零写入分区:

# dd if=/dev/zero of=/dev/sda1

/dev/zero是一个字符设备。它总是返回无限的零'\0'字符。

将一个硬盘克隆到另一个相同大小的硬盘如下:

# dd if=/dev/sda  of=/dev/sdb

这里/dev/sdb是第二个硬盘。

为了使用 CD ROM(ISO 文件)的映像文件使用:

# dd if=/dev/cdrom of=cdrom.iso

还有更多...

当在使用dd生成的文件中创建文件系统时,我们可以将其挂载到挂载点。让我们看看如何处理它。

挂载映像文件

使用dd创建的任何文件映像都可以使用回环方法挂载。使用mount命令和-o loop

# mkdir /mnt/mount_point
# mount -o loop file.img /mnt/mount_point

现在我们可以通过位置/mnt/mount_point访问映像文件的内容。

参见

  • 创建 ISO 文件,混合 ISO 第三章解释了如何使用 dd 从 CD 创建 ISO 文件

第七章:老男孩网络

在本章中,我们将涵盖:

  • 基本网络入门

  • 让我们 ping 一下!

  • 列出网络上所有活动的机器

  • 通过网络传输文件

  • 使用脚本设置以太网和无线局域网

  • 使用 SSH 进行无密码自动登录

  • 使用 SSH 在远程主机上运行命令

  • 在本地挂载点挂载远程驱动器

  • 在网络上多播窗口消息

  • 网络流量和端口分析

介绍

网络是通过网络互连机器并配置网络中的节点具有不同规格的行为。我们使用 TCP/IP 作为我们的网络堆栈,并且所有操作都基于它。网络是每个计算机系统的重要组成部分。网络中连接的每个节点都被分配一个唯一的 IP 地址以进行标识。网络中有许多参数,如子网掩码、路由、端口、DNS 等,需要基本的理解才能跟进。

许多使用网络的应用程序通过打开和连接到防火墙端口来运行。每个应用程序可能提供诸如数据传输、远程 shell 登录等服务。在由许多机器组成的网络上可以执行许多有趣的管理任务。Shell 脚本可用于配置网络中的节点、测试机器的可用性、自动执行远程主机上的命令等。本章重点介绍了介绍有趣的与网络相关的工具或命令的不同配方,以及它们如何用于解决不同的问题。

基本网络入门

在基于网络的配方之前,您有必要对设置网络、术语和命令进行基本了解,例如分配 IP 地址、添加路由等。本配方将概述 GNU/Linux 中用于网络的不同命令及其用法。

准备工作

网络中的每个节点都需要分配许多参数才能成功工作并与其他机器互连。一些不同的参数包括 IP 地址、子网掩码、网关、路由、DNS 等。

本配方将介绍ifconfigroutenslookuphost命令。

如何做...

网络接口用于连接到网络。通常,在类 UNIX 操作系统的上下文中,网络接口遵循 eth0、eth1 的命名约定。此外,还有其他接口,如 usb0、wlan0 等,可用于 USB 网络接口、无线局域网等网络。

ifconfig是用于显示网络接口、子网掩码等详细信息的命令。

ifconfig位于/sbin/ifconfig。当键入ifconfig时,一些 GNU/Linux 发行版会显示错误“找不到命令”。这是因为用户的 PATH 环境变量中没有包含/sbin。当键入命令时,Bash 会在 PATH 变量中指定的目录中查找。

默认情况下,在 Debian 中,ifconfig不可用,因为/sbin不在 PATH 中。

/sbin/ifconfig是绝对路径,因此尝试使用绝对路径(即/sbin/ifconfig)运行 ifconfig。对于每个系统,默认情况下都会有一个名为'lo'的接口,称为环回,指向当前机器。例如:

$ ifconfig
lo        Link encap:Local Loopback
inet addr:127.0.0.1  Mask:255.0.0.0
inet6addr: ::1/128 Scope:Host
 UP LOOPBACK RUNNING  MTU:16436  Metric:1
 RX packets:6078 errors:0 dropped:0 overruns:0 frame:0
 TX packets:6078 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
 RX bytes:634520 (634.5 KB)  TX bytes:634520 (634.5 KB)

wlan0     Link encap:EthernetHWaddr 00:1c:bf:87:25:d2
inet addr:192.168.0.82  Bcast:192.168.3.255  Mask:255.255.252.0
inet6addr: fe80::21c:bfff:fe87:25d2/64 Scope:Link
 UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
 RX packets:420917 errors:0 dropped:0 overruns:0 frame:0
 TX packets:86820 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
 RX bytes:98027420 (98.0 MB)  TX bytes:22602672 (22.6 MB)

ifconfig输出中最左边的列显示了网络接口的名称,右侧列显示了与相应网络接口相关的详细信息。

还有更多...

有几个附加命令经常用于查询和配置网络。让我们一起了解基本命令和用法。

打印网络接口列表

以下是一个一行命令序列,用于打印系统上可用的网络接口列表。

$ ifconfig | cut -c-10 | tr -d ' ' | tr -s '\n'
lo
wlan0

ifconfig输出的每行的前 10 个字符用于写入网络接口的名称。因此,我们使用cut提取每行的前 10 个字符。tr -d ' '删除每行中的每个空格字符。现在使用tr -s '\n'来压缩\n换行符,以产生一个接口名称列表。

分配和显示 IP 地址

ifconfig命令显示系统上可用的每个网络接口的详细信息。但是,我们可以通过使用以下命令将其限制为特定接口:

$ ifconfig iface_name

例如:

$ ifconfig wlan0
wlan0     Link encap:Ethernet HWaddr 00:1c:bf:87:25:d2
inet addr:192.168.0.82  Bcast:192.168.3.255
 Mask:255.255.252.0

从前面提到的命令的输出中,我们感兴趣的是 IP 地址、广播地址、硬件地址和子网掩码。它们如下:

  • HWaddr 00:1c:bf:87:25:d2是硬件地址(MAC 地址)

  • inet addr:192.168.0.82是 IP 地址

  • Bcast:192.168.3.255是广播地址

  • 子网掩码:255.255.252.0

在几种脚本上下文中,我们可能需要从脚本中提取这些地址中的任何一个以进行进一步操作。

提取 IP 地址是一个常见的任务。为了从ifconfig输出中提取 IP 地址,请使用:

$ ifconfig wlan0 | egrep -o "inet addr:[^ ]*" | grep -o "[0-9.]*"
192.168.0.82

第一个命令egrep -o "inet addr:[^ ]*"将打印inet addr:192.168.0.82

模式以inet addr:开头,以一些非空格字符序列(由[^ ]*指定)结尾。现在在下一个管道中,它打印数字和'.'的字符组合。

为了设置网络接口的 IP 地址,请使用:

# ifconfig wlan0 192.168.0.80

您需要以 root 身份运行上述命令。192.168.0.80是要设置的地址。

设置子网掩码以及 IP 地址如下:

# ifconfig wlan0 192.168.0.80  netmask 255.255.252.0

欺骗硬件地址(MAC 地址)

在某些情况下,通过使用硬件地址提供对网络上计算机的身份验证或过滤,我们可以使用硬件地址欺骗。硬件地址显示为ifconfig输出中的HWaddr 00:1c:bf:87:25:d2

我们可以在软件级别欺骗硬件地址,如下所示:

# ifconfig eth0 hw ether 00:1c:bf:87:25:d5

在上述命令中,00:1c:bf:87:25:d5是要分配的新 MAC 地址。

当我们需要通过 MAC 认证的服务提供商访问互联网以为单个机器提供互联网访问时,这可能是有用的。

名称服务器和 DNS(域名服务)

互联网的基本寻址方案是 IP 地址(点分十进制形式,例如202.11.32.75)。但是,互联网上的资源(例如网站)是通过称为 URL 或域名的 ASCII 字符组合访问的。例如,google.com是一个域名。它实际上对应一个 IP 地址。在浏览器中输入 IP 地址也可以访问 URLwww.google.com

将 IP 地址与符号名称抽象化的技术称为域名服务DNS)。当我们输入google.com时,配置了我们网络的 DNS 服务器将域名解析为相应的 IP 地址。而在本地网络上,我们可以使用本地 DNS 通过主机名对网络上的本地机器进行符号命名。

分配给当前系统的名称服务器可以通过读取/etc/resolv.conf来查看。例如:

$ cat /etc/resolv.conf
nameserver 8.8.8.8

我们可以手动添加名称服务器,如下所示:

# echo nameserver IP_ADDRESS >> /etc/resolv.conf

我们如何获取相应域名的 IP 地址?

获取 IP 地址的最简单方法是尝试 ping 给定的域名,并查看回显回复。例如:

$ ping google.com
PING google.com (64.233.181.106) 56(84) bytes of data.
Here 64.233.181.106 is the corresponding IP address.

一个域名可以分配多个 IP 地址。在这种情况下,DNS 服务器将从 IP 地址列表中返回一个地址。要获取分配给域名的所有地址,我们应该使用 DNS 查找实用程序。

DNS 查找

命令行中有不同的 DNS 查找实用程序可用。这些将请求 DNS 服务器进行 IP 地址解析。hostnslookup是两个 DNS 查找实用程序。

当执行host时,它将列出附加到域名的所有 IP 地址。nslookup是另一个类似于host的命令,它可以用于查询与 DNS 和名称解析相关的详细信息。例如:

$ host google.com
google.com has address 64.233.181.105
google.com has address 64.233.181.99
google.com has address 64.233.181.147
google.com has address 64.233.181.106
google.com has address 64.233.181.103
google.com has address 64.233.181.104

它还可以列出 DNS 资源记录,如 MX(邮件交换器)如下:

$ nslookup google.com
Server:    8.8.8.8
Address:  8.8.8.8#53

Non-authoritative answer:
Name:  google.com
Address: 64.233.181.105
Name:  google.com
Address: 64.233.181.99
Name:  google.com
Address: 64.233.181.147
Name:  google.com
Address: 64.233.181.106
Name:  google.com
Address: 64.233.181.103
Name:  google.com
Address: 64.233.181.104

Server:    8.8.8.8

上面的最后一行对应于用于 DNS 解析的默认名称服务器。

在不使用 DNS 服务器的情况下,可以通过向文件/etc/hosts添加条目来将符号名称添加到 IP 地址解析中。

要添加一个条目,请使用以下语法:

# echo IP_ADDRESS symbolic_name >> /etc/hosts

例如:

# echo 192.168.0.9 backupserver.com  >> /etc/hosts

添加了这个条目后,每当解析到backupserver.com时,它将解析为192.168.0.9

设置默认网关,显示路由表信息

当本地网络连接到另一个网络时,需要分配一些机器或网络节点,通过这些节点进行互连。因此,目的地在本地网络之外的 IP 数据包应该被转发到与外部网络相连的节点机器。这个特殊的节点机器,能够将数据包转发到外部网络,被称为网关。我们为每个节点设置网关,以便连接到外部网络。

操作系统维护一个称为路由表的表,其中包含有关数据包如何转发以及通过网络中的哪个机器节点的信息。路由表可以显示如下:

$ route
Kernel IP routing table
Destination      Gateway   Genmask      Flags  Metric  Ref  UseIface
192.168.0.0         *      255.255.252.0  U     2      0     0wlan0
link-local          *      255.255.0.0    U     1000   0     0wlan0
default          p4.local  0.0.0.0        UG    0      0     0wlan0

或者,您也可以使用:

$ route -n
Kernel IP routing table
Destination   Gateway      Genmask       Flags Metric Ref  Use   Iface
192.168.0.0   0.0.0.0      255.255.252.0   U     2     0     0   wlan0
169.254.0.0   0.0.0.0      255.255.0.0     U     1000  0     0   wlan0
0.0.0.0       192.168.0.4  0.0.0.0         UG    0     0     0   wlan0

使用-n指定显示数字地址。当使用-n时,它将显示每个带有数字 IP 地址的条目,否则它将显示 DNS 条目下的符号主机名而不是 IP 地址。

默认网关设置如下:

# route add default gw IP_ADDRESS INTERFACE_NAME

例如:

# route add default gw 192.168.0.1 wlan0

Traceroute

当应用程序通过 Internet 请求服务时,服务器可能位于遥远的位置,并通过任意数量的网关或设备节点连接。数据包通过多个网关传输并到达目的地。有一个有趣的命令traceroute,它显示了数据包到达目的地所经过的所有中间网关的地址。traceroute信息帮助我们了解每个数据包需要经过多少跳才能到达目的地。中间网关或路由器的数量为连接在大型网络中的两个节点之间的距离提供了一个度量标准。traceroute的输出示例如下:

$ traceroute google.com
traceroute to google.com (74.125.77.104), 30 hops max, 60 byte packets
1  gw-c6509.lxb.as5577.net (195.26.4.1)  0.313 ms  0.371 ms  0.457 ms
2  40g.lxb-fra.as5577.net (83.243.12.2)  4.684 ms  4.754 ms  4.823 ms
3  de-cix10.net.google.com (80.81.192.108)  5.312 ms  5.348 ms  5.327 ms
4  209.85.255.170 (209.85.255.170)  5.816 ms  5.791 ms 209.85.255.172 (209.85.255.172)  5.678 ms
5  209.85.250.140 (209.85.250.140)  10.126 ms  9.867 ms  10.754 ms
6  64.233.175.246 (64.233.175.246)  12.940 ms 72.14.233.114 (72.14.233.114)  13.736 ms  13.803 ms
7  72.14.239.199 (72.14.239.199)  14.618 ms 209.85.255.166 (209.85.255.166)  12.755 ms 209.85.255.143 (209.85.255.143)  13.803 ms
8  209.85.255.98 (209.85.255.98)  22.625 ms 209.85.255.110 (209.85.255.110)  14.122 ms
* 
9  ew-in-f104.1e100.net (74.125.77.104)  13.061 ms  13.256 ms  13.484 ms

另请参阅

  • 使用变量和环境变量玩耍 第一章 ,解释了 PATH 变量

  • 使用 grep 在文件中搜索和挖掘“文本” 第四章 ,解释了 grep 命令

让我们 ping!

ping是最基本的网络命令,每个用户都应该首先了解。它是一个通用命令,在主要操作系统上都可以使用。它也是一个用于验证网络上两个主机之间连接性的诊断工具。它可以用来查找网络上哪些机器是活动的。让我们看看如何使用 ping。

如何做...

为了检查网络上两个主机的连接性,ping命令使用Internet Control Message Protocol (ICMP)回显数据包。当这些回显数据包发送到主机时,如果主机是可达或活动的,主机将以回复的方式响应。

检查主机是否可达如下:

$ ping ADDRESS

ADDRESS可以是主机名、域名或 IP 地址本身。

ping将持续发送数据包,并且回复信息将打印在终端上。通过按下Ctrl + C来停止 ping。

例如:

  • 当主机是可达时,输出将类似于以下内容:
$ ping 192.168.0.1 
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=1.44 ms
^C 
--- 192.168.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.440/1.440/1.440/0.000 ms

$ ping google.com
PING google.com (209.85.153.104) 56(84) bytes of data.
64 bytes from bom01s01-in-f104.1e100.net (209.85.153.104): icmp_seq=1 ttl=53 time=123 ms
^C 
--- google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 123.388/123.388/123.388/0.000 ms

  • 当主机不可达时,输出将类似于:
$ ping 192.168.0.99
PING 192.168.0.99 (192.168.0.99) 56(84) bytes of data.
From 192.168.0.82 icmp_seq=1 Destination Host Unreachable
From 192.168.0.82 icmp_seq=2 Destination Host Unreachable

一旦主机不可达,ping 将返回Destination Host Unreachable错误消息。

有更多

除了检查网络中两点之间的连接性外,ping命令还可以与其他选项一起使用以获得有用的信息。让我们看看ping的其他选项。

往返时间

ping命令可用于查找网络上两个主机之间的往返时间RTT)。 RTT 是数据包到达目标主机并返回到源主机所需的时间。 RTT 以毫秒为单位可以从 ping 中获得。示例如下:

--- google.com ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4000ms
rtt min/avg/max/mdev = 118.012/206.630/347.186/77.713 ms

这里最小的 RTT 为 118.012ms,平均 RTT 为 206.630ms,最大 RTT 为 347.186ms。 ping输出中的mdev(77.713ms)参数代表平均偏差。

限制要发送的数据包数量

ping命令发送回显数据包,并无限期等待echo的回复,直到通过按下Ctrl + C停止。但是,我们可以使用-c标志来限制要发送的回显数据包的数量。

用法如下:

-c COUNT

例如:

$ ping 192.168.0.1 -c 2 
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data. 
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=4.02 ms 
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=1.03 ms 

--- 192.168.0.1 ping statistics --- 
2 packets transmitted, 2 received, 0% packet loss, time 1001ms 
rtt min/avg/max/mdev = 1.039/2.533/4.028/1.495 ms 

在上一个示例中,ping命令发送两个回显数据包并停止。

当我们需要通过脚本 ping 多台机器并检查其状态时,这是非常有用的。

ping 命令的返回状态

ping命令在成功时返回退出状态 0,并在失败时返回非零。成功意味着目标主机是可达的,而失败意味着目标主机是不可达的。

返回状态可以轻松获得如下:

$ ping ADDRESS -c2
if [ $? -eq 0 ];
then
 echo Successful ;
else
 echo Failure
fi

列出网络上所有活动的机器

当我们处理大型局域网时,我们可能需要检查网络中其他机器的可用性,无论是存活还是不存活。机器可能不存活有两种情况:要么它没有通电,要么由于网络中的问题。通过使用 shell 脚本,我们可以轻松地找出并报告网络上哪些机器是活动的。让我们看看如何做到这一点。

准备就绪

在这个配方中,我们使用了两种方法。第一种方法使用ping,第二种方法使用fpingfping不会默认随 Linux 发行版一起提供。您可能需要使用软件包管理器手动安装fping

如何做到...

让我们通过脚本找出网络上所有活动的机器以及查找相同的替代方法。

  • 方法 1:

我们可以使用ping命令编写自己的脚本来查询 IP 地址列表,并检查它们是否存活,如下所示:

#!/bin/bash
#Filename: ping.sh
# Change base address 192.168.0 according to your network.

for ip in 192.168.0.{1..255} ;
do
  ping $ip -c 2 &> /dev/null ;

  if [ $? -eq 0 ];
  then
    echo $ip is alive
  fi

done

输出如下:

$ ./ping.sh
192.168.0.1 is alive
192.168.0.90 is alive

  • 方法 2:

我们可以使用现有的命令行实用程序来查询网络上计算机的状态,如下所示:

$ fping -a 192.160.1/24 -g 2> /dev/null 
192.168.0.1 
192.168.0.90

或者,使用:

$ fping -a 192.168.0.1 192.168.0.255 -g

它是如何工作的...

在方法 1 中,我们使用ping命令来查找网络上存活的计算机。我们使用for循环来遍历 IP 地址列表。列表生成为192.168.0.{1..255}{start..end}符号将扩展并生成 IP 地址列表,例如192.168.0.1192.168.0.2192.168.0.3直到192.168.0.255

ping $ip -c 2 &> /dev/null将在每次循环执行时对相应的 IP 地址运行ping-c 2用于限制要发送的回显数据包的数量为两个数据包。&> /dev/null用于将stderrstdout重定向到/dev/null,以便它不会打印在终端上。使用$?我们评估退出状态。如果成功,退出状态为 0,否则为非零。因此,成功的 IP 地址将被打印。我们还可以打印不成功的 IP 地址列表,以提供不可达的 IP 地址列表。

注意

这里有一个练习给你。不要在脚本中使用硬编码的 IP 地址范围,而是修改脚本以从文件或stdin中读取 IP 地址列表。

在这个脚本中,每个 ping 都是依次执行的。尽管所有 IP 地址彼此独立,但由于是顺序程序,ping命令会由于发送两个回显数据包和接收它们的延迟或执行下一个ping命令的超时而执行。

当涉及到 255 个地址时,延迟很大。让我们并行运行所有ping命令,使其更快。脚本的核心部分是循环体。为了使ping命令并行运行,将循环体括在( )&中。( )括起一组命令以作为子 shell 运行,&通过离开当前线程将其发送到后台。例如:

(
 ping $ip -c2 &> /dev/null ;

 if [ $? -eq 0 ];
 then
 echo $ip is alive
 fi
)&

wait

for循环体执行许多后台进程,退出循环并终止脚本。为了使脚本在所有子进程结束之前终止,我们有一个称为wait的命令。在脚本的末尾放置一个wait,以便它等待直到所有子( )子 shell 进程完成。

注意

wait命令使脚本只有在所有子进程或后台进程终止或完成后才能终止。

查看书中提供的代码中的fast_ping.sh

方法 2 使用了一个名为fping的不同命令。它可以同时 ping 一系列 IP 地址并非常快速地响应。fping可用的选项如下:

  • fping-a选项指定打印所有活动机器的 IP 地址

  • -u选项与fping一起指定打印所有不可达的机器

  • -g选项指定从斜杠子网掩码表示法指定的 IP/mask 或起始和结束 IP 地址生成 IP 地址范围:

$ fping -a 192.160.1/24 -g

$ fping -a 192.160.1 192.168.0.255 -g

  • 2>/dev/null用于将由于不可达主机而打印的错误消息转储到空设备

还可以手动指定 IP 地址列表作为命令行参数或通过stdin列表。例如:

$ fping -a 192.168.0.1 192.168.0.5 192.168.0.6
# Passes IP address as arguments
$ fping -a <ip.list
# Passes a list of IP addresses from a file

还有更多...

fping命令可用于从网络查询 DNS 数据。让我们看看如何做到这一点。

使用 fping 进行 DNS 查找

fping有一个-d选项,通过使用 DNS 查找返回主机名。它将在 ping 回复中打印主机名而不是 IP 地址。

$ cat ip.list
192.168.0.86
192.168.0.9
192.168.0.6

$ fping -a -d 2>/dev/null  <ip.list
www.local
dnss.local

另请参阅

  • 玩转文件描述符和重定向第一章,解释了数据重定向

  • 比较和测试第一章,解释数字比较

文件传输

计算机网络的主要目的是资源共享。在资源共享中,最突出的用途是文件共享。有多种方法可以在网络上的不同节点之间传输文件。本教程讨论了如何使用常用协议 FTP、SFTP、RSYNC 和 SCP 进行文件传输。

准备工作

执行网络文件传输的命令在 Linux 安装中通常是默认可用的。可以使用lftp命令通过 FTP 传输文件。可以使用sftp通过 SSH 连接传输文件,使用rsync命令和使用scp通过 SSH 进行传输。

如何做...

文件传输协议FTP)是一种用于在网络上的机器之间传输文件的旧文件传输协议。我们可以使用lftp命令来访问启用 FTP 的服务器进行文件传输。它使用端口 21。只有在远程机器上安装了 FTP 服务器才能使用 FTP。许多公共网站使用 FTP 共享文件。

要连接到 FTP 服务器并在其间传输文件,请使用:

$ lftp username@ftphost

现在它将提示输入密码,然后显示如下的登录提示:

lftp username@ftphost:~> 

您可以在此提示中键入命令。例如:

  • 要更改目录,请使用cd directory

  • 要更改本地机器的目录,请使用lcd

  • 要创建目录,请使用mkdir

  • 要下载文件,请使用get filename如下:

lftp username@ftphost:~> get filename

  • 要从当前目录上传文件,请使用put filename如下:
lftp username@ftphost:~> put filename

  • 可以使用quit命令退出lftp会话

lftp提示中支持自动完成。

还有更多...

让我们来看一些用于通过网络传输文件的其他技术和命令。

自动 FTP 传输

ftp是另一个用于基于 FTP 的文件传输的命令。lftp更灵活。lftpftp命令打开一个与用户的交互会话(它通过显示消息提示用户输入)。如果我们想要自动化文件传输而不是使用交互模式怎么办?我们可以通过编写一个 shell 脚本来自动化 FTP 文件传输,如下所示:

#!/bin/bash
#Filename: ftp.sh
#Automated FTP transfer
HOST='domain.com'
USER='foo'
PASSWD='password'
ftp -i -n $HOST <<EOF
user ${USER} ${PASSWD}
binary
cd /home/slynux
puttestfile.jpg
getserverfile.jpg
quit
EOF

上述脚本具有以下结构:

<<EOF
DATA
EOF

这用于通过stdin发送数据到 FTP 命令。第一章中的Playing with file descriptors and redirection一节解释了各种重定向到stdin的方法。

ftp-i选项关闭与用户的交互会话。user ${USER} ${PASSWD}设置用户名和密码。binary将文件模式设置为二进制。

SFTP(安全 FTP)

SFTP 是一种类似 FTP 的文件传输系统,运行在 SSH 连接的顶部。它利用 SSH 连接来模拟 FTP 接口。它不需要远程端安装 FTP 服务器来执行文件传输,但需要安装和运行 OpenSSH 服务器。它是一个交互式命令,提供sftp提示符。

以下命令用于执行文件传输。对于具有特定 HOST、USER 和 PASSWD 的每个自动化 FTP 会话,所有其他命令保持不变:

cd /home/slynux
put testfile.jpg
get serverfile.jpg

要运行sftp,请使用:

$ sftp user@domainname

类似于lftpsftp会话可以通过输入quit命令退出。

SSH 服务器有时不会在默认端口 22 上运行。如果它在不同的端口上运行,我们可以在sftp后面指定端口,如-oPort=PORTNO

例如:

$ sftp -oPort=422 user@slynux.org

注意

-oPort应该是sftp命令的第一个参数。

RSYNC

rsync 是一个重要的命令行实用程序,广泛用于在网络上复制文件和进行备份快照。这在单独的使用 rsync 进行备份快照一节中有更好的解释,解释了rsync的用法。

SCP(安全复制)

SCP 是一种比传统的名为rcp的远程复制工具更安全的文件复制技术。文件通过加密通道传输。SSH 用作加密通道。我们可以通过以下方式轻松地将文件传输到远程计算机:

$ scp filename user@remotehost:/home/path

这将提示输入密码。可以通过使用自动登录 SSH 技术使其无需密码。使用 SSH 进行无密码自动登录一节解释了 SSH 自动登录。

因此,使用scp进行文件传输不需要特定的脚本。一旦 SSH 登录被自动化,scp命令可以在不需要交互式提示输入密码的情况下执行。

这里的remotehost可以是 IP 地址或域名。scp命令的格式是:

$ scp SOURCE DESTINATION

SOURCEDESTINATION可以采用username@localhost:/path的格式,例如:

$ scp user@remotehost:/home/path/filename filename

上述命令将文件从远程主机复制到当前目录,并给出文件名。

如果 SSH 运行的端口与 22 不同,请使用与sftp相同语法的-oPort

使用 SCP 进行递归复制

通过使用scp,我们可以通过以下方式在网络上的两台计算机之间递归复制目录,使用-r参数:

$ scp -r /home/slynux user@remotehost:/home/backups
# Copies the directory /home/slynux recursively to remote location

scp也可以通过使用-p参数保留权限和模式来复制文件。

另请参阅

  • 第一章的Playing with file descriptors and redirection一节解释了使用 EOF 的标准输入

使用脚本设置以太网和无线局域网

以太网配置简单。由于它使用物理电缆,因此没有特殊要求,如身份验证。然而,无线局域网需要身份验证,例如 WEP 密钥以及要连接的无线网络的 ESSID。让我们看看如何通过编写一个 shell 脚本连接到无线网络和有线网络。

准备工作

连接有线网络时,我们需要使用ifconfig实用程序分配 IP 地址和子网掩码。但是对于无线网络连接,将需要额外的实用程序,如iwconfigiwlist,以配置更多参数。

如何做...

为了从有线接口连接到网络,执行以下脚本:

#!/bin/bash
#Filename: etherconnect.sh
#Description: Connect Ethernet

#Modify the parameters below according to your settings
######### PARAMETERS ###########

IFACE=eth0
IP_ADDR=192.168.0.5
SUBNET_MASK=255.255.255.0
GW=192.168.0.1
HW_ADDR='00:1c:bf:87:25:d2'
# HW_ADDR is optional
#################################

if [ $UID -ne 0 ];
then
  echo "Run as root"
  exit 1
fi

# Turn the interface down before setting new config
/sbin/ifconfig $IFACE down

if [[ -n $HW_ADDR  ]];
then
  /sbin/ifconfig hw ether $HW_ADDR
 echo Spoofed MAC ADDRESS to $HW_ADDR

fi

/sbin/ifconfig $IFACE $IP_ADDR netmask $SUBNET_MASK

route add default gw $GW $IFACE

echo Successfully configured $IFACE

连接到带有 WEP 的无线局域网的脚本如下:

#!/bin/bash
#Filename: wlan_connect.sh
#Description: Connect to Wireless LAN

#Modify the parameters below according to your settings
######### PARAMETERS ###########
IFACE=wlan0
IP_ADDR=192.168.1.5
SUBNET_MASK=255.255.255.0
GW=192.168.1.1
HW_ADDR='00:1c:bf:87:25:d2' 
#Comment above line if you don't want to spoof mac address

ESSID="homenet"
WEP_KEY=8b140b20e7 
FREQ=2.462G
#################################

KEY_PART=""

if [[ -n $WEP_KEY ]];
then
  KEY_PART="key $WEP_KEY"
fi

# Turn the interface down before setting new config
/sbin/ifconfig $IFACE down

if [ $UID -ne 0 ];
then
  echo "Run as root"
  exit 1;
fi

if [[ -n $HW_ADDR  ]];
then
  /sbin/ifconfig $IFACE hw ether $HW_ADDR
  echo Spoofed MAC ADDRESS to $HW_ADDR
fi

/sbin/iwconfig $IFACE essid $ESSID $KEY_PART freq $FREQ

/sbin/ifconfig $IFACE $IP_ADDR netmask $SUBNET_MASK

route add default gw $GW $IFACE

echo Successfully configured $IFACE

它是如何工作的...

命令ifconfigiwconfigroute需要以 root 用户身份运行。因此,在脚本开始时会检查 root 用户。

以太网连接脚本非常简单,并且使用了食谱中解释的概念,基本网络入门。让我们来看看用于连接到无线局域网的命令。

无线局域网需要一些参数,如essidkey和频率才能连接到网络。essid是我们需要连接的无线网络的名称。一些有线 等效 协议WEP)网络使用 WEP 密钥进行身份验证,而有些网络则不使用。WEP 密钥通常是一个 10 位十六进制密码。接下来是分配给网络的频率。iwconfig是用于将无线网卡连接到适当的无线网络、WEP 密钥和频率的命令。

我们可以使用实用程序iwlist扫描和列出可用的无线网络。要进行扫描,请使用以下命令:

# iwlist scan 
wlan0     Scan completed : 
 Cell 01 - Address: 00:12:17:7B:1C:65
 Channel:11
 Frequency:2.462 GHz (Channel 11) 
 Quality=33/70  Signal level=-77 dBm
 Encryption key:on
 ESSID:"model-2" 

频率参数可以从扫描结果中提取,从Frequency:2.462 GHz (Channel 11)这一行中。

另请参阅

  • 比较和测试第一章中,解释了字符串比较。

使用 SSH 进行无密码自动登录

SSH 在自动化脚本中被广泛使用。通过使用 SSH,可以在远程主机上远程执行命令并读取它们的输出。SSH 通过使用用户名和密码进行身份验证。在执行 SSH 命令时会提示输入密码。但是在自动化脚本中,SSH 命令可能会在循环中执行数百次,因此每次提供密码是不切实际的。因此我们需要自动化登录。SSH 具有一个内置功能,可以使用 SSH 密钥进行自动登录。本食谱描述了如何创建 SSH 密钥并实现自动登录。

如何做...

SSH 使用基于公钥和私钥的加密技术进行自动身份验证。身份验证密钥有两个元素:公钥和私钥对。我们可以使用ssh-keygen命令创建一个身份验证密钥。为了自动化身份验证,公钥必须放置在服务器上(通过将公钥附加到~/.ssh/authorized_keys文件),并且其配对的私钥文件应该存在于客户端机器的~/.ssh目录中,即您要从中登录的计算机。关于 SSH 的几个配置(例如authorized_keys文件的路径和名称)可以通过修改配置文件/etc/ssh/sshd_config来配置。

设置使用 SSH 进行自动身份验证有两个步骤。它们是:

  1. 从需要登录到远程机器的机器上创建 SSH 密钥。

  2. 将生成的公钥传输到远程主机,并将其附加到~/.ssh/authorized_keys文件中。

为了创建一个 SSH 密钥,输入以下指定 RSA 加密算法类型的ssh-keygen命令:

$ ssh-keygen -t rsa
Generating public/private rsa key pair. 
Enter file in which to save the key (/home/slynux/.ssh/id_rsa): 
Created directory '/home/slynux/.ssh'. 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/slynux/.ssh/id_rsa. 
Your public key has been saved in /home/slynux/.ssh/id_rsa.pub. 
The key fingerprint is: 
f7:17:c6:4d:c9:ee:17:00:af:0f:b3:27:a6:9c:0a:05slynux@slynux-laptop 
The key's randomart image is: 
+--[ RSA 2048]----+ 
|           .     | 
|            o . .|
|     E       o o.|
|      ...oo | 
|       .S .+  +o.| 
|      .  . .=....| 
|     .+.o...| 
|      . . + o.  .|
|       ..+       | 
+-----------------+ 

生成公私钥对需要输入一个密码。也可以在不输入密码的情况下生成密钥对,但这是不安全的。我们可以编写使用脚本从脚本到多台机器进行自动登录的监控脚本。在这种情况下,应该在运行ssh-keygen命令时将密码留空,以防止脚本在运行时要求输入密码。

现在~/.ssh/id_rsa.pub~/.ssh/id_rsa已经生成。id_dsa.pub是生成的公钥,id_dsa是私钥。公钥必须附加到远程服务器上的~/.ssh/authorized_keys文件,我们需要从当前主机自动登录。

为了附加一个密钥文件,使用:

$ ssh USER@REMOTE_HOST "cat >> ~/.ssh/authorized_keys" < ~/.ssh/id_rsa.pub
Password:

在上一个命令中提供登录密码。

自动登录已设置。从现在开始,在执行过程中 SSH 不会提示输入密码。您可以使用以下命令进行测试:

$ ssh USER@REMOTE_HOST uname
Linux

您将不会被提示输入密码。

使用 SSH 在远程主机上运行命令

SSH 是一个有趣的系统管理工具,可以通过登录 shell 来控制远程主机。SSH 代表安全外壳。可以在通过登录到远程主机收到的 shell 上执行命令,就好像我们在本地主机上运行命令一样。它通过加密隧道运行网络数据传输。本教程将介绍在远程主机上执行命令的不同方法。

准备工作

SSH 不会默认随所有 GNU/Linux 发行版一起提供。因此,您可能需要使用软件包管理器安装openssh-serveropenssh-client软件包。SSH 服务默认在端口号 22 上运行。

如何做...

要连接到运行 SSH 服务器的远程主机,请使用:

$ ssh username@remote_host

在这个命令中:

  • username是存在于远程主机上的用户。

  • remote_host可以是域名或 IP 地址。

例如:

$ ssh mec@192.168.0.1
The authenticity of host '192.168.0.1 (192.168.0.1)' can't be established.
RSA key fingerprint is 2b:b4:90:79:49:0a:f1:b3:8a:db:9f:73:2d:75:d6:f9.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.0.1' (RSA) to the list of known hosts.
Password:

Last login: Fri Sep  3 05:15:21 2010 from 192.168.0.82
mec@proxy-1:~$

它将交互式地要求用户密码,并在成功验证后将返回用户的 shell。

默认情况下,SSH 服务器在端口 22 上运行。但是某些服务器在不同端口上运行 SSH 服务。在这种情况下,使用ssh命令的-p port_no来指定端口。

为了连接到运行在端口 422 上的 SSH 服务器,请使用:

$ ssh user@locahost -p 422

您可以在对应于远程主机的 shell 中执行命令。Shell 是一个交互式工具,用户可以在其中输入和运行命令。但是,在 shell 脚本上下文中,我们不需要交互式 shell。我们需要自动化多个任务。我们需要在远程 shell 上执行多个命令,并在本地主机上显示或存储其输出。在自动化脚本中每次输入密码都不切实际,因此应配置 SSH 的自动登录。

使用 SSH 实现无密码自动登录中解释了 SSH 命令。

在运行使用 SSH 的自动化脚本之前,请确保已配置自动登录。

要在远程主机上运行命令并在本地主机 shell 上显示其输出,请使用以下语法:

$ ssh user@host 'COMMANDS'

例如:

$ ssh mec@192.168.0.1 'whoami'
Password: 
mec

可以通过在命令之间使用分号分隔符来给出多个命令,如下所示:

$ ssh user@host 'command1 ; command2 ; command3'

命令可以通过stdin发送,并且命令的输出将可用于stdout

语法如下:

$ ssh user@remote_host  "COMMANDS" > stdout.txt 2> errors.txt

COMMANDS字符串应该用引号引起来,以防止分号字符在本地主机 shell 中充当分隔符。我们还可以通过stdin传递任何涉及管道语句的命令序列到 SSH 命令,如下所示:

$ echo  "COMMANDS" | sshuser@remote_host> stdout.txt 2> errors.txt

例如:

$ ssh mec@192.168.0.1  "echo user: $(whoami);echo OS: $(uname)"
Password: 
user: slynux 
OS: Linux 

在此示例中,在远程主机上执行的命令是:

echo user: $(whoami);
echo OS: $(uname)

它可以概括为:

COMMANDS="command1; command2; command3"
$ ssh user@hostname  "$COMMANDS"

我们还可以通过使用( )子 shell 运算符在命令序列中传递更复杂的子 shell。

让我们编写一个基于 SSH 的 shell 脚本,用于收集一组远程主机的正常运行时间。正常运行时间是系统已经运行的时间。uptime命令用于显示系统已经运行多长时间。

假设IP_LIST中的所有系统都有一个共同的用户test

#!/bin/bash
#Filename: uptime.sh
#Description: Uptime monitor

IP_LIST="192.168.0.1 192.168.0.5 192.168.0.9"
USER="test"

for IP in $IP_LIST;
do
 utime=$(ssh $USER@$IP uptime  | awk '{ print $3 }' )
 echo $IP uptime:  $utime
done

预期输出是:

$ ./uptime.sh
192.168.0.1 uptime: 1:50,
192.168.0.5 uptime: 2:15,
192.168.0.9 uptime: 10:15,

还有更多...

ssh命令可以使用多个附加选项执行。让我们逐一介绍它们。

使用压缩的 SSH

SSH 协议还支持使用压缩进行数据传输,当带宽成为问题时非常有用。使用ssh命令的-C选项启用压缩,如下所示:

$ ssh -C user@hostname COMMANDS

将数据重定向到远程主机 shell 命令的 stdin

有时我们需要将一些数据重定向到远程 shell 命令的stdin中。让我们看看如何做到这一点。一个例子如下:

$ echo "text" | ssh user@remote_host 'cat >> list'

或:

# Redirect data from file as:
$ ssh user@remote_host 'cat >> list'  < file

cat >> list将通过stdin接收的数据附加到文件列表中。此命令在远程主机上执行。但是数据是从本地主机传递到stdin

另请参阅

  • 使用 SSH 实现无密码自动登录,解释了如何配置自动登录以执行命令而无需提示输入密码。

在本地挂载点挂载远程驱动器

在进行读写数据传输操作时,拥有一个本地挂载点以访问远程主机文件系统将非常有帮助。SSH 是网络中最常见的传输协议,因此我们可以利用它与sshfs一起使用。sshfs使您能够将远程文件系统挂载到本地挂载点。让我们看看如何做到这一点。

准备工作

sshfs在 GNU/Linux 发行版中默认未安装。使用软件包管理器安装sshfssshfs是 fuse 文件系统包的扩展,允许支持的操作系统将各种数据挂载为本地文件系统。

如何做...

为了将远程主机上的文件系统位置挂载到本地挂载点,使用:

# sshfs user@remotehost:/home/path /mnt/mountpoint
Password:

在提示时输入用户密码。

现在,远程主机上/home/path的数据可以通过本地挂载点/mnt/mountpoint访问。

在完成工作后卸载,使用:

# umount /mnt/mountpoint

参见

  • 使用 SSH 在远程主机上运行命令,解释了 ssh 命令。

在网络上多播窗口消息

网络管理员经常需要向网络上的节点发送消息。在用户的桌面上显示弹出窗口将有助于提醒用户获得信息。使用 shell 脚本和 GUI 工具包可以实现此任务。本教程讨论了如何向远程主机发送自定义消息的弹出窗口。

准备工作

要实现 GUI 弹出窗口,可以使用 zenity。Zenity 是一个可脚本化的 GUI 工具包,用于创建包含文本框、输入框等的窗口。SSH 可用于连接到远程主机上的远程 shell。Zenity 在 GNU/Linux 发行版中默认未安装。使用软件包管理器安装 zenity。

如何做...

Zenity 是可脚本化的对话框创建工具包之一。还有其他工具包,如 gdialog、kdialog、xdialog 等。Zenity 似乎是一个灵活的工具包,符合 GNOME 桌面环境。

为了使用 zenity 创建信息框,使用:

$ zenity --info --text "This is a message"
# It will display a window with "This is a message" as text.

Zenity 可以用来创建带有输入框、组合输入、单选按钮、按钮等的窗口。这些不在本教程的范围内。查看 zenity 的 man 页面以获取更多信息。

现在,我们可以使用 SSH 在远程机器上运行这些 zenity 语句。为了在远程主机上通过 SSH 运行此语句,运行:

$ ssh user@remotehost 'zenity --info --text "This is a message"'

但是这将返回一个错误,如下:

(zenity:3641): Gtk-WARNING **: cannot open display: 

这是因为 zenity 依赖于 Xserver。Xsever 是一个负责在屏幕上绘制图形元素的守护进程,包括 GUI。裸的 GNU/Linux 系统只包含文本终端或 shell 提示符。

Xserver 使用一个特殊的环境变量DISPLAY来跟踪正在系统上运行的 Xserver 实例。

我们可以手动设置DISPLAY=:0来指示 Xserver 关于 Xserver 实例。

上一个 SSH 命令可以重写为:

$ ssh username@remotehost 'export DISPLAY=:0 ; zenity --info --text "This is a message"'

如果具有用户名的用户已登录到任何窗口管理器中,此语句将在remotehost上显示一个弹出窗口。

为了将弹出窗口多播到多个远程主机,编写一个 shell 脚本如下:

#!/bin/bash
#Filename: multi_cast_window.sh
# Description: Multi-cast window popups

IP_LIST="192.168.0.5 192.168.0.3 192.168.0.23"
USER="username"

COMMAND='export DISPLAY=:0 ;zenity --info --text "This is a message" '
for host in $IP_LIST;
do
  ssh $USER@$host  "$COMMAND" &
done

它是如何工作的...

在上面的脚本中,我们有一个 IP 地址列表,应该在这些 IP 地址上弹出窗口。使用循环来遍历 IP 地址并执行 SSH 命令。

在 SSH 语句中,最后我们使用了后缀&&将 SSH 语句发送到后台。这样做是为了方便执行多个 SSH 语句的并行化。如果不使用&,它将启动 SSH 会话,执行 zenity 对话框,并等待用户关闭弹出窗口。除非远程主机上的用户关闭窗口,否则循环中的下一个 SSH 语句将不会被执行。为了避免循环被阻塞,等待 SSH 会话终止,使用了&技巧。

参见

  • 使用 SSH 在远程主机上运行命令,解释了 ssh 命令。

网络流量和端口分析

网络端口是网络应用程序的基本参数。应用程序在主机上打开端口,并通过远程主机上打开的端口与远程主机通信。了解打开和关闭的端口对于安全上下文至关重要。恶意软件和 root 工具包可能在具有自定义端口和自定义服务的系统上运行,这允许攻击者捕获对数据和资源的未经授权访问。通过获取打开的端口和运行在端口上的服务列表,我们可以分析和保护系统免受 root 工具包的控制,并且该列表有助于有效地将它们移除。打开端口的列表不仅有助于恶意软件检测,还有助于收集有关系统上打开端口的信息,从而能够调试基于网络的应用程序。它有助于分析特定端口连接和端口监听功能是否正常工作。本教程讨论了用于端口分析的各种实用程序。

准备就绪

有多种命令可用于监听每个端口上运行的端口和服务(例如,lsofnetstat)。这些命令默认情况下在所有 GNU/Linux 发行版上都可用。

如何做...

要列出系统上所有打开的端口以及每个附加到它的服务的详细信息,请使用:

$ lsof -i
COMMAND    PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
firefox-b 2261 slynux   78u  IPv4  63729      0t0  TCP localhost:47797->localhost:42486 (ESTABLISHED)
firefox-b 2261 slynux   80u  IPv4  68270      0t0  TCP slynux-laptop.local:41204->192.168.0.2:3128 (CLOSE_WAIT)
firefox-b 2261 slynux   82u  IPv4  68195      0t0  TCP slynux-laptop.local:41197->192.168.0.2:3128 (ESTABLISHED)
ssh       3570 slynux    3u  IPv6  30025      0t0  TCP localhost:39263->localhost:ssh (ESTABLISHED)
ssh       3836 slynux    3u  IPv4  43431      0t0  TCP slynux-laptop.local:40414->boneym.mtveurope.org:422 (ESTABLISHED)
GoogleTal 4022 slynux   12u  IPv4  55370      0t0  TCP localhost:42486 (LISTEN)
GoogleTal 4022 slynux   13u  IPv4  55379      0t0  TCP localhost:42486->localhost:32955 (ESTABLISHED)

lsof的输出中的每个条目对应于打开端口进行通信的每个服务。输出的最后一列包含类似于以下行:

slynux-laptop.local:34395->192.168.0.2:3128 (ESTABLISHED)

在此输出中,slynux-laptop.local:34395对应于本地主机部分,192.168.0.2:3128对应于远程主机。

34395是从当前机器打开的端口,3128是服务连接到远程主机的端口。

要列出当前机器上打开的端口,请使用:

$ lsof -i | grep ":[0-9]\+->" -o | grep "[0-9]\+" -o  | sort | uniq

:[0-9]\+->正则表达式用于从lsof输出中提取主机端口部分(:34395->)。接下来的grep用于提取端口号(即数字)。通过同一端口可能发生多个连接,因此同一端口的多个条目可能会发生。为了仅显示每个端口一次,它们被排序并打印唯一的端口。

还有更多...

让我们通过其他实用程序来查看打开的端口和与网络流量相关的信息。

使用 netstat 列出打开的端口和服务

netstat 是用于网络服务分析的另一个命令。解释netstat的所有功能不在本教程的范围内。我们现在将看看如何列出服务和端口号。

使用netstat -tnp列出打开的端口和服务如下:

$ netstat -tnp
(Not all processes could be identified, non-owned process info 
will not be shown, you would have to be root to see it all.) 
Active Internet connections (w/o servers) 
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name 
tcp        0      0 192.168.0.82:38163      192.168.0.2:3128        ESTABLISHED 2261/firefox-bin 
tcp        0      0 192.168.0.82:38164      192.168.0.2:3128        TIME_WAIT   - 
tcp        0      0 192.168.0.82:40414      193.107.206.24:422      ESTABLISHED 3836/ssh 
tcp        0      0 127.0.0.1:42486         127.0.0.1:32955         ESTABLISHED 4022/GoogleTalkPlug 
tcp        0      0 192.168.0.82:38152      192.168.0.2:3128        ESTABLISHED 2261/firefox-bin 
tcp6       0      0 ::1:22                  ::1:39263               ESTABLISHED - 
tcp6       0      0 ::1:39263               ::1:22                  ESTABLISHED 3570/ssh 

第八章:戴上监视器的帽子

在本章中,我们将涵盖:

  • 磁盘使用技巧

  • 计算命令的执行时间

  • 有关已登录用户、启动日志、启动失败的信息

  • 打印最常用的 10 个命令

  • 列出 1 小时内 CPU 消耗最多的前 10 个进程

  • 使用 watch 监视命令输出

  • 记录对文件和目录的访问

  • 使用 logrotate 管理日志文件

  • 使用 syslog 记录

  • 监视用户登录以查找入侵者

  • 远程磁盘使用健康监控

  • 查找系统上活跃用户的小时数

介绍

操作系统由一系列系统软件组成,设计用于不同的目的,为不同的任务集提供服务。这些程序中的每一个都需要被操作系统或系统管理员监视,以了解它是否正常工作。我们还将使用一种称为日志记录的技术,通过该技术在应用程序运行时将重要信息写入文件。通过阅读这个文件,我们可以了解正在进行的特定软件或守护进程的操作时间线。如果应用程序或服务崩溃,这些信息有助于调试问题,并使我们能够解决任何问题。日志记录和监视还有助于从数据池中收集信息。日志记录和监视是确保操作系统安全和调试的重要任务。

本章介绍了可以用于监视不同活动的不同命令。它还介绍了日志记录技术及其用途。

磁盘使用技巧

磁盘空间是有限的资源。我们经常对硬盘或任何存储介质进行磁盘使用计算,以找出磁盘上可用的空闲空间。当空闲空间变得稀缺时,我们需要找出需要删除或移动的大文件,以便创建空闲空间。磁盘使用操作通常在 shell 脚本环境中使用。本文将说明用于磁盘操作的各种命令,以及可以使用各种选项计算磁盘使用情况的问题。

准备工作

dfdu是用于计算 Linux 磁盘使用的两个重要命令。命令df代表磁盘空闲,du代表磁盘使用。让我们看看如何使用它们执行涉及磁盘使用计算的各种任务。

如何做...

要查找文件(或文件)使用的磁盘空间,请使用:

$ du  FILENAME1 FILENAME2 ..

例如:

$ du file.txt
4

为了获取目录中所有文件的磁盘使用情况,以及在每行中显示每个文件的个别磁盘使用情况,请使用:

$ du -a DIRECTORY

-a 在指定的目录或递归目录中输出所有文件的结果。

提示

运行du DIRECTORY将输出类似的结果,但它只会显示子目录消耗的大小。然而,它们不显示每个文件的磁盘使用情况。要打印文件的磁盘使用情况,-a 是必需的。

例如:

$  du -a test
4  test/output.txt
4  test/process_log.sh
4  test/pcpu.sh
16  test

使用du DIRECTORY的示例如下:

$ du test
16  test

还有更多...

让我们看看du命令的其他用法。

以 KB、MB 或块显示磁盘使用情况

默认情况下,磁盘使用命令显示文件使用的总字节数。当以标准单位 KB、MB 或 GB 表示磁盘使用时,更易读的格式。为了以友好的格式打印磁盘使用情况,请使用-h如下:

du -h FILENAME

例如:

$ du -sh test/pcpu.sh
4.0K  test/pcpu.sh
# Multiple file arguments are accepted

或:

# du -h DIRECTORY
$ du -h hack/
16K  hack/

显示磁盘使用的总和

假设我们需要计算所有文件或目录占用的总大小,显示个别文件大小将不会有帮助。du有一个选项-c,它将输出作为参数给出的所有文件和目录的总磁盘使用情况。它附加了一行 SIZE 总和结果。语法如下:

$ du -c FILENAME1 FILENAME2..

例如:

du -c process_log.sh pcpu.sh
4  process_log.sh
4  pcpu.sh
8  total

或:

$ du  -c DIRECTORY

例如:

$ du -c test/
16  test/
16  total

或:

$ du -c *.txt
# Wildcards

-c 可以与其他选项一起使用,如-a 和-h。它提供与不使用-c 相同的输出。唯一的区别是它附加了一个包含总大小的额外行。

还有另一个选项-s(汇总),它将只打印总和作为输出。它将打印总和,并且可以与-h标志一起使用以以人类可读的格式打印。这个命令在实践中经常使用。语法如下:

$ du -s FILES(s)
$ du -sh DIRECTORY

例如:

$ du -sh slynux
680K  slynux

以指定单位打印文件

我们可以强制du以指定的单位打印磁盘使用量。例如:

  • 通过使用以下方式以字节(默认)打印大小:
$ du -b FILE(s)

  • 通过使用以下方式以千字节为单位打印大小:
$ du -k FILE(s)

  • 通过使用以下方式打印以兆字节为单位的大小:
$ du -m FILE(s)

  • 通过使用指定的 BLOCK 大小打印大小:
$ du -B BLOCK_SIZE FILE(s)

这里,BLOCK_SIZE以字节为单位指定。

一个包含所有命令的示例如下:

$ du pcpu.sh
4  pcpu.sh
$ du -b pcpu.sh
439	pcpu.sh
$ du -k pcpu.sh
4  pcpu.sh
$ du -m pcpu.sh
1  pcpu.sh
$ du -B 4  pcpu.sh
1024  pcpu.sh

排除磁盘使用量计算中的文件

有时我们需要从磁盘使用量计算中排除某些文件。这些被排除的文件可以通过两种方式指定:

  1. 通配符

我们可以使用通配符指定如下:

$ du --exclude "WILDCARD" DIRECTORY

例如:

$ du --exclude "*.txt" FILES(s)
# Excludes all .txt files from calculation

  1. 排除列表

我们可以指定要从文件中排除的文件列表如下:

$ du --exclude-from EXCLUDE.txt DIRECTORY
# EXCLUDE.txt is the file containing list

du还提供了一些其他方便的选项,以限制磁盘使用量的计算。我们可以通过使用--max-depth参数指定du应该遍历的层次结构的最大深度来计算整个磁盘使用量。指定深度为1计算当前目录中文件的大小。深度2将计算当前目录中的文件和下一个子目录的文件,并在第二个子目录处停止遍历。

例如:

$ du --max-depth 2 DIRECTORY

注意

du可以通过使用-x参数限制只遍历单个文件系统。假设运行du DIRECTORY,它将递归地遍历DIRECTORY的每个可能的子目录。目录层次结构中的一个子目录可能是一个挂载点(例如,/mnt/sda1/mnt的子目录,它是设备/dev/sda1的挂载点)。du将遍历该挂载点,并计算该设备文件系统的磁盘使用总和。为了防止du遍历和计算其他挂载点或文件系统,可以在其他du选项中使用-x标志。du -x /将排除/mnt/中的所有挂载点进行磁盘使用量计算。

在使用du时,请确保它遍历的目录或文件具有适当的读取权限。

从给定目录中查找最大的 10 个文件大小

查找大尺寸文件是我们经常遇到的一个常规任务。我们经常需要删除这些巨大的文件或移动它们。我们可以使用dusort命令轻松找到大尺寸文件。以下一行脚本可以完成这项任务:

$ du -ak SOURCE_DIR | sort -nrk 1 | head

这里的-a指定所有目录和文件。因此,du遍历SOURCE_DIR并计算所有文件的大小。输出的第一列包含以千字节为单位的大小,因为指定了-k,第二列包含文件或文件夹名称。

sort用于对第 1 列进行数字排序并将其反转。head用于从输出中解析前 10 行。

例如:

$ du -ak /home/slynux | sort -nrk 1 | head -n 4
50220 /home/slynux
43296 /home/slynux/.mozilla
43284 /home/slynux/.mozilla/firefox
43276 /home/slynux/.mozilla/firefox/8c22khxc.default

上述一行命令的一个缺点是它包括目录在结果中。然而,当我们只需要找到最大的文件而不是目录时,我们可以改进一行命令,只输出大尺寸的文件,如下所示:

$ find . -type f -exec du -k {} \; | sort -nrk 1 | head

我们使用find来过滤du而不是允许du自行递归遍历。

磁盘空闲信息

du命令提供有关使用情况的信息,而df提供有关可用磁盘空间的信息。它可以与-h一起使用,也可以不使用。当df-h一起使用时,它以人类可读的格式打印磁盘空间。

例如:

$ df
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/sda1              9611492   2276840   6846412  25% /
none                    508828       240    508588   1% /dev
none                    513048       168    512880   1% /dev/shm
none                    513048        88    512960   1% /var/run
none                    513048         0    513048   0% /var/lock
none                    513048         0    513048   0% /lib/init/rw
none                   9611492   2276840   6846412  25% /var/lib/ureadahead/debugfs

$ df -h
FilesystemSize  Used Avail Use% Mounted on
/dev/sda1             9.2G  2.2G  6.6G  25% /
none                  497M  240K  497M   1% /dev
none                  502M  168K  501M   1% /dev/shm
none                  502M   88K  501M   1% /var/run
none                  502M     0  502M   0% /var/lock
none                  502M     0  502M   0% /lib/init/rw
none                  9.2G  2.2G  6.6G  25% /var/lib/ureadahead/debugfs

计算命令的执行时间

在测试应用程序或比较给定问题的不同算法时,程序所花费的执行时间非常关键。一个好的算法应该在最短的时间内执行。有几种情况下,我们需要监视程序执行所花费的时间。例如,在学习排序算法时,如何实际陈述哪个算法更快?答案是计算相同数据集的执行时间。让我们看看如何做到这一点。

如何做到...

time是任何类 UNIX 操作系统中都可用的命令。您可以在要计算执行时间的命令前加上time,例如:

$ time COMMAND

命令将被执行并显示其输出。除了输出之外,time命令还会在stderr中附加所用的时间。例如:

$ time ls
test.txt
next.txt
real    0m0.008s
user    0m0.001s
sys     0m0.003s

它将显示执行的实际、用户和系统时间。三种不同的时间可以定义如下:

  • Real是挂钟时间——从调用开始到结束的时间。这是包括其他进程使用的时间片和进程被阻塞时花费的时间(例如,如果它正在等待 I/O 完成)的所有经过的时间。

  • User是进程内用户模式代码(内核之外)中花费的 CPU 时间。这只是执行进程时实际使用的 CPU 时间。其他进程和进程被阻塞时花费的时间不计入这个数字。

  • Sys是进程内核中花费的 CPU 时间。这意味着内核中系统调用中执行的 CPU 时间,而不是仍在用户空间中运行的库代码。与“用户时间”一样,这只是进程使用的 CPU 时间。

注意

time命令的可执行二进制文件位于/usr/bin/time,还有一个名为time的 shell 内置命令。当我们运行time时,默认情况下会调用 shell 内置命令。shell 内置的 time 选项有限。因此,我们应该使用可执行文件(/usr/bin/time)的绝对路径来执行其他功能。

我们可以使用-o filename选项将时间统计信息写入文件,如下所示:

$ /usr/bin/time -o output.txt COMMAND

文件名应该始终出现在-o标志之后。

为了将时间统计信息附加到文件而不覆盖,使用-a标志以及-o选项如下:

$ /usr/bin/time –a -o output.txt COMMAND

我们还可以使用-f选项使用格式字符串格式化时间输出。格式字符串由与特定选项对应的参数组成,前缀为%。实际时间、用户时间和系统时间的格式字符串如下:

  • 实际时间 - %e

  • 用户 - %U

  • sys - %S

通过组合参数字符串,我们可以创建格式化的输出,如下所示:

$ /usr/bin/time -f "FORMAT STRING" COMMAND

例如:

$ /usr/bin/time -f "Time: %U" -a -o timing.log uname
Linux

这里%U是用户时间的参数。

生成格式化输出时,命令的格式化输出被写入标准输出,而被计时的COMMAND的输出被写入标准错误。我们可以使用重定向运算符(>)重定向格式化输出,并使用(2>)错误重定向运算符重定向时间信息输出。例如:

$ /usr/bin/time -f "Time: %U" uname> command_output.txt 2>time.log
$ cat time.log
Time: 0.00
$ cat command_output.txt
Linux

使用time命令可以收集有关进程的许多细节。重要的细节包括退出状态、接收的信号数、进行的上下文切换次数等。可以使用适当的格式字符串显示每个参数。

以下表格显示了一些有趣的参数:

参数 描述
%C 被计时命令的名称和命令行参数。
%D 进程的未共享数据区的平均大小,以千字节为单位。
%E 进程使用的实际经过的时间(挂钟时间)[小时:]分钟:秒。
%x 命令的退出状态。
%k 传递给进程的信号数。
%W 进程被交换出主内存的次数。
%Z 系统的页面大小(以字节为单位)。这是一个每系统常量,但在系统之间有所不同。
%P 此作业获得的 CPU 百分比。这只是用户+系统时间除以总运行时间。它还打印一个百分号。
%K 进程的平均总(数据+堆栈+文本)内存使用量,以千字节为单位。
%w 程序自愿上下文切换的次数,例如在等待 I/O 操作完成时。
%c 进程被非自愿上下文切换的次数(因为时间片到期)。

例如,可以使用%Z参数显示页面大小如下:

$ /usr/bin/time -f "Page size: %Z bytes" ls> /dev/null
Page size: 4096 bytes

这里不需要timed命令的输出,因此将标准输出重定向到/dev/null设备,以防止它写入终端。

还有更多的格式字符串参数可用。阅读man time以获取更多详细信息。

有关登录用户、引导日志和引导失败的信息

收集有关操作环境、登录用户、计算机已经运行的时间以及任何引导失败的信息非常有帮助。这个教程将介绍一些用于收集有关活动机器信息的命令。

准备工作

这个教程将介绍whowusersuptimelastlastb命令。

如何做...

要获取有关当前登录到计算机的用户的信息,请使用:

$ who
slynux   pts/0   2010-09-29 05:24 (slynuxs-macbook-pro.local)
slynux   tty7    2010-09-29 07:08 (:0) 

或者:

$ w
 07:09:05 up  1:45,  2 users,  load average: 0.12, 0.06, 0.02
USER     TTY     FROM    LOGIN@   IDLE  JCPU PCPU WHAT
slynux   pts/0   slynuxs 05:24  0.00s  0.65s 0.11s sshd: slynux 
slynux   tty7    :0      07:08  1:45m  3.28s 0.26s gnome-session

它将提供有关登录用户、用户使用的伪 TTY、当前从伪终端执行的命令以及用户登录的 IP 地址的信息。如果是本地主机,它将显示主机名。whow的格式输出略有不同。w命令提供的详细信息比who更多。

TTY 是与文本终端相关联的设备文件。当用户新生成终端时,将在/dev/中创建相应的设备(例如/dev/pts/3)。可以通过键入和执行tty命令来找出当前终端的设备路径。

要列出当前登录到计算机的用户,请使用:

$ users
Slynux slynux slynux hacker

如果用户已经打开了多个伪终端,将显示相同用户的多个条目。在上面的输出中,用户slynux已经打开了三个伪终端。打印唯一用户的最简单方法是使用sortuniq进行过滤,如下所示:

$ users | tr ' ' '\n' | sort | uniq
slynux
hacker

我们使用tr' '替换为'\n'。然后sortuniq的组合将为每个用户生成唯一的条目。

为了查看系统已经运行了多长时间,请使用:

$ uptime
 21:44:33 up  3:17,  8 users,  load average: 0.09, 0.14, 0.09

跟在单词up后面的时间表示系统已经运行的时间。我们可以编写一个简单的一行代码来提取仅运行时间。

uptime输出中的平均负载是指系统负载的一个参数。这在章节Administration Calls!中有更详细的解释。为了获取有关以前的引导和用户登录会话的信息,请使用:

$ last
slynux   tty7         :0              Tue Sep 28 18:27   still logged in
reboot   system boot  2.6.32-21-generi Tue Sep 28 18:10 - 21:46  (03:35)
slynux   pts/0        :0.0            Tue Sep 28 05:31 - crash  (12:39)

last命令将提供有关登录会话的信息。实际上,它是一个系统登录的日志,其中包含tty从中登录的信息、登录时间、状态等。

last命令使用日志文件/var/log/wtmp作为输入日志数据。还可以使用-f选项明确指定last命令的日志文件。例如:

$ last –f /var/log/wtmp

为了获取单个用户的登录会话信息,请使用:

$ last USER

获取有关重新启动会话的信息如下:

$ last reboot
reboot   system boot  2.6.32-21-generi Tue Sep 28 18:10 - 21:48  (03:37)
reboot   system boot  2.6.32-21-generi Tue Sep 28 05:14 - 21:48  (16:33)

为了获取有关失败的用户登录会话的信息,请使用:

# lastb
test     tty8         :0               Wed Dec 15 03:56 - 03:56  (00:00) 
slynux   tty8         :0               Wed Dec 15 03:55 - 03:55  (00:00)

您应该以 root 用户身份运行lastb

打印最常用的 10 个命令

终端是用于访问 shell 提示符的工具,在那里我们输入和执行命令。用户在 shell 中运行许多命令。其中许多是经常使用的。通过查看他经常使用的命令,可以很容易地识别用户的性质。这个教程是一个小练习,用于找出最常用的 10 个命令。

准备工作

Bash 通过用户之前输入的命令并存储在文件~/.bash_history中来跟踪先前输入的命令。但它只保留最近执行的一定数量(比如 500)的命令。可以使用history命令或cat ~/.bash_history命令查看命令的历史记录。我们将使用这个来查找经常使用的命令。

如何做...

我们可以从~/.bash_history获取命令列表,仅获取不包括参数的命令,计算每个命令的出现次数,并找出出现次数最高的 10 个命令。

以下脚本可用于查找经常使用的命令:

#!/bin/bash
#Filename: top10_commands.sh
#Description: Script to list top 10 used commands

printf "COMMAND\tCOUNT\n" ;

cat ~/.bash_history | awk '{ list[$1]++; } \
END{
for(i in list)
{
printf("%s\t%d\n",i,list[i]); }
}'| sort -nrk 2 | head

示例输出如下:

$ ./top10_commands.sh
COMMAND  COUNT
ping    80
ls      56
cat     35
ps      34
sudo    26
du      26
cd      26
ssh     22
sftp    22
clear   21

它是如何工作的...

在上述脚本中,历史文件~/.bash_history是使用的源文件。源输入通过管道传递给awk。在awk中,我们有一个关联数组列表。这个数组可以使用命令名称作为索引,并将命令的计数存储在数组位置中。因此,对于每个命令的到达或出现,它将递增一个(list[$1]++)。$1被用作索引。$1是输入行中文本的第一个单词。如果使用$0,它将包含命令的所有参数。例如,如果ssh 192.168.0.4是来自.bash_history的一行,$0等于ssh 192.168.0.4$1等于ssh

一旦历史文件的所有行都被遍历,我们将得到一个带有命令名称作为索引和它们计数作为值的数组。因此,具有最大计数值的命令名称将是最常用的命令。因此,在awkEND{}块中,我们遍历命令的索引并打印所有命令名称和它们的计数。sort -nrk 2将根据第二列(COUNT)执行数值排序并反转它。因此,我们使用head命令从列表中提取前 10 个命令。您可以使用参数head -n NUMBER将前 10 个自定义为前 5 个或任何其他数字。

列出一个小时内消耗前 10 个 CPU 的进程

CPU 时间是一个重要的资源,有时我们需要跟踪在一段时间内消耗最多 CPU 周期的进程。在常规的台式机或笔记本电脑上,CPU 被大量消耗可能不是一个问题。然而,对于处理大量请求的服务器来说,CPU 是一个关键资源。通过监视一定时间内的 CPU 使用情况,我们可以识别一直占用 CPU 的进程,并优化它们以有效地使用 CPU 或者由于其他问题对它们进行调试。这个配方是一个处理监视和记录进程的实践。

准备工作

ps是一个用于收集有关系统上运行的进程的详细信息的命令。它可以用于收集诸如 CPU 使用情况、正在执行的命令、内存使用情况、进程状态等的详细信息。可以记录一个小时内消耗 CPU 的进程,并通过适当使用ps和文本处理来确定前 10 个进程。有关ps命令的更多详细信息,请参阅章节:管理调用

如何做...

让我们通过以下 shell 脚本来监视和计算一个小时内的 CPU 使用情况:

#!/bin/bash
#Name: pcpu_usage.sh
#Description: Script to calculate cpu usage by processes for 1 hour

SECS=3600
UNIT_TIME=60

#Change the SECS to total seconds for which monitoring is to be performed.
#UNIT_TIME is the interval in seconds between each sampling

STEPS=$(( $SECS / $UNIT_TIME ))

echo Watching CPU usage... ;

for((i=0;i<STEPS;i++))
do
  ps -eo comm,pcpu | tail -n +2 >> /tmp/cpu_usage.$$
  sleep $UNIT_TIME
done

echo
echo CPU eaters :

cat /tmp/cpu_usage.$$ | \
awk '
{ process[$1]+=$2; }
END{ 
  for(i in process)
  {
    printf("%-20s %s",i, process[i] ;
  }

   }' | sort -nrk 2 | head

rm /tmp/cpu_usage.$$
#Remove the temporary log file

示例输出如下:

$ ./pcpu_usage.sh
Watching CPU usage...
CPU eaters :
Xorg        20
firefox-bin   15
bash        3
evince      2
pulseaudio    1.0
pcpu.sh         0.3
wpa_supplicant  0
wnck-applet     0
watchdog/0      0
usb-storage     0

它是如何工作的...

在上述脚本中,主要的输入来源是ps -eocomm, pcpucomm代表命令名称,pcpu代表 CPU 使用率百分比。它将输出所有进程名称和 CPU 使用率百分比。对于输出中的每个进程都存在一行。由于我们需要监视一个小时的 CPU 使用情况,我们会使用ps -eo comm,pcpu | tail -n +2重复地获取使用统计信息,并将其附加到一个文件/tmp/cpu_usage.$$中,该文件在for循环中运行,每次迭代都等待 60 秒。这个等待是由sleep 60提供的。它将每分钟执行一次ps

tail -n +2用于剥离ps输出中的标题和COMMAND %CPU

$$cpu_usage.$$中表示它是当前脚本的进程 ID。假设 PID 为 1345,在执行期间它将被替换为/tmp/cpu_usage.1345。我们将这个文件放在/tmp中,因为它是一个临时文件。

统计文件将在一小时后准备好,并包含 60 个条目,对应于每分钟的进程状态。然后使用awk来对每个进程的总 CPU 使用情况进行求和。一个关联数组进程用于 CPU 使用情况的求和。它使用进程名称作为数组索引。最后,它根据总 CPU 使用情况进行数字逆排序,并通过 head 获取前 10 个使用情况条目。

另请参阅

  • 第四章的基本 awk 入门,解释了 awk 命令

  • head 和 tail - 打印第三章的最后或前十行,解释了 tail 命令

使用 watch 监视命令输出

我们可能需要在相等的时间间隔内持续观察命令的输出一段时间。例如,对于大文件复制,我们需要观察文件大小的增长。为了做到这一点,新手们反复输入命令并按回车键多次。相反,我们可以使用 watch 命令重复查看输出。本教程解释了如何做到这一点。

如何做...

watch命令可用于定期监视终端上命令的输出。watch命令的语法如下:

$ watch COMMAND

例如:

$ watch ls

或者:

$ watch 'COMMANDS'

例如:

$ watch 'ls -l | grep "^d"'
# list only directories

此命令将以默认间隔两秒更新输出。

我们还可以通过使用-n SECONDS来指定输出需要更新的时间间隔。例如:

$ watch -n 5 'ls -l'
#Monitor the output of ls -l at regular intervals of 5 seconds

还有更多

让我们探索watch命令的一个附加功能。

突出显示 watch 输出中的差异

watch中,有一个选项可以在执行命令期间更新的差异以突出显示,并使用颜色进行标记。可以通过使用-d选项启用差异突出显示,如下所示:

$ watch -d 'COMMANDS'

记录对文件和目录的访问

记录文件和目录访问对于跟踪文件和文件夹发生的变化非常有帮助。本教程将描述如何记录用户访问。

准备就绪

inotifywait命令可用于收集有关文件访问的信息。它不会默认随每个 Linux 发行版一起提供。您必须使用软件包管理器安装inotify-tools软件包。它还需要 Linux 内核编译时启用 inotify 支持。大多数新的 GNU/Linux 发行版都启用了内核中的 inotify。

如何做...

让我们走一遍监视目录访问的 shell 脚本:

#/bin/bash
#Filename: watchdir.sh
#Description: Watch directory access
path=$1
#Provide path of directory or file 
as argument to script

inotifywait -m -r -e create,move,delete $path  -q 

示例输出如下:

$ ./watchdir.sh .
./ CREATE new
./ MOVED_FROM new
./ MOVED_TO news
./ DELETE news

它是如何工作的...

先前的脚本将记录从给定路径创建、移动和删除文件和文件夹的事件。给出-m选项以持续监视更改,而不是在事件发生后退出。给出-r以启用递归监视目录。-e指定要监视的事件列表。-q是为了减少冗长的消息并只打印所需的消息。此输出可以重定向到日志文件。

我们可以添加或删除事件列表。可用的重要事件如下:

事件 描述
access 当文件发生读取时。
modify 当文件内容被修改时。
attrib 当元数据被更改时。
move 当文件进行移动操作时。
create 当创建新文件时。
open 当文件进行打开操作时。
close 当文件进行关闭操作时。
delete 当文件被删除时。

使用 logrotate 进行日志文件管理

日志文件是 Linux 系统维护的重要组成部分。日志文件有助于跟踪系统上不同服务发生的事件。这有助于系统管理员调试问题,还提供了有关实时机器上发生的事件的统计信息。需要管理日志文件,因为随着时间的推移,日志文件的大小会变得越来越大。因此,我们使用一种称为轮换的技术来限制日志文件的大小,如果日志文件达到了限制之外的大小,它将剥离日志文件并将日志文件的旧条目存储在归档中。因此,旧日志可以被存储和保留以供将来参考。让我们看看如何轮换日志并存储它们。

准备工作

logrotate是每个 Linux 系统管理员都应该了解的命令。它有助于限制日志文件的大小。在日志文件中,记录器将信息追加到日志文件中。因此,最近的信息出现在日志文件的底部。logrotate将根据配置文件扫描特定的日志文件。它将保留日志文件的最后 100 千字节(例如,指定 SIZE = 100k),并将其余数据(旧的日志数据)移动到一个新文件logfile_name.1中。当日志文件(logfile_name.1)中出现更多条目并且超过了 SIZE 时,它将使用最新的条目更新日志文件,并创建带有旧日志的logfile_name.2。这个过程可以很容易地通过logrotate进行配置。logrotate还可以将旧日志压缩为logfile_name.1.gzlogfile_name2.gz等。是否压缩旧日志文件的选项在logrotate配置中可用。

如何做...

logrotate的配置目录位于/etc/logrotate.d。如果列出该目录的内容,可以找到许多其他日志文件的配置。

我们可以为我们的日志文件编写自定义配置(比如/var/log/program.log)如下:

$ cat /etc/logrotate.d/program
/var/log/program.log {
missingok
notifempty
size 30k
 compress
weekly
 rotate 5
create 0600 root root
}

现在配置已经完成。配置中的/var/log/program.log指定了日志文件路径。它将在相同的目录路径中归档旧日志。让我们看看这些参数各是什么:

参数 描述
missingok 如果日志文件丢失,则忽略并返回而不进行日志轮换。
notifempty 只有在源日志文件不为空时才进行日志轮换。
size 30k 限制要进行轮换的日志文件的大小。可以是 1M 表示 1MB。
compress 启用 gzip 对较旧的日志进行压缩。
weekly 指定进行轮换的时间间隔。可以是每周、每年或每天。
rotate 5 要保留的旧日志文件归档副本的数量。由于指定了 5,将会有program.log.1.gzprogram.log.2.gz,以此类推,直到program.log.5.gz
create 0600 root root 指定要创建的日志文件归档的模式、用户和组。

表中指定的选项是可选的;我们可以在logrotate配置文件中只指定所需的选项。logrotate有许多可用的选项。请参考 man 页面(linux.die.net/man/8/logrotate)获取有关logrotate的更多信息。

使用 syslog 记录

日志文件是为向用户提供服务的应用程序的重要组成部分。应用程序在运行时将状态信息写入其日志文件。如果发生任何崩溃或者我们需要查询有关服务的一些信息,我们会查看日志文件。您可以在/var/log目录中找到与不同守护程序和应用程序相关的许多日志文件。这是存储日志文件的常见目录。如果您阅读日志文件的几行,您会发现日志中的行是以常见格式编写的。在 Linux 中,创建和将日志信息写入/var/log日志文件由一种称为 syslog 的协议处理。它由syslogd守护程序处理。每个标准应用程序都使用 syslog 来记录信息。在本教程中,我们将讨论如何使用syslogd从 shell 脚本记录信息。

准备就绪

日志文件对帮助您推断系统出现了什么问题非常有用。因此,在编写关键应用程序时,始终将应用程序的进度记录到日志文件中是一种良好的做法。我们将学习命令记录器以使用syslogd记录到日志文件。在了解如何写入日志文件之前,让我们先浏览一下 Linux 中使用的重要日志文件列表:

日志文件 描述
/var/log/boot.log 启动日志信息。
/var/log/httpd Apache Web 服务器日志。
/var/log/messages 启动后内核信息。
/var/log/auth.log 用户认证日志。
/var/log/dmesg 系统启动消息。
/var/log/mail.log 邮件服务器日志。
/var/log/Xorg.0.log X 服务器日志。

如何做…

为了记录到 syslog 文件/var/log/messages,请使用:

$ logger LOG_MESSAGE

例如:

$ logger This is a test log line

$ tail -n 1 /var/log/messages
Sep 29 07:47:44 slynux-laptop slynux: This is a test log line

日志文件/var/log/messages是一个通用目的的日志文件。当使用logger命令时,默认情况下会记录到/var/log/messages。为了记录到具有指定标记的 syslog 中,请使用:

$ logger -t TAG This is a message

$ tail -n 1 /var/log/messages
Sep 29 07:48:42 slynux-laptop TAG: This is a message

syslog 处理/var/log中的许多日志文件。但是,当记录器发送消息时,它使用标记字符串来确定需要记录到哪个日志文件中。syslogd通过使用与日志相关的TAG来决定应将日志记录到哪个文件中。您可以从位于/etc/rsyslog.d/目录中的配置文件中查看标记字符串和相关的日志文件。

为了将另一个日志文件的最后一行记录到系统日志中,请使用:

$ logger -f /var/log/source.log

另见

  • head 和 tail - 打印最后或前 10 行 第三章 ,解释 head 和 tail 命令

监视用户登录以查找入侵者

日志文件可用于收集有关系统状态的详细信息。以下是一个有趣的脚本编写问题陈述:

我们有一个连接到启用 SSH 的互联网的系统。许多攻击者试图登录系统。我们需要编写一个 shell 脚本来设计入侵检测系统。入侵者被定义为尝试在两分钟以上进行多次尝试并且所有尝试都失败的用户。应检测此类用户并生成报告,其中包括以下详细信息:

  • 尝试登录的用户帐户

  • 尝试次数

  • 攻击者的 IP 地址

  • IP 地址的主机映射

  • 尝试登录的时间范围。

入门

我们可以编写一个 shell 脚本,可以扫描日志文件并从中收集所需的信息。在这里,我们正在处理 SSH 登录失败。用户认证会话日志被写入日志文件/var/log/auth.log。脚本应扫描日志文件以检测失败的登录尝试,并对日志执行不同的检查以推断数据。我们可以使用host命令从 IP 地址找出主机映射。

如何做…

让我们编写一个入侵者检测脚本,该脚本可以使用认证日志文件生成入侵者报告,如下所示:

#!/bin/bash
#Filename: intruder_detect.sh
#Description: Intruder reporting tool with auth.log input
AUTHLOG=/var/log.auth.log

if [[ -n $1 ]];
then
  AUTHLOG=$1
  echo Using Log file : $AUTHLOG
fi

LOG=/tmp/valid.$$.log
grep -v "invalid" $AUTHLOG > $LOG
users=$(grep "Failed password" $LOG | awk '{ print $(NF-5) }' | sort | uniq)

printf "%-5s|%-10s|%-10s|%-13s|%-33s|%s\n" "Sr#" "User" "Attempts" "IP address" "Host_Mapping" "Time range"

ucount=0;

ip_list="$(egrep -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" $LOG | sort | uniq)"

for ip in $ip_list;
do
  grep $ip $LOG > /tmp/temp.$$.log

for user in $users;
do
  grep $user /tmp/temp.$$.log> /tmp/$$.log
  cut -c-16 /tmp/$$.log > $$.time
  tstart=$(head -1 $$.time);
  start=$(date -d "$tstart" "+%s");

  tend=$(tail -1 $$.time);
  end=$(date -d "$tend" "+%s")

  limit=$(( $end - $start ))

  if [ $limit -gt 120 ];
  then
    let ucount++;

    IP=$(egrep -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" /tmp/$$.log | head -1 );

    TIME_RANGE="$tstart-->$tend"

    ATTEMPTS=$(cat /tmp/$$.log|wc -l);

    HOST=$(host $IP | awk '{ print $NF }' )

  printf "%-5s|%-10s|%-10s|%-10s|%-33s|%-s\n" "$ucount" "$user" "$ATTEMPTS" "$IP" "$HOST" "$TIME_RANGE";
  fi
done
done

rm /tmp/valid.$$.log /tmp/$$.log $$.time /tmp/temp.$$.log 2> /dev/null

示例输出如下:

如何做…

它是如何工作的...

intruder_detect.sh脚本中,我们使用auth.log文件作为输入。我们可以通过使用命令行参数将日志文件作为输入提供给脚本,或者默认情况下,它读取/var/log/auth.log文件。我们只需要记录关于有效用户名的登录尝试的详细信息。当发生无效用户的登录尝试时,类似于Failed password for invalid user bob from 203.83.248.32 port 7016 ssh2的日志将被记录到auth.log。因此,我们需要排除日志文件中所有包含“invalid”单词的行。使用带有反转选项(-v)的grep命令来删除所有与无效用户对应的日志。下一步是找出发生登录尝试并失败的用户列表。SSH 将记录类似于sshd[21197]: Failed password for bob1 from 203.83.248.32 port 50035 ssh2的日志行,表示密码错误。

因此,我们应该找到所有包含“failed password”单词的行。现在需要找出所有唯一的 IP 地址,以提取与每个 IP 地址对应的所有日志行。可以使用 IP 地址的正则表达式和egrep命令来提取 IP 地址的列表。使用for循环来迭代 IP 地址,并使用grep找到相应的日志行,并将其写入临时文件。日志行中倒数第六个单词是用户名(例如,bob1)。使用awk命令从最后一个单词中提取第六个单词。NF返回最后一个单词的列数。因此,NF-5给出了倒数第六个单词的列数。我们使用sortuniq命令来生成一个不重复的用户列表。

现在我们应该收集包含每个用户名称的失败登录日志行。使用for循环来读取与每个用户对应的行,并将这些行写入临时文件。每个日志行中的前 16 个字符是时间戳。使用cut命令来提取时间戳。一旦我们获得了用户的所有失败登录尝试的时间戳,我们应该检查第一次尝试和最后一次尝试之间的时间差。第一条日志行对应于第一次尝试,最后一条日志行对应于最后一次尝试。我们使用head -1提取第一行和tail -1提取最后一行。现在我们有第一次(tstart)和最后一次尝试(tends)的时间戳的字符串格式。使用date命令,我们可以将字符串表示的日期转换为 UNIX Epoch 时间的总秒数(第一章的获取、设置日期和延迟中的配方解释了 Epoch 时间)。

变量 start 和 end 具有与日期字符串中的开始和结束时间戳对应的秒数。现在,取它们之间的差异,并检查是否超过两分钟(120 秒)。因此,特定用户被称为入侵者,并且相应的带有详细信息的条目将被生成为日志。可以使用 IP 地址的正则表达式和egrep命令从日志中提取 IP 地址。尝试次数是用户的日志行数。可以使用wc命令找出行数。主机名映射可以通过使用 IP 地址作为参数运行 host 命令的输出来提取。时间范围可以使用我们提取的时间戳来打印。最后,脚本中使用的临时文件将被删除。

上述脚本旨在说明从日志中扫描并生成报告的模型。它试图使脚本更小更简单,以排除复杂性。因此它有一些错误。您可以通过使用更好的逻辑来改进脚本。

远程磁盘使用健康监视器

一个网络由几台具有不同用户的机器组成。网络需要对远程机器的磁盘使用情况进行集中监控。网络的系统管理员需要每天记录网络中所有机器的磁盘使用情况。每条日志行应包含日期、机器的 IP 地址、设备、设备容量、已使用空间、剩余空间、使用百分比和健康状态等详细信息。如果任何远程机器中任何分区的磁盘使用率超过 80%,健康状态应设置为警报,否则应设置为安全。本示例将说明如何编写一个监控脚本,可以从网络中的远程机器收集详细信息。

准备工作

我们需要从网络中的每台机器单独收集磁盘使用统计信息,并在中央机器中编写日志文件。可以安排每天在特定时间运行收集详细信息并写入日志的脚本。可以使用 SSH 登录到远程系统来收集磁盘使用数据。

如何做…

首先,我们必须在网络中的所有远程机器上设置一个公共用户帐户。这是为了让 disklog 程序登录到系统中。我们应该为该特定用户配置 SSH 自动登录(在第七章的使用 SSH 进行无密码自动登录一节中,解释了自动登录的配置)。我们假设所有远程机器中都有一个名为 test 的用户,配置了自动登录。让我们来看一下 shell 脚本:

#!/bin/bash
#Filename: disklog.sh
#Description: Monitor disk usage health for remote systems

logfile="diskusage.log"

if [[ -n $1 ]]
then
  logfile=$1
fi

if [ ! -e $logfile ]
then

  printf "%-8s %-14s %-9s %-8s %-6s %-6s %-6s %s\n" "Date" "IP address" "Device" "Capacity" "Used" "Free" "Percent" "Status" > $logfile
fi

IP_LIST="127.0.0.1 0.0.0.0"
#provide the list of remote machine IP addresses 

(
for ip in $IP_LIST;
do

  ssh slynux@$ip 'df -H' | grep ^/dev/ > /tmp/$$.df

  while read line;
  do
    cur_date=$(date +%D)
    printf "%-8s %-14s " $cur_date $ip
    echo $line | awk '{ printf("%-9s %-8s %-6s %-6s %-8s",$1,$2,$3,$4,$5); }'

  pusg=$(echo $line | egrep -o "[0-9]+%")
  pusg=${pusg/\%/};
  if [ $pusg -lt 80 ];
  then
    echo SAFE
  else
    echo ALERT
  fi

  done< /tmp/$$.df	
done

) >> $logfile

我们可以使用 cron 实用程序安排定期运行脚本。例如,要在每天上午 10 点运行脚本,可以在crontab中写入以下条目:

00 10 * * * /home/path/disklog.sh /home/user/diskusg.log

运行命令crontab -e。添加上述行并保存文本编辑器。

您可以手动运行脚本,如下所示:

$ ./disklog.sh

上述脚本的示例输出日志如下:

如何做…

它是如何工作的…

disklog.sh脚本中,我们可以将日志文件路径作为命令行参数提供,否则它将使用默认日志文件。如果日志文件不存在,它将把日志文件头文本写入新文件中。使用-e $logfile来检查文件是否存在。远程机器的 IP 地址列表存储在变量IP_LIST中,用空格分隔。必须确保IP_LIST中列出的所有远程系统都有一个名为test的公共用户,并配置了 SSH 自动登录。使用for循环来迭代每个 IP 地址。执行远程命令df -H来使用ssh命令获取磁盘空闲使用数据。它被存储在一个临时文件中。使用while循环逐行读取文件。使用awk提取数据并打印。还打印日期。使用egrep命令提取百分比使用率,并用none替换%以获得百分比的数值。检查百分比值是否超过 80。如果小于 80,则状态设置为安全,如果大于或等于 80,则状态设置为警报。整个打印数据应重定向到日志文件。因此,代码部分被封装在子 shell()中,并且标准输出被重定向到日志文件。

另请参阅

  • 使用 cron 进行调度在第九章中,解释了 crontab 命令

查找系统上活跃用户的小时数

考虑一个具有共享托管的 Web 服务器。每天有许多用户登录和退出服务器。用户活动记录在服务器的系统日志中。这个示例是一个实践任务,利用系统日志,找出每个用户在服务器上花了多少小时,并根据总使用小时数对它们进行排名。应该生成一个报告,包括排名、用户、第一次登录日期、最后登录日期、登录次数和总使用小时数等详细信息。让我们看看如何解决这个问题。

准备工作

last命令用于列出系统中用户的登录会话的详细信息。日志数据存储在/var/log/wtmp文件中。通过为每个用户单独添加会话小时数,我们可以找出总使用小时数。

如何做到这一点...

让我们通过脚本找出活跃用户并生成报告。

#!/bin/bash
#Filename: active_users.sh
#Description: Reporting tool to find out active users

log=/var/log/wtmp

if [[ -n $1 ]];
then
  log=$1
fi

printf "%-4s %-10s %-10s %-6s %-8s\n" "Rank" "User" "Start" "Logins" "Usage hours"

last -f $log | head -n -2   > /tmp/ulog.$$

cat /tmp/ulog.$$ |  cut -d' ' -f1 | sort | uniq> /tmp/users.$$

(
while read user;
do
  grep ^$user /tmp/ulog.$$ > /tmp/user.$$
  seconds=0

while read t
  do
    s=$(date -d $t +%s 2> /dev/null) 
    let seconds=seconds+s
  done< <(cat /tmp/user.$$ | awk '{ print $NF }' | tr -d ')(')

  firstlog=$(tail -n 1 /tmp/user.$$ | awk '{ print $5,$6 }')
  nlogins=$(cat /tmp/user.$$ | wc -l) 
  hours=$(echo "$seconds / 60.0" | bc)

  printf "%-10s %-10s %-6s %-8s\n"  $user "$firstlog" $nlogins $hours
done< /tmp/users.$$ 

) | sort -nrk 4 | awk '{ printf("%-4s %s\n", NR, $0) }' 
rm /tmp/users.$$ /tmp/user.$$ /tmp/ulog.$$

一个示例输出如下:

$ ./active_users.sh
Rank User       Start      Logins Usage hours
1    easyibaa   Dec 11     531    11437311943
2    demoproj   Dec 10     350    7538718253
3    kjayaram   Dec 9      213    4587849555
4    cinenews   Dec 11     85     1830831769
5    thebenga   Dec 10     54     1163118745
6    gateway2   Dec 11     52     1120038550
7    soft132    Dec 12     49     1055420578
8    sarathla   Nov 1      45     969268728
9    gtsminis   Dec 11     41     883107030
10   agentcde   Dec 13     39     840029414

它是如何工作的...

active_users.sh脚本中,我们可以将wtmp日志文件作为命令行参数提供,或者它将使用defaulwtmp日志文件。使用last -f命令来打印日志文件内容。日志文件中的第一列是用户名。通过使用cut命令,我们从日志文件中提取第一列。然后使用sortuniq命令找出唯一的用户。现在对于每个用户,使用grep找出对应其登录会话的日志行,并将其写入临时文件。最后一条日志中的最后一列是用户登录会话的持续时间。因此,为了找出用户的总使用小时数,需要将会话持续时间相加。使用时间命令将使用持续时间转换为秒。

为了提取用户的会话小时数,我们使用了awk命令。为了去掉括号,使用了tr -d命令。使用<( COMMANDS )操作符将使用小时字符串列表传递给while循环的标准输入。它充当文件输入。通过使用date命令,将每个小时字符串转换为秒,并添加到变量seconds中。用户的第一次登录时间在最后一行,并且被提取出来。登录尝试次数是日志行数。为了根据总使用小时数计算每个用户的排名,数据记录需要按照使用小时作为关键字进行降序排序。为了指定反向排序的数量,使用-nr选项以及sort命令。-k4用于指定关键列(使用小时)。最后,排序的输出被传递给awkawk命令为每行添加了行号,这成为每个用户的排名。

第九章:管理调用

在本章中,我们将涵盖:

  • 关于进程的信息收集

  • 终止进程并发送或响应信号

  • Which、whereis、file、whatis 和 loadavg 的解释

  • 向用户终端发送消息

  • 收集系统信息

  • 使用/proc – 收集信息

  • 使用 cron 进行调度

  • 从 Bash 中写入和读取 MySQL 数据库

  • 用户管理脚本

  • 批量图像调整和格式转换

介绍

GNU/Linux 生态系统由运行的程序、服务、连接的设备、文件系统、用户等组成。全面了解整个系统并根据我们的意愿管理操作系统,是系统管理的主要目的。人们应该掌握常用命令和适当的使用方法,以收集系统信息和管理资源,编写脚本和自动化工具来执行管理任务。本章将介绍几个命令和方法,用于收集关于系统的信息,并利用这些命令来编写管理脚本。

关于进程的信息收集

进程是程序的运行实例。计算机上运行多个进程,每个进程都被分配一个称为进程 ID 的唯一标识号。它是一个整数。可以同时执行相同名称的程序的多个实例。但它们都将有不同的进程 ID。进程包括多个属性,例如拥有进程的用户、程序使用的内存量、程序使用的 CPU 量等。本教程将介绍如何收集关于进程的信息。

准备工作

与进程管理相关的重要命令是toppspgrep。让我们看看如何收集关于进程的信息。

如何做…

ps是收集关于进程的信息的重要工具。ps提供了有关拥有进程的用户、进程启动时间、用于执行进程的命令路径、进程 ID(PID)、它附加的终端(TTY)、进程使用的内存、进程使用的 CPU 等信息。例如:

$ ps
 PID TTY          TIME CMD
 1220 pts/0    00:00:00 bash
 1242 pts/0    00:00:00 ps

ps命令通常与一组参数一起使用。当没有任何参数运行时,ps将显示在当前(TTY)终端上运行的进程。第一列显示进程 ID(PID),第二列是 TTY(终端),第三列是进程启动后经过的时间,最后是 CMD(命令)。

为了显示包含更多信息的更多列,使用-f(表示完整)如下:

$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
slynux    1220  1219  0 18:18 pts/0    00:00:00 -bash
slynux    1587  1220  0 18:59 pts/0    00:00:00 ps -f

上述的ps命令并不实用,因为它没有提供除了附加到当前终端的进程之外的任何信息。为了获取关于系统上运行的每个进程的信息,添加-e(每个)选项。-ax(全部)选项也会产生相同的输出。

注意

-x参数与-a一起指定了默认情况下由ps施加的去除 TTY 限制。通常,使用ps而不带参数会打印仅附加到终端的进程。

运行ps -eps –ef,否则运行ps -axps –axf

$ ps -e | head
 PID TTY          TIME CMD
1 ?        00:00:00 init
2 ?        00:00:00 kthreadd
3 ?        00:00:00 migration/0
4 ?        00:00:00 ksoftirqd/0
5 ?        00:00:00 watchdog/0
6 ?        00:00:00 events/0
7 ?        00:00:00 cpuset
8 ?        00:00:00 khelper
9 ?        00:00:00 netns

这将是一个很长的列表。示例使用head过滤输出,所以我们只得到前 10 个条目。

ps命令支持显示进程名称和进程 ID 以及其他信息。默认情况下,ps将信息显示为不同的列。其中大部分对我们来说并不实用。我们实际上可以使用-o标志指定要显示的列。因此,我们可以只打印所需的列。下面将讨论与参数相关的不同参数和-o的用法。

为了使用ps显示所需的输出列,使用:

$ ps [OTHER OPTIONS] -o parameter1,parameter2,parameter3 ..

注意

使用逗号(,)运算符来分隔-o的参数。应该注意的是逗号运算符和下一个参数之间没有空格。大多数情况下,-o选项与-e(every)选项(-oe)结合使用,因为它应该列出系统中运行的每个进程。然而,当与–o一起使用某些过滤器时,比如用于列出指定用户拥有的进程的过滤器时,-e不会与–o一起使用。使用带有过滤器的-e将使过滤器无效,并显示所有进程条目。

一个例子如下。这里,comm代表 COMMAND,pcpu代表 CPU 使用率的百分比:

$ ps -eo comm,pcpu | head
COMMAND         %CPU
init             0.0
kthreadd         0.0
migration/0      0.0
ksoftirqd/0      0.0
watchdog/0       0.0
events/0         0.0
cpuset           0.0
khelper          0.0
netns            0.0

可以使用-o选项的不同参数及其描述如下:

参数 描述
pcpu CPU 百分比
pid 进程 ID
ppid 父进程 ID
pmem 内存百分比
comm 可执行文件名
cmd 简单命令
user 启动进程的用户
nice 优先级(niceness)
time 累积 CPU 时间
etime 进程启动后的经过时间
tty 关联的 TTY 设备
euid 有效用户
stat 进程状态

还有更多...

让我们通过进程操作命令的其他用法示例。

top

top对于系统管理员来说是一个非常重要的命令。top命令默认会输出 CPU 消耗最多的进程列表。该命令的使用如下:

$ top

它将显示与 CPU 消耗最多的进程相关的几个参数。

根据参数对 ps 输出进行排序

ps命令的输出可以根据指定的列使用--sort参数进行排序。

可以通过使用+(升序)或-(降序)前缀来指定升序或降序排序。

$ ps [OPTIONS] --sort -paramter1,+parameter2,parameter3..

例如,要列出前 10 个 CPU 消耗最多的进程,请使用:

$ ps -eo comm,pcpu --sort -pcpu | head
COMMAND         %CPU
Xorg             0.1
hald-addon-stor  0.0
ata/0            0.0
scsi_eh_0        0.0
gnome-settings-  0.0
init             0.0
hald             0.0
pulseaudio       0.0
gdm-simple-gree  0.0

在这里,进程按 CPU 使用率的降序排序,并且应用head来提取前 10 个进程。

我们可以使用grep来提取与给定进程名称或其他参数相关的ps输出条目。为了找出关于运行 bash 进程的条目,请使用:

$ ps -eo comm,pid,pcpu,pmem | grep bash
bash             1255  0.0  0.3
bash             1680  5.5  0.3

查找给定命令名称时的进程 ID

假设正在执行某个命令的几个实例,我们可能需要识别这些进程的进程 ID。可以通过使用pspgrep命令来找到这些信息。我们可以使用ps如下:

$ ps -C COMMAND_NAME

或者:

$ ps -C COMMAND_NAME -o pid=

在食谱的前面部分描述了-o用户定义格式说明符。但是在这里你可以看到=pid附加在一起。这是为了在ps的输出中移除标题 PID。为了移除每列的标题,将=附加到参数上。例如:

$ ps -C bash -o pid=
 1255
 1680

该命令列出 bash 进程的进程 ID。

另外,还有一个方便的命令叫做pgrep。您应该使用pgrep来快速获取特定命令的进程 ID 列表。例如:

$ pgrep COMMAND
$ pgrep bash
1255
1680

注意

pgrep只需要命令名称的一部分作为其输入参数来提取 Bash 命令,例如,pgrep ashpgrep bas也可以工作。但是ps需要您输入确切的命令。

pgrep接受许多其他输出过滤选项。为了指定一个分隔符字符来输出,而不是使用换行符作为分隔符,请使用:

$ pgrep COMMAND -d DELIMITER_STRING
$ pgrep bash -d ":"
1255:1680

指定匹配进程的用户所有者列表如下:

$ pgrep -u root,slynux COMMAND

在这个命令中,rootslynux是用户。

返回匹配进程的计数如下:

$ pgrep -c COMMAND

使用 ps 进行真实用户或 ID、有效用户或 ID 的过滤

使用ps可以根据指定的真实用户和有效用户名称或 ID 对进程进行分组。指定的参数可以用来通过检查每个条目是否属于特定的有效用户或真实用户的列表来过滤ps的输出,并仅显示与它们匹配的条目。可以按以下方式完成:

  • 通过使用-u EUSER1, EUSER2等来指定有效用户列表

  • 通过使用-U RUSER1, RUSER2等指定真实用户列表

例如:

$ ps -u root -U root -o user,pcpu

此命令将显示所有以root作为有效用户 ID 和真实用户 ID 运行的进程,并且还将显示用户和 CPU 使用率百分比列。

提示

通常,我们会在-eo中找到-o-e。但是当应用筛选器时,-o应该像上面提到的那样单独起作用。

用于 ps 的 TTY 筛选器

可以通过指定进程附加到的 TTY 来选择ps输出。使用-t选项来指定 TTY 列表如下:

$ ps -t TTY1, TTY2 ..

例如:

$ ps -t pts/0,pts/1
 PID TTY          TIME CMD
 1238 pts/0    00:00:00 bash
 1835 pts/1    00:00:00 bash
 1864 pts/0    00:00:00 ps

有关进程线程的信息

通常,关于进程线程的信息在ps输出中是隐藏的。我们可以通过添加-L选项来在ps输出中显示有关线程的信息。然后它将显示两列 NLWP 和 NLP。NLWP 是进程的线程计数,NLP 是 PS 中每个条目的线程 ID。例如:

$ ps -eLf

或者:

$ ps -eLf --sort -nlwp | head
UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
root       647     1   647  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon
root       647     1   654  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon
root       647     1   656  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon
root       647     1   657  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon
root       647     1   658  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon
root       647     1   659  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon
root       647     1   660  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon
root       647     1   662  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon
root       647     1   663  0   64 14:39 ?        00:00:00 /usr/sbin/console-kit-daemon --no-daemon

此命令列出了具有最大线程数的 10 个进程。

指定输出宽度和要显示的列

我们可以使用用户定义的输出格式说明符-o来指定要在ps输出中显示的列。另一种指定输出格式的方法是使用“标准”选项。根据您的使用风格进行练习。尝试这些选项:

  • -f ps –ef

  • u ps -e u

  • ps ps -e w(w 代表宽输出)

显示进程的环境变量

了解进程依赖的环境变量是我们可能需要的非常有用的信息。进程是否工作可能严重依赖于设置的环境变量。我们可以调试并利用环境数据来解决与进程运行相关的几个问题。

为了列出ps条目以及环境变量,请使用:

$ ps -eo cmd e

例如:

$ ps -eo pid,cmd  e | tail -n 3
 1162 hald-addon-acpi: listening on acpid socket /var/run/acpid.socket
 1172 sshd: slynux [priv]
 1237 sshd: slynux@pts/0
 1238 -bash USER=slynux LOGNAME=slynux HOME=/home/slynux PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games MAIL=/var/mail/slynux SHELL=/bin/bash SSH_CLIENT=10.211.55.2 49277 22 SSH_CONNECTION=10.211.55.2 49277 10.211.55.4 22 SSH_TTY=/dev/pts/0 TERM=xterm-color LANG=en_IN XDG_SESSION_COOKIE=d1e96f5cc8a7a3bc3a0a73e44c95121a-1286499339.592429-1573657095

这种类型的环境跟踪可以派上用场的一个例子是跟踪 apt-get 软件包管理器的问题。如果您使用 HTTP 代理连接到互联网,您可能需要设置环境变量http_proxy=host:port。但有时即使设置了,apt-get命令也不会选择代理,因此会返回错误。然后您可以实际查看环境变量并跟踪问题。

我们可能需要一些应用程序使用诸如crontab之类的调度工具自动运行。但它可能取决于一些环境变量。假设我们想在指定时间打开一个 GUI 窗口应用程序。我们使用crontab在指定时间安排它。但是,如果给出以下条目,您会注意到该应用程序不会在指定时间启动:

00 10 * * * /usr/bin/windowapp

这是因为窗口应用程序始终依赖于 DISPLAY 环境变量。环境变量需要传递给应用程序。

首先手动运行windowapp,然后运行ps -C windowapp -eo cmd e

查找环境变量。在crontab中出现命令名称之前加上前缀。问题将得到解决。

将条目修改如下:

00 10 * * * DISPLAY=:0 /usr/bin/windowapp

DISPLAY=:0可以从ps输出中获取。

另请参阅

  • 使用 cron 进行调度,解释如何安排任务

终止进程并发送或响应信号

终止进程是我们经常遇到的重要任务。有时我们可能需要终止程序的所有实例。命令行提供了几种终止程序的选项。关于类 UNIX 环境中进程的一个重要概念是信号。信号是一种用于中断运行进程以执行某些操作的进程间通信机制。程序的终止也是通过使用信号技术来执行的。本文介绍了信号和信号的使用。

准备就绪

信号是 Linux 中可用的进程间机制。我们可以通过使用特定的信号来中断一个进程。每个信号都与一个整数值相关联。当一个进程接收到一个信号时,它会通过执行一个信号处理程序来做出响应。在 Shell 脚本中,也可以发送和接收信号,并根据信号做出响应。KILL是用于终止进程的信号。诸如Ctrl + CCtrl + Z之类的事件也是信号的一种。kill命令用于向进程发送信号,trap命令用于处理接收到的信号。

如何做...

为了列出所有可用的信号,使用:

$ kill -l

它将打印信号编号和信号名称。

终止一个进程如下:

$ kill PROCESS_ID_LIST

kill命令默认发出 TERM 信号。进程 ID 列表应该用空格作为进程 ID 之间的分隔符来指定。

为了指定要通过kill命令发送的信号,使用:

$ kill -s SIGNAL PID

SIGNAL参数可以是信号名称或信号编号。虽然有许多为不同目的指定的信号,但我们通常只使用少数信号。它们如下:

  • SIGHUP 1——检测控制进程或终端死机

  • SIGINT 2——在按下Ctrl + C时发出的信号

  • SIGKILL 9——用于强制终止进程的信号

  • SIGTERM -15——默认用于终止进程的信号

  • SIGTSTP 20——在按下Ctrl + Z时发出的信号

我们经常使用强制终止进程。为了强制终止一个进程,使用:

$ kill -s SIGKILL PROCESS_ID

或者:

$ kill -9 PROCESS_ID

还有更多...

让我们浏览一下用于终止和发送信号的其他命令。

kill 命令的系列

kill命令以进程 ID 作为参数。kill系列中还有其他一些命令,它们接受命令名称作为参数并向进程发送信号。

killall命令按名称终止进程如下:

$ killall process_name

为了通过名称发送信号给一个进程使用:

$ killall -s SIGNAL process_name

为了通过名称强制终止进程使用:

$ killall -9 process_name

例如:

$ killall -9 gedit

通过使用指定的用户拥有的名称来指定进程的名称:

$ killall -u USERNAME process_name

为了在杀死进程之前进行交互式询问,使用killall命令和-i参数。

pkill命令类似于kill命令,但默认情况下接受进程名称而不是进程 ID。例如:

$ pkill process_name
$ pkill -s SIGNAL process_name

SIGNAL是信号编号。SIGNAL名称不支持pkill

它提供了许多与kill命令相同的选项。查看pkill手册以获取更多详细信息。

捕获和响应信号

trap是一个用于在脚本中为信号分配信号处理程序的命令。一旦使用trap命令为信号分配了一个函数,当脚本运行并接收到一个信号时,该函数将在接收到相应信号时被执行。

语法如下:

trap 'signal_handler_function_name' SIGNAL LIST

SIGNAL LIST由空格分隔。它可以是信号编号或信号名称。

让我们编写一个 Shell 脚本来响应SIGINT信号:

#/bin/bash
#Filename: sighandle.sh
#Description: Signal handler 

function handler()
{
  echo Hey, received signal : SIGINT
}

echo My process ID is $$
# $$ is a special variable that returns process ID of current process/script
trap 'handler' SIGINT
#handler is the name of the signal handler function for SIGINT signal

while true;
do
  sleep 1
done

在终端中运行这个脚本。当脚本运行时,如果按下Ctrl + C,它将通过执行与之关联的信号处理程序来显示消息。Ctrl + CSIGINT信号。

while循环用于通过使用无限循环使进程保持运行而不终止。因此,进程被无限地保持运行,以便它可以响应由另一个进程异步发送给进程的信号。用于无限保持进程运行的循环通常被称为事件循环。如果无限循环不可用,脚本将在执行语句后终止。但对于信号处理程序脚本,它必须等待并响应信号。

我们可以通过使用kill命令和脚本的进程 ID 向脚本发送信号:

$ kill -s SIGINT PROCESS_ID

上述脚本的PROCESS_ID在执行时将被打印出来。或者您可以使用ps命令找到它

如果没有为信号指定信号处理程序,它将调用操作系统分配的默认信号处理程序。通常,按下 Ctrl + C 将终止程序,因为操作系统提供的默认处理程序将终止进程。但是在这里定义的自定义处理程序指定了收到信号时的自定义操作。

我们可以为任何可用的信号(kill -l)定义信号处理程序,使用 trap 命令。还可以为多个信号设置单个信号处理程序。

which、whereis、file、whatis 和 loadavg 解释

这个配方旨在解释我们遇到的一些命令。了解这些命令对用户有帮助。

如何做...

让我们逐个命令及其用法示例。

  • which

which 命令用于查找命令的位置。我们在终端中键入命令时,并不知道可执行文件存储的位置。

当我们键入一个命令时,终端会在一组位置中查找该命令,并在找到位置时执行可执行文件。这组位置是使用环境变量 PATH 指定的。例如:

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

我们可以导出 PATH 并在键入命令名称时添加我们自己的位置以进行搜索。例如,要将 /home/slynux/bin 添加到 PATH,请使用以下命令:

$ export PATH=$PATH:/home/slynux/bin
# /home/slynux/bin is added to PATH

which 命令输出给定参数的命令位置。例如:

$ which ls
/bin/ls

  • whereis

whereis 类似于 which 命令。但它不仅返回命令的路径,还会打印 manpage 的位置(如果有的话),以及命令的源代码路径(如果有的话)。例如:

$ whereis ls
ls: /bin/ls /usr/share/man/man1/ls.1.gz

  • file

file 命令是一个有趣且经常使用的命令。它用于确定文件类型。

$ file FILENAME

这将打印有关文件的详细信息,包括文件类型。

一个例子如下:

$ file /bin/ls
/bin/ls: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, stripped

  • whatis

whatis 命令输出给定参数的命令的一行描述。它从 manpage 中解析信息。例如:

$ whatis ls
ls (1)               - list directory contents

提示

apropos

有时我们需要搜索与某个单词相关的命令是否存在。然后我们可以在命令的 manpages 中搜索字符串。为此,我们可以使用:

apropos COMMAND

  • 平均负载

平均负载是运行系统的总负载的重要参数。它指定系统上可运行进程的平均数量。它由三个值指定。第一个值表示一分钟的平均值,第二个表示五分钟的平均值,第三个表示 15 分钟的平均值。

可以通过运行 uptime 来获得。例如:

$ uptime
 12:40:53 up  6:16,  2 users,  load average: 0.00, 0.00, 0.00

向用户终端发送消息

系统管理员可能需要向网络上所有机器的每个用户或指定用户的终端屏幕发送消息。这个配方是执行这项任务的指南。

准备工作

wall 是一个用于在所有已登录用户的终端上写消息的命令。它可以用于向服务器或多个访问机器上的所有已登录用户传达消息。有时向所有用户发送消息可能不太有用。我们可能需要向特定用户或特定终端发送消息。终端在 Linux 系统中被视为设备,因此这些打开的终端将在 /dev/pts/ 下有一个相应的设备节点文件。向特定设备写入数据将在相应的终端上显示消息。

如何做...

为了向所有用户和所有已登录的终端广播消息,请使用:

$ cat message | wall

或:

$ wall < message
Broadcast Message from slynux@slynux-laptop
 (/dev/pts/1) at 12:54 ...

This is a message

消息概述将显示谁发送了消息(哪个用户和哪个主机)。如果其他用户发送消息,则消息将被显示到当前终端,只有在启用了“写消息”选项时才会显示。默认情况下,在大多数发行版中,“写消息”默认是启用的。如果消息的发送者是 root,则消息将显示在屏幕上,无论用户是否启用或禁用“写消息”选项。

为了启用写入消息,请使用:

$ mesg y

为了禁用写入消息,请使用:

$ mesg n

让我们编写一个专门向给定用户终端发送消息的脚本:

#/bin/bash
#Filename: message_user.sh
#Description: Script to send message to specified user logged terminals.
USER=$1

devices=`ls /dev/pts/* -l | awk '{ print $3,$9 }' | grep $USER | awk '{ print $2 }'`
for dev in $devices;
do
  cat /dev/stdin > $dev
done

按以下方式运行脚本:

./message_user.sh USERNAME < message.txt
# Pass message through stdin and username as argument

输出将如下所示:

$ cat message.txt
A message to slynux. Happy Hacking!
# ./message_user.sh slynux  < message.txt
# Run message_user.sh as root since the message is to be send to a specifc user.

现在,slynux 的终端将接收消息文本。

它是如何工作的...

/dev/pts目录将包含与系统上每个已登录终端对应的字符设备。我们可以通过查看设备文件的所有者来找出谁登录到哪个终端。ls -l输出将包含所有者名称和设备路径。这些信息是通过使用awk提取的。然后它使用grep仅提取对应于指定用户的行。用户名作为脚本的第一个参数被接受并存储为变量 USER。然后制作给定用户的终端列表。使用for循环来迭代每个设备路径。/dev/stdin将包含传递给当前进程的标准输入数据。因此,通过读取/dev/stdin,数据被读取并重定向到相应的终端(TTY)设备。因此消息被显示。

收集系统信息

从命令行收集有关当前系统的信息非常重要,用于记录系统数据。不同的系统信息数据包括主机名、内核版本、Linux 发行版名称、CPU 信息、内存信息、磁盘分区信息等。本教程将向您展示在 Linux 系统中收集有关系统信息的不同来源。

如何做...

为了打印当前系统的主机名,使用:

$ hostname

或者:

$ uname -n

通过使用以下方式打印有关 Linux 内核版本、硬件架构等的详细信息:

$ uname -a

为了打印内核版本,使用:

$ uname -r

按以下方式打印机器类型:

$ uname –m

为了打印有关 CPU 详细信息,使用:

$ cat /proc/cpuinfo

为了提取处理器名称,使用:

$ cat /proc/cpuinfo | head -n 5 | tail -1

第五行包含处理器名称。因此,首先提取前五行。然后提取最后一行以打印处理器名称。

按以下方式打印有关内存或 RAM 的详细信息:

$ cat /proc/meminfo

按以下方式打印系统上可用的总内存(RAM):

$ cat /proc/meminfo  | head -1
MemTotal:        1026096 kB

为了列出系统上可用的分区信息,使用:

$ cat /proc/partitions

或者:

$ fdisk -l

按以下方式获取有关系统的完整详细信息:

$ lshw

使用/proc – 收集信息

/proc是 GNU/Linux 操作系统上可用的内存伪文件系统。它被引入以提供一个接口,从用户空间读取几个系统参数。这非常有趣,我们可以从中收集大量信息。让我们看看proc文件系统提供的一些功能。

如何做...

如果您查看/proc,您可以看到几个文件和目录。其中一些在本章的另一个教程中已经解释过。您可以简单地cat文件和子目录中的文件,以获取信息。所有这些都是格式良好的文本。

在系统上运行的每个进程都将在/proc中有一个目录。在/proc中进程的目录名称与该进程的进程 ID 相同。

假设对于 Bash,进程 ID 为 4295(pgrep bash),/proc/4295将存在。与该进程对应的每个目录都将包含有关该进程的大量信息。/proc/PID中的一些重要文件如下。

  • environ—包含与该进程关联的环境变量。

通过cat /proc/4295/environ,我们可以显示传递给该进程的所有环境变量。

  • cwd—是进程的工作目录的符号链接。

  • exe—是当前进程的运行可执行文件的符号链接。

$ readlink /proc/4295/exe
/bin/bash

  • fd—是由进程使用的文件描述符条目组成的目录。

使用 cron 进行调度

在给定时间或给定时间间隔执行脚本是一个常见的需求。GNU/Linux 系统配备了不同的实用程序来安排任务。cron就是这样一个实用程序,它允许任务在系统的后台通过cron守护程序定期自动运行。cron实用程序使用一个名为“cron 表”的文件,其中存储了要执行的脚本或命令的时间表以及它们要执行的时间。这是一个非常有用的实用程序。一个常见的用法是在空闲时间(某些 ISP 提供免费使用的时间,通常是在大多数人睡觉的夜间)安排从互联网下载文件。用户不需要在夜间醒来开始下载。用户可以编写一个 cron 条目并安排下载。您还可以安排在免费使用时间结束时自动断开互联网连接并关闭系统。

准备工作

cron 调度实用程序默认随所有 GNU/Linux 发行版一起提供。一旦我们编写了 cron 表条目,命令将在指定的执行时间执行。crontab命令用于向 cron 调度域添加调度条目。cron 调度是一个简单的文本文件。每个用户都有自己的 cron 调度。cron 调度通常称为 cron 作业。

如何做…

为了安排任务,我们应该知道编写 cron 表的格式。cron 作业指定要执行的脚本或命令的路径以及要执行的时间。每个 cron 表由以下顺序的六个部分组成:

  • 分钟(0-59)

  • 小时(0-23)

  • 天(1-31)

  • 月(1-12)

  • 工作日(0-6)

  • COMMAND(要在指定时间执行的脚本或命令)

前五个部分指定要执行命令的实例的时间。还有一些其他选项可以指定时间表。

星号(*)用于指定命令应在每个时间实例执行。也就是说,如果在 cron 作业的小时字段中写入*,则命令将每小时执行一次。同样,如果您想要在特定时间段的多个实例执行命令,请在相应的时间字段中用逗号分隔指定时间段(例如,要在第五分钟和第十分钟运行命令,请在分钟字段中输入5,10)。我们还有另一个很好的选项,可以在特定的时间间隔运行命令。在分钟字段中使用*/5以在每五分钟运行一次命令。我们可以将此应用于任何时间字段。cron 表条目可以包含一个或多个 cron 作业的行。cron 表条目中的每一行都是一个作业。例如:

  • 让我们写一个示例crontab条目以进行说明:
02 * * * * /home/slynux/test.sh

此 cron 作业将在所有小时的所有天的第二分钟执行test.sh脚本。

  • 为了在所有天的第五、第六和第七小时运行脚本,使用:
00 5,6,7 * * /home/slynux/test.sh
  • 在每个星期日的每个小时执行script.sh如下:
00 */12 * * 0 /home/slynux/script.sh
  • 每天凌晨 2 点关闭计算机如下:
00 02 * * * /sbin/shutdown -h

现在,让我们看看如何安排 cron 作业。您可以以多种方式执行crontab命令以安排脚本。

当您手动运行crontab时,使用-e选项输入 cron 作业:

$ crontab –e
02 02 * * * /home/slynux/script.sh

输入crontab -e后,将打开默认的文本编辑器(通常是 vi),用户可以输入 cron 作业并保存。此 cron 作业将按指定的时间间隔进行调度和执行。

通常在脚本中调用crontab命令进行任务调度时,我们通常使用另外两种方法:

  1. 创建一个文本文件(例如task.cron)并编写 cron 作业。

然后使用文件名作为命令参数运行crontab

$ crontab task.cron

  1. 通过使用下一种方法,我们可以在不创建单独文件的情况下指定内联的 cron 作业。例如:
crontab<<EOF
02 * * * * /home/slynux/script.sh
EOF

cron 作业需要在crontab<<EOF 和 EOF之间编写。

使用crontab命令执行 Cron 作业时具有特权。如果需要执行需要更高特权的命令,比如关机命令,需要以 root 身份运行crontab命令。

在 cron 作业中指定的命令使用完整路径写入。这是因为 cron 作业执行的环境与我们在终端上执行的环境不同。因此PATH环境变量可能未设置。如果您的命令需要设置某些环境变量才能运行,您应该显式设置环境变量。

还有更多…

crontab命令还有更多选项。让我们看一些。

指定环境变量

许多命令要求环境变量正确设置才能执行。我们可以通过在用户的 cron 表中插入一个带有变量赋值语句的行来设置环境变量。

例如,如果您使用代理服务器连接到互联网,要安排使用互联网的命令,您必须设置 HTTP 代理环境变量http_proxy。可以按以下方式完成:

crontab<<EOF
http_proxy=http://192.168.03:3128
00 * * * * /home/slynux/download.sh
EOF

查看 cron 表

我们可以使用-l选项列出现有的 cron 作业:

$ crontab –l
02 05 * * * /home/user/disklog.sh

crontab -l列出当前用户的 cron 表中的现有条目。

我们还可以通过使用-u选项指定用户名来查看其他用户的 cron 表,如下所示:

$ crontab –l –u slynux
09 10 * * * /home/slynux/test.sh

当使用-u选项获取更高特权时,应以 root 身份运行。

删除 cron 表

我们可以使用-r选项删除当前用户的 cron 表:

$ crontab –r

为了删除另一个用户的 cron 表,使用:

# crontab –u slynux –r

以 root 身份运行以获得更高特权。

从 Bash 中写入和读取 MySQL 数据库

MySQL 是一个广泛使用的数据库系统。通常,MySQL 数据库用作使用 PHP、Python、C++等语言编写的应用程序的存储系统。从 shell 脚本访问和操作 MySQL 数据库将会很有趣。我们可以编写脚本将文本文件或 CSV(逗号分隔值)中的内容写入表中,并与 MySQL 数据库交互以读取和操作数据。例如,我们可以通过从 shell 脚本运行查询来读取存储在留言板程序数据库中的所有电子邮件地址。在本教程中,我们将看到如何从 Bash 中读取和写入 MySQL 数据库。例如,这是一个示例问题:

我有一个包含学生详细信息的 CSV 文件。我需要将文件内容插入数据库表中。根据这些数据,我需要为每个部门生成一个单独的排名表。

准备工作

为了处理 MySQL 数据库,您应该在系统上安装 mysql-server 和 mysql-client 软件包。这些工具不会默认随 Linux 发行版提供。由于 MySQL 带有用于身份验证的用户名和密码,您应该有一个用户名和密码来运行脚本。

如何做…

可以使用 Bash 实用程序(如sortawk等)解决上述问题。或者,我们可以通过使用 SQL 数据库表来解决。我们将为创建数据库和表、将学生数据插入表中以及从表中读取和显示处理后的数据编写三个脚本。

创建数据库和表的脚本如下:

#!/bin/bash
#Filename: create_db.sh
#Description: Create MySQL database and table

USER="user"
PASS="user"

mysql -u $USER -p$PASS <<EOF 2> /dev/null
CREATE DATABASE students;
EOF

[ $? -eq 0 ] && echo Created DB || echo DB already exist 

mysql -u $USER -p$PASS students <<EOF 2> /dev/null
CREATE TABLE students(
id int,
name varchar(100),
mark int,
dept varchar(4)
);
EOF

[ $? -eq 0 ] && echo Created table students || echo Table students already exist 

mysql -u $USER -p$PASS students <<EOF
DELETE FROM students;
EOF

插入数据到表的脚本如下:

#!/bin/bash
#Filename: write_to_db.sh
#Description: Read from CSV and write to MySQLdb

USER="user"
PASS="user"

if [ $# -ne 1 ];
then
  echo $0 DATAFILE
  echo
  exit 2
fi

data=$1

while read line;
do

  oldIFS=$IFS
  IFS=,
  values=($line)
  values[1]="\"`echo ${values[1]} | tr ' ' '#' `\""
  values[3]="\"`echo ${values[3]}`\""

  query=`echo ${values[@]} | tr ' #' ', ' `
  IFS=$oldIFS

  mysql -u $USER -p$PASS students <<EOF
INSERT INTO students VALUES($query);
EOF

done< $data
echo Wrote data into DB

从数据库查询的脚本如下:

#!/bin/bash
#Filename: read_db.sh
#Description: Read from the database

USER="user"
PASS="user"

depts=`mysql -u $USER -p$PASS students <<EOF | tail -n +2
SELECT DISTINCT dept FROM students;
EOF`

for d in $depts;
do

echo Department : $d

result="`mysql -u $USER -p$PASS students <<EOF
SET @i:=0;
SELECT @i:=@i+1 as rank,name,mark FROM students WHERE dept="$d" ORDER BY mark DESC;
EOF`"

echo "$result"
echo

done

输入 CSV 文件(studentdata.csv)的数据如下:

1,Navin M,98,CS
2,Kavya N,70,CS
3,Nawaz O,80,CS
4,Hari S,80,EC
5,Alex M,50,EC
6,Neenu J,70,EC
7,Bob A,30,EC
8,Anu M,90,AE
9,Sruthi,89,AE
10,Andrew,89,AE

按以下顺序执行脚本:

$ ./create_db.sh 
Created DB
Created table students

$ ./write_to_db.sh studentdat.csv
Wrote data into DB

$ ./read_db.sh 
Department : CS
rank  name  mark
1  Navin M  98
2  Nawaz O  80
3  Kavya N  70

Department : EC
rank  name  mark
1  Hari S  80
2  Neenu J 70
3  Alex M  50
4  Bob A   30

Department : AE
rank  name  mark
1  Anu M    90
2  Sruthi   89
3  Andrew   89

它是如何工作的…

我们现在将逐个查看上述脚本的解释。第一个脚本create_db.sh用于创建名为students的数据库和其中的名为students的表。我们需要 MySQL 用户名和密码来访问或修改 DBMS 中的数据。变量USERPASS用于存储用户名和密码。mysql命令用于 MySQL 操作。mysql命令可以通过-u指定用户名,并通过-pPASSWORD指定密码。mysql命令的另一个命令参数是数据库名称。如果将数据库名称指定为mysql命令的参数,它将用于数据库操作,否则我们必须在 SQL 查询中明确指定要使用的数据库名称。mysql命令接受要通过标准输入(stdin)执行的查询。通过<<EOF方法通过stdin提供多行的便捷方式。出现在<<EOFEOF之间的文本将作为标准输入传递给mysql。在CREATE DATABASE查询中,我们已将stderr重定向到/dev/null,以防止显示错误消息。此外,在表创建查询中,我们已将stderr重定向到/dev/null以忽略任何错误。然后,我们使用退出状态变量$?检查mysql命令的退出状态,以了解表或数据库是否已经存在。如果数据库或表已经存在,则显示消息通知。否则,我们将创建它们。

下一个脚本write_to_db.sh接受一个学生数据 CSV 文件的文件名。我们使用while循环读取 CSV 文件的每一行。因此,在每次迭代中,将收到一个逗号分隔值的行。然后,我们需要将行中的值组成一个 SQL 查询。为此,将数据项存储在逗号分隔行中的最简单方法是使用数组。我们知道数组赋值的形式是array=(val1 val2 val3)。这里空格字符是内部字段分隔符IFS)。我们有一个逗号分隔值的行,因此通过将 IFS 更改为逗号,我们可以轻松地将值分配给数组(IFS=,)。逗号分隔行中的数据项是idnamemarkdepartmentidmark是整数值,而namedept是字符串(字符串必须用引号括起来)。另外,名字中可能包含空格字符。空格可能与内部字段分隔符冲突。因此,我们应该用某个字符(#)替换名字中的空格,并在构建查询后再替换它。为了引用字符串,数组中的值用\"前缀和后缀。tr用于将名字中的空格替换为#。最后,通过将空格字符替换为逗号并将#替换为空格来形成查询,并执行此查询。

第三个脚本read_db.sh用于查找部门并打印每个部门学生的排名列表。第一个查询用于查找部门的不同名称。我们使用while循环来遍历每个部门,并运行查询以按最高分显示学生详细信息。SET @i=0是一个 SQL 构造,用于设置变量i=0。在每一行上,它都会递增,并显示为学生的排名。

用户管理脚本

GNU/Linux 是一个多用户操作系统。许多用户可以登录并同时执行多项活动。有几个管理任务是通过用户管理来处理的。任务包括为用户设置默认 shell,禁用用户帐户,禁用 shell 帐户,添加新用户,删除用户,设置密码,为用户帐户设置到期日期等。本文旨在编写一个用户管理工具,可以处理所有这些任务。

如何做…

让我们来看一下用户管理脚本:

#!/bin/bash
#Filename: user_adm.sh
#Description: A user administration tool

function usage()
{
  echo Usage:
  echo Add a new user
  echo $0 -adduser username password
  echo
  echo Remove an existing user
  echo $0 -deluser username
  echo
  echo Set the default shell for the user
  echo $0 -shell username SHELL_PATH
  echo
  echo Suspend a user account
  echo $0 -disable username
  echo
  echo Enable a suspended user account
  echo $0 -enable username
  echo
  echo Set expiry date for user account
  echo $0 -expiry DATE 
  echo
  echo Change password for user account
  echo $0 -passwd username
  echo
  echo Create a new user group
  echo $0 -newgroup groupname
  echo
  echo Remove an existing user group
  echo $0 -delgroup groupname
  echo
  echo Add a user to a group
  echo $0 -addgroup username groupname
  echo
  echo Show details about a user
  echo $0 -details username
  echo
  echo Show usage
  echo $0 -usage
  echo

  exit
}

if [ $UID -ne 0 ];
then
  echo Run $0 as root.
  exit 2
fi

case $1 in

  -adduser) [ $# -ne 3 ] && usage ; useradd $2 -p $3 -m ;; 
  -deluser) [ $# -ne 2 ] && usage ; deluser $2 --remove-all-files;;
  -shell)    [ $# -ne 3 ] && usage ; chsh $2 -s $3 ;;
  -disable) [ $# -ne 2 ] && usage ; usermod -L $2 ;; 
  -enable) [ $# -ne 2 ] && usage ; usermod -U $2  ;;
  -expiry) [ $# -ne 3 ] && usage ; chage $2 -E $3 ;;
  -passwd) [ $# -ne 2 ] && usage ; passwd $2 ;;
  -newgroup) [ $# -ne 2 ] && usage ; addgroup $2 ;;
  -delgroup) [ $# -ne 2 ] && usage ; delgroup $2 ;;
  -addgroup) [ $# -ne 3 ] && usage ; addgroup $2 $3 ;;
  -details) [ $# -ne 2 ] && usage ; finger $2 ; chage -l $2 ;;
  -usage) usage ;;
  *) usage ;;
esac

示例输出如下:

# ./user_adm.sh -details test
Login: test                 Name: 
Directory: /home/test                 Shell: /bin/sh
Last login Tue Dec 21 00:07 (IST) on pts/1 from localhost
No mail.
No Plan.
Last password change          : Dec 20, 2010
Password expires          : never
Password inactive         : never
Account expires             : Oct 10, 2010
Minimum number of days between password change    : 0
Maximum number of days between password change    : 99999
Number of days of warning before password expires  : 7

它是如何工作的…

user_adm.sh脚本可用于执行许多用户管理任务。您可以按照usage()文本使用脚本。定义了一个usage()函数,用于显示如何使用脚本以及用户给出的任何参数出错或运行了–usage参数时如何执行脚本。使用case语句来匹配命令参数并根据匹配情况执行相应的命令。user_adm.sh脚本的有效命令选项包括:-adduser-deluser-shell-disable-enable-expiry-passwd-newgroup-delgroup-addgroup-details-usage。当匹配到*) case 时,意味着是错误的选项,因此调用usage()。对于每个匹配情况,我们使用[ $# -ne 3 ] && usage。用于检查参数的数量。如果命令参数的数量不等于所需数量,则调用usage()函数,脚本将在不执行更多操作的情况下退出。为了运行用户管理命令,脚本需要以 root 身份运行。因此,会检查用户 ID 0(root 用户 ID 为 0)。如果用户 ID 为非零,则表示正在以非 root 身份执行。因此,会显示一个以 root 身份运行的消息,并退出脚本。

让我们逐一解释每种情况:

  • -useradd

useradd命令可用于创建新用户。它的语法是:

useradd USER –p PASSWORD

-m选项用于创建主目录

还可以使用–c FULLNAME选项提供用户的全名。

  • -deluser:

deluser命令可用于删除用户。语法是:

deluser USER

--remove-all-files用于删除与用户相关的所有文件,包括主目录。

  • -shell

chsh命令用于更改用户的默认 shell。语法是:

chsh USER –s SHELL
  • -disable–enable

usermod命令用于操纵与用户帐户相关的多个属性。

usermod –L USER锁定用户帐户,usermod –U USER解锁用户帐户。

  • -expiry

chage命令用于操纵用户帐户到期信息。语法是:

chage –E DATE

还有其他选项如下:

  • -m MIN_DAYS(将密码更改之间的最小天数设置为MIN_DAYS

  • -M MAX_DAYS(设置密码有效的最大天数)

  • -W WARN_DAYS(设置密码更改所需的警告天数)

  • -passwd

passwd命令用于更改用户的密码。语法是:

passwd USER

该命令将提示输入新密码。

  • -newgroupaddgroup

addgroup命令将在系统中添加一个新的用户组。语法是:

addgroup GROUP

要将现有用户添加到组中,请使用:

addgroup USER GROUP

-delgroup

delgroup命令将删除用户组。语法是:

delgroup GROUP

  • -details

finger USER命令将显示用户的用户信息,其中包括用户主目录路径、上次登录时间、默认 shell 等。chage –l命令将显示用户帐户到期信息。

批量图像调整大小和格式转换

我们所有人都使用数码相机并从相机以及互联网下载照片。当我们需要处理大量图像文件时,我们可以使用脚本轻松地对文件进行批量操作。我们经常遇到的一个常规任务是调整文件大小。此外,还会用到格式转换,将一种图像格式转换为另一种格式(例如,将 JPEG 转换为 PNG)。当我们从相机下载图片时,大分辨率图片会占用大量空间。但我们可能需要尺寸较小的图片,以便在互联网上存储和发送电子邮件。因此,我们将其调整为较低的分辨率。本文将讨论如何使用脚本进行图像管理。

准备工作

Imagemagick是一个优秀的图像处理工具,可以处理多种图像格式和不同的构造,并提供丰富的选项。大多数 GNU/Linux 发行版都没有预装 Imagemagick。您需要手动安装该软件包。convert是我们经常使用的命令。

如何做…

为了将一种图像格式转换为另一种图像格式,请使用:

$ convert INPUT_FILE OUTPUT_FILE

例如:

$ convert file1.png file2.png

我们可以通过指定缩放百分比或指定输出图像的宽度和高度来将图像大小调整为指定的图像大小。

通过以下方式指定WIDTHHEIGHT来调整图像大小:

$ convert image.png -resize WIDTHxHEIGHT image.png

例如:

$ convert image.png -resize 1024x768 image.png

需要提供WIDTHHEIGHT中的一个,以便另一个将自动计算并调整大小,以保持图像大小比例:

$ convert image.png -resize WIDTHx image.png

例如:

$ convert image.png -resize 1024x image.png

通过指定百分比缩放因子来调整图像大小,如下所示:

$ convert image.png -resize "50%" image.png

让我们看一个图像管理的脚本:

#!/bin/bash
#Filename: image_help.sh
#Description: A script for image management

if [ $# -ne 4 -a $# -ne 6 -a $# -ne 8 ];
then
  echo Incorrect number of arguments
  exit 2
fi

while [ $# -ne 0 ];
do

  case $1 in
  -source) shift; source_dir=$1 ; shift ;;
  -scale) shift; scale=$1 ; shift ;;
  -percent) shift; percent=$1 ; shift ;;
  -dest) shift ; dest_dir=$1 ; shift ;;
  -ext) shift ; ext=$1 ; shift ;;
  *) echo Wrong parameters; exit 2 ;;
  esac;

done

for img in `echo $source_dir/*` ;
do
  source_file=$img
  if [[ -n $ext ]];
  then
    dest_file=${img%.*}.$ext
  else
    dest_file=$img
  fi

  if [[ -n $dest_dir ]];
  then
    dest_file=${dest_file##*/}
    dest_file="$dest_dir/$dest_file"
  fi

  if [[ -n $scale ]];
  then
    PARAM="-resize $scale"
  elif [[ -n $percent ]];
  then
    PARAM="-resize $percent%"	
  fi

  echo Processing file : $source_file
  convert $source_file $PARAM $dest_file

done

以下是一个示例输出,将目录sample_dir中的图像缩放到20%大小:

$ ./image_help.sh -source sample_dir -percent 20%
Processing file :sample/IMG_4455.JPG
Processing file :sample/IMG_4456.JPG
Processing file :sample/IMG_4457.JPG
Processing file :sample/IMG_4458.JPG

为了将图像缩放到宽度 1024,请使用:

$ ./image_help.sh -source sample_dir –scale 1024x

将文件更改为 PNG 格式,方法是在上述命令中添加-ext png

按照以下方式指定目标目录来缩放或转换文件:

$ ./image_help.sh -source sample -scale 50% -ext png -dest newdir
# newdir is the new destination directory

工作原理…

上述image_help.sh脚本可以接受多个命令行参数,例如-source-percent-scale-ext-dest。以下是对每个参数的简要解释:

  • -source参数用于指定图像的源目录。

  • -percent参数用于指定缩放百分比,-scale用于指定缩放宽度和高度。

  • 要么使用-percent,要么使用-scale。它们两者不会同时出现。

  • -ext参数用于指定目标文件格式。-ext是可选的;如果未指定,将不执行格式转换。

  • -dest参数用于指定缩放或转换图像文件的目标目录。-dest是可选的。如果未指定-dest,目标目录将与源目录相同。在脚本的第一步中,它检查给定给脚本的命令参数数量是否正确。可以出现 4 个、6 个或 8 个参数。

现在,通过使用while循环和case语句,我们将解析与变量对应的命令行参数。$#是一个特殊变量,返回参数的数量。shift命令将命令参数向左移动一个位置,这样在每次执行shift时,我们可以通过相同的$1变量访问一个一个的命令参数,而不是使用$1$2$3等。case语句匹配$1的值。这类似于 C 编程语言中的 switch 语句。当匹配到一个 case 时,执行相应的语句。每个匹配的 case 语句以;;结束。一旦所有参数都被解析为变量percentscalesource_dirextdest_dir,就会使用for循环来遍历源目录中每个文件的路径,并执行相应的操作来转换文件。

如果变量ext已定义(如果命令参数中给出了-ext),则将目标文件的扩展名从source_file.extension更改为source_file.$ext。在下一条语句中,它检查是否提供了-dest参数。如果指定了目标目录,则通过使用文件名切片,将源路径中的目录替换为目标目录来构建目标文件路径。在下一条语句中,它为convert命令构建参数以执行调整大小(-resize widthx-resize perc%)。参数构建完成后,使用正确的参数执行convert命令。

另请参阅

  • 基于第二章的扩展名切片,解释了如何提取文件名的部分
posted @ 2024-05-16 19:07  绝不原创的飞龙  阅读(16)  评论(0编辑  收藏  举报