Linux-Shell-编程秘籍-三-

82 阅读27分钟

Linux Shell 编程秘籍(三)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第三章:文件输入,文件输出

在本章中,我们将涵盖:

  • 生成任意大小的文件

  • 文本文件上的交集和集差(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结合使用的组合才会打印目录。

  • lsFgrep"/ 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开头的行的字符串。
$行尾标记。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 2040%. 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 2040%. 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

另请参阅

  • 在文件中迭代行、单词和字符,解释了从单词中切片一个字符