LinuxShell脚本攻略(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-09 23:34:55

点击下载

作者:(印)SarathLakshman著

出版社:北京图灵文化发展有限公司

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

LinuxShell脚本攻略

LinuxShell脚本攻略试读:

前言

GNU/Linux可谓是一款不同凡响的操作系统,它拥有一个稳定、可靠且极其强大的完备的开发环境。作为与操作系统进行沟通的原生界面(native interface),shell能够控制整个操作系统的运作。理解shell脚本可以让你更好地了解操作系统,同时还能帮助你通过短短几行脚本自动地将大部分手头工作搞定,从而节省大量的时间。shell脚本可以和许多外部命令行工具结合起来完成信息查询、简化文本处理、调度任务运行时间、生成报表以及发送邮件之类的工作。尽管不少shell命令也配有对应的文档,但是仍然不太好理解。本书收集了诸多适合于实战的命令行脚本攻略,同时辅以详细的讲解。内容上涵盖了大多数重要的Linux命令的用法,其中包括大量的实例。本书能够帮你借助几个命令来完成涉及文本处理、文件管理、备份等任务的繁杂的数据处理工作。

仅凭一行代码就能搞定复杂的文本处理任务,你想成为这样的命令行高手吗?想过写几个shell脚本和报表工具来找点儿乐子,或是做点动真格的系统管理工作吗?那么本书就是为你量身打造的。好了,开始上路吧!

本书内容

第1章涵盖了如终端打印、数学运算、数组、操作符、函数、别名、文件重定向等可以通过Bash脚本来完成的一系列初级任务。作为入门篇,本章目的在于让读者掌握Bash中的基本概念及特性。

第2章展示了GNU/Linux下多个命令在不同情境下的实战用法。介绍了cat、md5sum、find、tr、sort、uniq、split、rename、look等重要命令。本章考查了用户可能会遇到并可借鉴的各种切实可行的用例。

第3章包含了多个与文件和文件系统相关的任务攻略。本章演示了如何生成大体积文件,将文件系统写入文件并挂载,查找并删除重复文件,统计文件行数,创建ISO镜像,收集文件细节信息、符号链接操作、文件权限及属性的详情,等等。

第4章以大量实例讲解了GNU/Linux下大部分命令行文本处理工具,同时还细致描述了正则表达式及sed和awk等命令。本章在各种实例中就大多数常见的文本处理任务,详细地剖析了其解决方案。

第5章包含了多个与Internet和Web相关的shell脚本,旨在帮助读者了解如何使用shell脚本同Web打交道,从而实现采集及解析Web页面数据,以POST和GET的方式发送用户数据,编写Web服务的客户端,下载Web页面等任务的自动化处理。

第6章结合脚本实例,演示了用于数据备份、归档、压缩等的若干命令以及用法。本章还介绍了tar、gzip、bunzip、cpio、lzma、dd、rsync、git、squashfs等命令,并讨论了一些重要的加密技术。

第7章讨论了Linux环境下的联网实践以及一些有助于编写基于网络的shell脚本的命令。为了照顾新手,本章一开头先介绍了一些网络基础知识。接下来的重头戏包括借助SSH实现无密码登录,通过网络传送文件,列出网络中的活动主机,以多播方式进行消息传播,等等。

第8章考查了Linux系统活动监视相关的实例以及日志记录和报表生成。本章讲解了诸如计算磁盘使用情况、监视用户访问、CPU占用、syslog、查看常用命令等任务。

第9章包含一系列系统管理方面的实战攻略。它介绍了用于完成系统信息采集、使用脚本进行用户管理、向用户发送消息、大图片缩放、通过shell访问MySQL数据库等任务的各种命令。

阅读本书的前提

只要具备任何一种GNU/Linux平台的一般性使用经验都有助于你更轻松地阅读本书。我们已竭尽所能地确保书中的所有例子清晰明了,并尽可能简单易懂。在Linux平台下学习的好奇心是你阅读本书所需的唯一条件。我们为你提供了循序渐进的辅导来解决书中有关脚本编写的难题。为了运行并测试书中的例子,我们推荐安装Ubuntu Linux。当然,其他的Linux发行版也足以胜任绝大多数任务。你会发现就编写shell脚本来说,本书绝对是一份通俗易懂的参考资料,同时它也是一位帮你编写出高效脚本的良师益友。

本书读者

如果你是一位初、中级用户,并且希望通过掌握快速编写脚本的技巧来完成各类事务处理,而又不愿去逐页翻阅手册,那么本书就是写给你的。你不用了解任何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中的SUGGESTA TITLE表格内留言,或者发送邮件至suggest@packtpub.com。

如果你在某方面有所专长并且愿意参与图书编写,请参阅我们的作者指南www.packtpub.com/authors。

客户支持

现在你已经拥有了这本由Packt出版的图书,为了让你的付出得到最大的回报,我们还为你提供了其他许多方面的服务。

下载本书的示例代码

如果你是通过http://www.packtpub.com的注册账户购买的图书,可以从该账户中下载你购买过的所有Packt图书中的示例代码。如果你是从其他地方购买本书的,可以访问http://www.packtpub.com/support并进行注册,我们将会为您发送一封附有示例代码文件的电子邮件。

勘误

尽管我们已经竭尽全力确保本书内容准确,但错误终难避免。如果你发现了书中的任何错误,无论是出现在正文还是代码中的,我们都非常乐于见到你将错误提交给我们。这样不仅能够减少其他读者的困惑,还能帮助我们改进本书后续版本的质量。如果需要提交勘误,请访问http://www.packtpub.com/support,选择相应的书名,单击勘误提交表格链接,就可以开始输入你的勘误信息了。一旦通过验证,我们将接受你的提交,同时勘误内容也将被上传到我们的网站,或者被添加到对应书目勘误区的现有勘误表中。任何图书当前的勘误都可以通过http://www.packtpub.com/support来查看。

侵权行为

各种媒体在Internet上一直饱受版权侵害的困扰。Packt坚持对版权和授权进行全力保护。如果你在Internet上发现我社图书的任何形式的盗版,请立即为我们提供地址或网站名称,以便我们采取进一步的措施。

疑难解答

如果你对本书的某方面抱有疑问,请通过questions@packtpub.com联系我们,我们会尽力为你解决。

致谢

我要感谢一直以来给予我极大支持和鼓励的好友和家人。感谢我的朋友Anu Mahadevan和Neenu Jacob,他们热情并耐心地通读了所有章节,更在此书的编写过程中提出了一些修改意见。同样要感谢Atanu Datta先生,他帮助我构思出了章节的标题。最后,非常感谢Packt出版社的工作人员,感谢他们帮助我,才使此书得以出版。第1章 小试牛刀

本章内容

□终端打印        □获取、设置日期及延时

□玩转变量与环境变量   □调试脚本

□通过shell进行数学运算   □函数和参数

□玩转文件描述符与重定向 □读取命令序列输出

□数组和关联数组     □以不按回车键的方式获取字符"n"

□使用别名        □字段分隔符和迭代器

□获取终端信息      □比较与测试

1.1 简介

诸多类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 user)。超级用户是Linux系统中权限最高的用户。shell脚本通常是一个以#!起始的文本文件,如下所示:

#!/bin/bash①

Linux环境下的任何脚本语言,都是以这样一个被称为shebang 的特殊行作为起始的。在这行中,字符#!被置于解释器路径之前。/bin/bash是Bash的路径。

注释:① shebang这个词其实是两个字符名称的组合。在Unix的行话里,用sharp或hash(有时候是mesh)来称呼字符“#”,用bang来称呼惊叹号“!”,因而shebang合起来就代表了这两个字符。详情请参考:http://en.wikipedia.org/wiki/Shebang_(Unix)。——译者注(书中所有的注均为译者注。)

有两种运行脚本的方式。一种是将脚本作为sh的命令行参数,另一种是将脚本作为具有执行权限的可执行文件。

将脚本作为命令行参数时的运行方式如下:sh script.sh # 假设脚本位于当前目录下

或者sh /home/path/script.sh # 使用script.sh的完整路径

如果将脚本作为sh的命令行参数来运行,那么脚本中的shebang行也就没什么用处了。

为了使shell脚本能够自己独立运行,需要具备可执行权限。要使脚本独立运行,必须利用shebang行。它通过使用位于#!之后的解释器来运行脚本。至于脚本的可执行权限,可以通过以下方式设置:chmod a+x script.sh

该命令赋予所有用户script.sh文件的可执行权限。这个脚本能以下列方式执行:./script.sh #./ 表示当前目录

或者/home/path/script.sh # 使用脚本的完整路径

shell程序读取脚本的首行,查看shebang行是否为#!/bin/bash。它会识别/bin/bash,并在内部以如下命令行执行该脚本:/bin/bash script.sh

当打开一个终端的时候,该终端最初会执行一组命令来定义诸如提示文本、颜色等各类设置。这组命令来自位于用户home目录中的.bashrc脚本文件(~/.bashrc)。Bash还维护了一个历史记录文件~/.bash_history,用于保存用户运行过的命令。~是一种简写,代表用户home目录的路径。

在Bash中,每个命令或是命令序列是通过使用分号或换行符来分隔的。比如:cmd1 ; cmd2

它等同于:cmd1cmd2

字符#指明注释的开始。注释部分以#为起始,一直延续到行尾。注释行通常用于为代码提供注释信息,或者用于暂停执行某行代

①码。

注释:① shell不执行脚本代码中的任何注释部分。

现在让我们继续。

1.2 终端打印

终端作为交互式工具,用户可以通过它与shell环境进行交互。在终端中打印文本是绝大多数shell脚本和工具日常需要进行的基本任务。能够执行打印的方法有很多,格式也各有不同。

1.2.1 实战演练

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;hello为例,echo hello被视为一个命令,第二个hello则被视为另一个命令。

□ 使用带单引号的echo时,Bash不会对单引号中的变量(如var)求值,而只是照原样显示。

这就意味着:echo 'var'将会返回var,而echovar将会根据变量var定义与否,返回var的值,或者什么都不返回。

另一个可用于终端打印的命令是printf。printf使用的参数和C语言中的printf函数一样。例如:printf "Hello world"

printf使用引用文本或由空格分隔的参数。我们可以在printf中使用格式化字符串。我们还可以指定字符串的宽度、左右对齐方式等。在默认情况下,printf并不像echo命令一样会自动添加换行符,我们必须在需要的时候手动添加,比如在下面的脚本中:

#!/bin/bash

#文件名: 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都是格式替代符(format substitution character),其所对应的参数可以置于带引号的格式字符串之后。

%-5s指明了一个格式为左对齐且宽度为5的字符串替代(-表示左对齐)。如果不用-指定对齐方式,字符串则采用右对齐形式。宽度指定了保留给某个变量的字符数。对Name而言,保留宽度是10。因此,任何Name字段的内容都会被显示在10字符宽的保留区域内,如果内容不足10字符,余下的则以空格符填充。

对于浮点数,我们可以使用其他参数对小数部分进行舍入。

对于Mark字段,我们将其格式化为%-4.2f,其中.2指定保留2个小数位。注意,在每行格式字符串后都有一个换行符\n。

1.2.2 补充内容

一定要留神的是echo和printf中的标志(如-e、-n等)应该出现在命令行内任何字符串之前,否则Bash会将其视为另外一个字符串。

1. 在echo中转义换行符

在默认情况下,echo会将一个换行符追加到输出文本的尾部。可以使用标志-n来忽略结尾的换行符。echo同样接受双引号字符串内的转义序列(escape sequence)作为参数。如果需要使用转义序列,则采用echo-e'包含转义序列的字符串"这种形式。例如:

echo -e "1\t2\t3"

123

2. 打印彩色输出

在终端中生成彩色输出相当好玩,我们可以使用转义序列来实现。

每种颜色都有对应的颜色码。比如:重置=0,黑色=30,红色=31,绿色=32,黄色=33,蓝色=34,洋红=35,青色=36,白色=37。

要打印彩色文本,可输入如下命令:

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

\e[0m"\e[1;31将颜色设为红色,\e[0m将颜色重新置回。你只需要将31替换成想要的颜色码就可以了。

要设置彩色背景,经常使用的颜色码是:重置=0,黑色=40,红色=41,绿色=42,黄色=43,蓝色=44,洋红=45,青色=46,白色=47。

要打印彩色文本,可输入如下命令:

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

1.3 玩转变量和环境变量

变量是任何一种编程语言必不可少的组成部分,用于存放各类数据。脚本语言通常不需要在使用变量之前声明其类型。只需要直接赋值就可以了。在Bash中,每一个变量的值都是字符串。无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。有一些特殊的变量会被shell环境和操作系统环境用来存储一些特别的值,这类变量就被称为环境变量。

让我们来看一些实例。

1.3.1 预备知识

变量采用常见的命名方式进行命名。当一个应用程序执行的时候,它接收一组环境变量。可以使用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的形式来描述,彼此之间由null字符(\0)分割。如果你将\0替换成\n,那么就可以将输出重新格式化,使得每一行显示一对variable=value。替换可以使用tr命令来实现:cat /proc/12501/environ | tr '\0' '\n'

现在,就让我们看看怎样对变量和环境变量赋值以及怎样使用它们吧。

1.3.2 实战演练

一个变量可以通过以下方式进行赋值:

var=value

var是变量名,value是赋给变量的值。如果value不包含任何空白字符(例如空格),那么它不

需要使用引号进行引用,反之,则必须使用单引号或双引号。

注意,var=value不同于var=value。把var=value写成var=value是一个常见的错误,但前者是赋值操作,后者则是相等操作。

在变量名之前加上前缀就可以打印出变量的内容:

var="value" #给变量var赋值

echovar

或者

echo{var}

输出如下:

value

我们可以在printf或echo命令的双引号中引用变量值。

#!/bin/bash

#文件名:variables.sh

fruit=apple

count=5

echo "We havecount{fruit}(s)"

输出如下:

We have 5 apple(s)

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

该环境变量通常被设置成:

HTTP_PROXY=http://192.168.0.2:3128

export HTTP_PROXY

export命令用来设置环境变量。至此之后,从当前shell脚本执行的任何程序都会继承这个变量。我们可以按照自己的需要,在执行的应用程序或者shell脚本中导出特定的变量。在默认情况下,有很多标准环境变量可供shell使用。

PATH就是其中之一。通常,变量PATH包含:

 echoPATH

 

/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

 echoPATH

/home/slynux/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/

sbin:/bin:/usr/games:/home/user/bin

这样,我们就将/home/user/bin添加到了PATH中。

还有一些众所周知的环境变量:HOME、PWD、USER、UID、SHELL等。

1.3.3 补充内容

让我们再多看些有关普通变量和环境变量的技巧。

1. 获得字符串长度

可以用下面的方法获得变量值的长度:

length={#var}

例如:var=12345678901234567890echo{#var}

20

length就是字符串所包含的字符数。

2. 识别当前的shell版本

可以用下面的方法获知当前使用的是哪种shell:

echoSHELL

也可以用

echo0

例如:echoSHELL

/bin/bash

 echo0

bash

3. 检查是否为超级用户

UID是一个重要的环境变量,可以用于检查当前脚本是以超级用户还是以普通用户的身份运行的。例如:

if [UID -ne 0 ]; then

echo Non root user. Please run as root.

else

echo "Root user"

fi

 

root用户的UID是0。

4. 修改Bash提示字符串(username@hostname:~)

当我们打开一个终端或是运行一个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 # 提示字符串已经改变

□ 我们可以利用类似\e[1;31的特定转义序列来设置彩色的提示字符串(参考1.2节的内容)。

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

1.4 通过shell进行数学运算

无论哪种编程语言都少不了算数操作,Bash shell同样提供了多种此类操作。

1.4.1 预备知识

在Bash shell环境中,可以利用let、(( ))和[]执行基本的算数操作。而在进行高级操作时,expr和bc这两个工具也会非常有用。

1.4.2 实战演练

可以用普通的变量赋值方法定义数值,这时,它会被存储为字符串。然而,我们可以用一些方法使它能像数字一样进行处理。

#!/bin/bash

no1=4;

no2=5;

 

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

当使用let时,变量名之前不需要再添加,例如:

let result=no1+no2

echoresult

□ 自加操作let no1++

□ 自减操作let no1--

□ 简写形式

let no+=6

let no-=6

它们分别等同于let no=no+6和let no=no-6。

□ 其他方法

操作符[]的使用方法和let命令类似:

result=[ no1+no2 ]

在[]中也可以使用前缀,例如:

result=[no1+5 ]

也可以使用(()),但使用(())时,变量名之前需要加上:

result=(( no1+50 ))

expr同样可以用于基本算数操作:

result=`expr 3+4`

result=(exprno1+5)

以上这些方法只能用于整数运算,而不支持浮点数。

bc是一个用于数学运算的高级工具,这个精密计算器包含了大量的选项。我们可以借助它执行浮点数运算并应用一些高级函数:

echo "4 * 0.56" | bc

2.24

 

no=54;

result=`echo "no * 1.5" | bc`

echoresult

81.0

其他参数可以置于要执行的具体操作之前,同时以分号作为定界符,通过stdin传递给bc。

■ 设定小数精度(数值范围):在下面的例子中,参数scale=2将小数位个数设置为2。因此,bc将会输出包含两个小数位的数值。

echo "scale=2;3/8" | bc

0.37

■ 进制转换:用bc可以将一种进制系统转换为另一种。来看看如何将十进制转换成二进制,然后再将二进制转换回十进制:

#!/bin/bash

#用途:数字转換

 

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^10" | bc #Square

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

文件描述符是与文件输入、输出相关联的整数。它们用来跟踪已打开的文件。最常见的文件描述符是stdin、stdout和stderr。我们可以将某个文件描述符的内容重定向到另一个文件描述符中。下面给出一些对文件描述符进行操作和重定向的例子。

1.5.1 预备知识

我们在编写脚本的时候会频繁使用标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。通过内容过滤将输出重定向到文件是我们从事的基础任务之一。当命令输出文本的时候,这些输出文本有可能是错误信息,也可能是正常的(非错误的)输出信息。单靠查看输出的文本本身,我们没法区分哪些是正常的输出文本,哪些是错误文本。不过,我们可以通过文件描述符来解决这个问题,将那些与特定描述符关联的文本提取出来。

文件描述符是与一个打开的文件或数据流相关联的整数。文件描述符0、1以及2是系统预留的。

□ 0 —— stdin(标准输入)。

□ 1 —— stdout(标准输出)。

□ 2 —— stderr(标准错误)。

1.5.2 实战演练

用下面的方法可以将输出文本重定向或保存到一个文件中:echo "This is a sample text 1" > temp.txt

这种方法通过截取文件的方式,将输出文本存储到文件temp.txt中,也就是说在把echo命令的输出写入文件之前,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

这里,+ 是一个非法参数,因此将返回错误信息。

 

成功和不成功的命令

当一个命令发生错误并退回时,它会返回一个非0的退出状态;而当命令成功完成后,它会返回数字0。退出状态可以从特殊变量?中获得(在命令执行语句之后立刻运行echo?,就可以打印出退出状态)。

 

下面的命令会将stderr文本打印到屏幕上,而不是文件中。ls + > out.txt

ls: cannot access +: No such file or directory

然而在下面的命令中,stdout没有任何输出,因此会生出空文件out.txt。ls + 2> out.txt # 正常运行

你可以将stderr单独重定向到一个文件,将stdout重定向到另一个文件:cmd 2>stderr.txt 1>stdout.txt

还可以利用下面的方法将stderr转换成stdout,使得stderr和stdout都被重定向到同一个文件中:cmd 2>&1 output.txt

或者采用下列方法:cmd &> output.txt

有时候,在输出中可能包含一些不必要的信息(比如除错信息)。如果你不想让终端中充斥着有关stderr的繁枝末节,那么你可以将stderr的输出重定向到/dev/null,保证一切都会被清除得干干净净。假设我们有三个文件,分别是a1、a2、a3。但是普通用户对文件a1没有“读―写―执行”权限。如果你需要打印文件名以a起始的所有文件的内容,你可以使用cat命令。

设置一些测试文件:echo a1 > a1cp a1 a2 ; cp a2 a3;chmod 000 a1 #清除所有权限

尽管可以使用通配符(a*)显示所有的文件内容,但是系统会显示一个出错信息,因为对文件a1没有可读权限。cat a*

cat: a1: Permission denied

a1

a1

其中,cat: a1: Permission denied属于stderr。我们可以将stderr信息重定向到一个文件中,而stdout仍然保持不变。考虑如下代码:cat a* 2> err.txt #stderr被重定向刭err.txt

a1

a1cat err.txt

cat: a1: Permission denied

观察下面的代码:some_command 2> /dev/null

在这个示例中,来自stderr的输出被丢到文件/dev/null中。/dev/null是一个特殊的设备文件,这个文件接收到的任何数据都会被丢弃。因此,null设备通常也被称为位桶(bit bucket)或黑洞。

当对stderr或stdout进行重定向时,重定向的文本将传入文件。因为文本已经被重定向到文件中,也就没剩下什么东西可以通过管道(|)传给接下来的命令,而这些命令是通过stdin来接收文本的。

但是有一个巧妙的方法可以一方面将数据重定向到文件,另一方面还可以提供一份重定向数据的副本作为后续命令的stdin。这一切都可以使用tee来实现。举个例子:要在终端中打印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 并没有在文件内容中出现。这是因为这些信息属于stderr,而tee只能从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代表标准输出。这些特殊的设备文件分别对应stdin、stderr和stdout。

1.5.3 补充内容

从stdin读取输入的命令能以多种方式接收数据。另外,还可以用cat和管道来制定我们自己的文件描述符,例如:cat file | cmdcmd1 | cmd2

1. 将文件重定向到命令

借助重定向,我们可以像使用stdin那样从文件中读取数据:cmd < file

2. 重定向脚本内部的文本块

有时候,我们需要对文本块(多行文本)像标准输入一样进行重定向。考虑一个特殊情况:源文本就位于shell脚本中。一个实用的例子是向log文件中写入头部数据,可以按照下面的方法完成:

#!/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

3. 自定义文件描述符

文件描述符是用于访问文件的一个抽象指针。存取文件离不开被称为“文件描述符”的特殊数字。0、1和2分别是stdin、stdout和stderr的预留描述符。

我们可以使用exec命令创建自定义的文件描述符。如果你对用其他编程语言进行文件编程非常熟悉,你可能已经注意到了文件打开模式。通常来说,会使用3种模式。

□ 只读模式。

□ 截断模式。

□ 追加模式。

<操作符用于从文件中读取至stdin。>操作符用于截断模式的文件写入(数据在目标文件内容被截断之后写入)。>>操作符用于追加模式的文件写入(数据被添加到文件的现有内容中,而且该目标文件中原有的内容不会丢失)。文件描述符可以用以上三种模式中的任意一种来创建。

为读取文件创建一个文件描述符:exec 3<input.txt # 使用文件描述符3打开并读取文件

我们可以这样使用它:echo this is a test line > input.txtexec 3<input.txt

现在你就可以在命令中使用文件描述符3了。例如:cat <&3

this is a test line

如果要再次读取,我们就不能再继续使用文件描述符3了,而是需要用exec重新分配文件描述符3以便用于读取。

创建一个文件描述符用于写入(截断模式):exec 4>output.txt # 打开文件用于写入

例如:exec 4>output.txtecho newline >&4cat output.txt

newline

创建一个文件描述符用于写入(追加模式):exec 5>>input.txt

例如:exec 5>>input.txtecho appended line >&5cat input.txt

newline

appended line

1.6 数组和关联数组

数组是shell脚本非常重要的组成部分,它借助素引将多个独立的数据存储为一个集合。

1.6.1 预备知识

Bash同时支持普通数组和关联数组。普通数组只能使用整数作为数组素引,而关联数组可以使用字符串作为数组素引。

关联数组在很多操作中相当有用。Bash从4.0版本开始支持关联数组。也就是说,使用旧版本的Bash则享受不到这种便利。

1.6.2 实战演练

定义数组的方法有很多种。可以在单行中使用一列值来定义一个数组:

array_var=(1 2 3 4 5 6)

#这些值将会存储在以0为起始索引的连续位置上

另外,还可以将数组定义成一组素引-值(index-value pair):

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=5echo{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

1.6.3 补充内容

关联数组从Bash 4.0版本开始被引入。借助散列技术,它成为解决很多问题的有力工具。接下来就让我们一探究竟。

1. 定义关联数组

在关联数组中,我们可以用任意的文本作为数组素引。而在普通数组中,只能用整数作为数组素引。

首先,需要使用单独的声明语句将一个变量名声明为关联数组。声明语句如下:declare -A ass_array

声明之后,可以用两种方法将元素添加到关联数组中。

(1) 利用内嵌素引-值列表法,提供一个素引-值列表:ass_array=([index1]=val1 [index2]=val2)

(2) 使用独立的素引-值进行赋值:ass_array[index1]=val1ass_array[index2]=val2

举个例子,试想如何用关联数组为水果制订价格:declare -A fruits_valuefruits_value=([apple]='100dollars' [orange]='150 dollars')

用下面的方法显示数组内容:echo "Apple costs{fruits_value[apple]}"

Apple costs 100 dollars

2. 列出数组索引

每一个数组元素都有一个素引用于查找。普通数组和关联数组具有不同的素引类型。我们可以用下面的方法获取数组的素引列表:echo{!array_var[*]}

也可以使用:echo{!array_var[@]}

以先前提到的fruits_value数组为例,运行如下:echo{!fruits_value[*]}

orange apple

对于普通数组,这个方法同样可行。

1.7 使用别名

别名就是一种便捷方式,以省去用户输入一长串命令序列的麻烦。

1.7.1 预备知识

别名有多种实现方式,可以使用函数,也可以使用alias命令。

1.7.2 实战演练

可以按照下面的方式创建一个别名:alias new_command='command sequence'

为安装命令apt-get install创建别名:alias install='sudo apt-get install'

这样一来,我们就可以用install pidgin代替sudo apt-get install pidgin了。

alias命令的作用只是暂时的。一旦关闭当前终端,所有设置过的别名就失效了。为了使别名设置一直保持作用,可以将它放入~/.bashrc文件中。因为每当一个新的shell进程生成时,都会执行~/.bashr中的命令。echo 'alias cmd="command seq"' >> ~/.bashrc

如果需要删除别名,只用将其对应的语句从 ~/.bashrc中删除,或者使用unalias命令。

另一种创建别名的方法是定义一个具有新名称的函数,并把它写入~/.bashrc。

我们可以创建一个别名rm,它能够删除原始文件,同时在backup目录中保留副本:

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

当你创建别名时,如果已经有同名的别名存在,那么原有的别名设置将被新的取代。

1.7.3 补充内容

有时别名也会造成安全问题。下面来看看应该如何识别这些隐患。

对别名进行转义

alias命令能够为任何重要的命令创建别名,不过你可能未必总是希望使用这些别名。我们可以将所要运行的命令进行转义,从而忽略当前定义过的所有别名。例如:\command

字符\对命令实施转义,使我们可以执行原本的命令,而不是这些命令的别名替身。在不信任的环境下执行特权命令,通过在命令前加上\来忽略可能存在的别名设置总是一个不错的安全实践。因为攻击者可能已经利用别名将某些特权命令替换成了一些别有用心的命令,借此来盗取用户输入的重要信息。

1.8 获取终端信息

编写命令行shell脚本的时候,总是免不了大量处理当前终端的相关信息,比如行数、列数、光标位置和遮盖密码字段等。这则攻略将帮助你学习如何收集和处理终端设置。

1.8.1 预备知识

tput和stty是两款终端处理工具。下面来看看如何用它们完成各种不同的任务。

1.8.2 实战演练

获取终端的行数和列数:

 

tput cols

tput lines

打印出当前终端名:

 

tput longname

将光标移动到方位(100,100)处:

 

tput cup 100 100

设置终端背景色:

 

tput setb no

其中,no可以在0到7之间取值。

将文本前景色设置为白色:

 

tput serf no

其中,no可以在0到7之间取值。

设置文本样式为粗体:

 

tput bold

设置下划线的起止:

 

tput smu1

tput rmu1

删除当前光标位置到行尾的所有内容:

 

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.9 获取、设置日期和延时

很多应用程序需要以不同的格式打印日期,设置日期和时间,以及根据日期和时间执行操作。延时通常用于在程序执行过程中提供一段等待时间(比如1秒钟)。比如在每隔五秒钟执行一次监视任务的这类脚本中,则需要掌握如何在程序中加入延时。这则攻略会告诉你怎么处理日期以及延时。

1.9.1 预备知识

我们能够以多种格式打印日期,也可以在命令行中设置日期。在类UNIX系统中,日期被存储为一个整数,其大小为自世界标准时间①② 1970年1月1日0时0分0秒 起所流逝的秒数。这种计时方式称之为纪元时或UNIX时间。来看看如何读取和设置它们吧。

注释:① UTC(Coordinated Universal Time),又称世界标准时间或世界协调时间。UTC是以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。

注释:② UNIX认为UTC1970年1月1日0点是纪元时间。POSIX标准推出后,这个时间也被称为POSIX时间。

1.9.2 实战演练

读取日期:data

Thu May 20 23:09:04 IST 2010

打印纪元时:date +%s

1290047248

纪元时被定义为从世界标准时间1970年1月1日0时0分0秒起至当①前时刻的总秒数,不包括闰秒 。当计算两个日期或两段时间的差值时,纪元时很有用处。你可以很容易得出两个特定时间戳的纪元时,并计算出两者之间的差值,由此就能知道两个日期之间相隔了多少秒。

注释:① 国际原子时的误差为每日数纳秒,而世界时的误差为每日数毫秒。针对这种情况,一种称为世界标准时间的折衷时标于1972年面世。为确保UTC与世界时相差不会超过0.9秒,在有需要的情况下会在UTC内加上正或负闰秒,因此UTC与国际原子时之间会出现若干整数秒的差别。位于巴黎的国际地球自转事务中央局负责决定何时加入闰秒。

我们可以从给定格式的日期串中得出对应的纪年时。你在输入时有多种日期格式可供选择。如果你要从系统日志中或者其他任何标准应用程序生成的输出中获取日期,那就完全不用烦心日期格式的问题。要将日期串转换成纪元时,只需要输入如下代码:date --date "Thu Nov 18 08:07:21 IST 2010" +%s

1290047841

选项--date用于提供日期串作为输入。但我们可以使用任意的日期格式化选项来打印输出。将日期串作为输入能够用来获知给定的日期是星期几。

例如:date --date "Jan 20 2001" +%A

Saturday

表1-1是一份日期格式字符串列表。表 1-1

用格式串结合+作为date命令的参数,可以按照你的选择打印出对应格式的日期。例如:date "+%d %B %Y"

20 May 2010

设置日期和时间:

# date -s "格式化的日期字符串"

例如:

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

有时候,我们需要检查一组命令所花费的时间,那就可以采用下面的方式:

#!/bin/bash

#文件名: time_take.sh

start=(date +%s)

commands;

statements;

 

end=(date +%s)

difference=(( end - start))

echo Time taken to execute commands isdifference seconds.

另一种方法则是使用timescriptpath来得到执行脚本所使用的时间。

1.9.3 补充内容

编写以循环方式运行的监视脚本时,设置时间间隔是必不可少的。让我们来看看如何生成延时。

在脚本中生成延时

为了在脚本中推迟执行一段时间,可以使用sleep:sleep no_of_seconds.

例如,下面的脚本就使用tput和sleep从0开始计数到40:

#!/bin/bash

#Filename: sleep.sh

echo -n Count:

tput sc

 

count=0;

while true;

do

if [count -lt 40 ];

then let count++;

sleep 1;

tput rc

tput ed

echo -ncount;

else exit 0;

fi

done

在上面的例子中,变量count初始化为0,随后每循环一次便增加1。echo语句打印出count的值。我们用tput sc存储光标位置。在每次循环中,我们通过恢复之前存储的光标位置,在终端中打印出新的count值。恢复光标位置的命令是tput rc。tput ed清除从当前光标位置到行尾之间的所有内容,使得旧的count值可以被清除并写入新值。循环内的1秒钟延时是通过sleep命令来实现的。

1.10 调试脚本

调试功能是每一种编程语言都应该实现的重要特性之一,当出现一些始料未及的情况时,用它来生成脚本运行信息。调试信息可以帮你弄清楚是什么原因使得程序发生崩溃或行为异常。每位系统程序员都应该了解Bash提供的那些调试选项,另外还需要了解一些调试技巧。

1.10.1 预备知识

调试shell脚本不需要什么特殊工具。Bash本身就包含了一些选项,能够打印出脚本接受的参数和输入。让我们来看看该怎么做。

1.10.2 实战演练

使用选项-x,启动跟踪调试shell脚本:bash -x script.sh

运行带有-x标志的脚本能打印出所执行的每一行命令以及当前状态。注意,你也可以使用sh -x script。

-x 标识将脚本中执行过的每一行都输出到stdout。不过,我们也可以要求只关注脚本某些部分的命令及参数的打印输出。针对这种情况,可以在脚本中使用setbuilt-in来启用或禁止调试打印。

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

□ set +x: 禁止调试。

□ set -v: 当命令进行读取时显示输入。

□ set +v: 禁止打印输入。

例如:

 

#!/bin/bash

#文件名: debug.sh

for i in {1..6}

do

set -x

echoi

set +x

done

echo "Script executed"

在上面的脚本中,仅在-x 和+x 所限制的区域内,echoi的调试信息才会被打印出来。

这种调试方法是由Bash的内建功能提供的。它们通常以固定的格式生成调试信息。但是在很多情况下,我们需要以自定义格式显示调试信息。这可以通过传递_DEBUG环境变量来建立这类调试风格。

请看下面的代码:

#!/bin/bash

function DEBUG()

{

[ "_DEBUG" == "on" ] ﹠﹠@ || :

}

 

for i in {1..10}

do

DEBUG echoi

done

可以将调试功能置为"on"来运行上面的脚本:_DEBUG=on ./script.sh

我们在每一个需要打印调试信息的语句前加上DEBUG。如果没有把_DEBUG=on传递给脚本,那么调试信息就不会打印出来。在Bash中,命令':'告诉shell不要进行任何操作。

1.10.3 补充内容

还有其他脚本调试的便捷方法,我们甚至可以巧妙地利用shebang来进行调试。

shebang的妙用

把shebang从 #!/bin/bash改成 #!/bin/bash -xv,这样一来,不用任何其他选项就可以启用调试功能了。

1.11 函数和参数

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

1.11.1 实战演练

定义函数:

function fname()

{

statements;

}

或者

fname()

{

statements;

}

只需要使用函数名就可以调用某个函数:fname ; # 执行函数

参数可以传递给函数,并由脚本进行访问:

fname arg1 arg2 ; # 传递参数

以下是函数fname的定义。在函数fname中,包含了各种访问函数参数的方法。

fname()

{

echo1,2; # 访问参数1和参数2

echo "@"; # 以列表的方式一次性打印所有参数

echo "*"; # 类似于@,但是参数被作为单个实体

return 0; # 返回值

}

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

□1是第一个参数。

□2是第二个参数。

□n是第n个参数。

□ "@" 被扩展成 "1" "2" "3"等。

□ "*" 被扩展成 "1c2c3",其中c是IFS的第一个字符。

□ "@" 用得最多。由于 "*"将所有的参数当做单个字符串,因此它很少被使用。

1.11.2 补充内容

让我们再研究Bash函数的一些技巧。

1. 递归函数

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

Fork炸弹

:(){ :|:﹠ };:

这个递归函数能够调用自身,不断地生成新的进程,最终造成拒绝服务攻击。函数调用前的﹠将子进程放入后台。这段危险的代码会分支出大量的进程,因而被称为Fork炸弹。

上面这段代码要理解起来可不容易。请参阅维基百科http://en.wikipedia.org/wiki/Fork_bomb,那里列出了有关Fork炸弹的细节以及更详细的解释。

可以通过修改配置文件/etc/security/limits.conf来限制可生成的最大进程数来避开这枚炸弹。

2. 导出函数

函数也能像环境变量一样用export导出,如此一来,函数的作用域就可以扩展到子进程中,如下:

export -f fname

3. 读取命令返回值(状态)

我们可以按照下面的方式获取命令或函数的返回值:

cmd;

echo?;? 会给出命令cmd的返回值。

返回值被称为退出状态。它可用于分析命令执行成功与否。如果命令成功退出,那么退出状态为0,否则为非0。

我们可以按照下面的方法检测某个命令是否成功结束:

#!/bin/bash

#文件名: success_test.sh

CMD="command" # command指代你要检测退出与否的目标命令

statusCMD

if [? -eq 0 ];

then

echo "CMD executed successfully"

else

echo "CMD terminated unsuccessfully"

fi

4. 向命令传递参数

命令的参数能够以不同的格式进行传递。假设-p、-v是可用选项,-k NO是另一个可以接受数字的选项,同时该命令还接受一个文件名作为参数,那么,它有如下几种执行方式:command -p -v -k 1 file

或者command -pv -k 1 file

或者command -vpk 1 file

或者command file -pvk 1

1.12 读取命令序列输出

shell脚本最棒的特性之一就是可以轻松地将多个命令或工具组合起来生成输出。一个命令的输出可以作为另一个命令的输入,而这个命令的输出又会传递至另一个命令,依次类推。这种命令组合的输出可以被存储在一个变量中。这则攻略将演示如何组合多个命令以及如何读取其输出。

1.12.1 预备知识

输入通常是通过stdin或参数传递给命令。输出要么出现在stderr,要么出现在stdout。当我们组合多个命令时,同时将stdin用于输入,stdout用于输出。

这些命令被称为过滤器(filter)。我们使用管道(pipe)来连接每一个过滤器。管道操作符是"|"。例如:cmd1 | cmd2 | cmd3

这里我们组合了三个命令。cmd1的输出传递给cmd2,而cmd2的输出传递给cmd3,最终(来自cmd3)的输出将会被打印或导入某个文件。

1.12.2 实战演练

请看下面的代码:ls | cat -n > out.txt

ls的输出(当前目录内容的列表)被传给cat -n,cat -n为通过stdin所接收到输入内容加上行号,然后将输出重定向到文件out.txt。

我们可以用下面的方法读取命令序列的输出:

 

cmd_output=(COMMANDS)

这种方法也被称为子shell(subshell)。例如:

 

cmd_output=(ls | cat -n)

echocmd_output

另一种被称为反引用(back-quote)的方法也可以用于存储命令输出:

 

cmd_output=`COMMANDS`

例如:

 

cmd_output=`ls | cat -n`

echocmd_output

反引用与单引号可不是一回事,它位于键盘的~键上。

1.12.3 补充内容

有很多种方法可以给命令分组。来看看其中的几种。

1. 利用子shell生成一个独立的进程

子shell本身就是独立的进程。可以使用()操作符来定义一个子shell:

pwd;

(cd /bin; ls);

pwd;

当命令在子shell中执行时,不会对当前shell有任何影响;所有的改变仅限于子shell内。例如,当用cd命令改变子shell的当前目录时,这种变化不会反映到主shell环境中。

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

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

2. 通过引用子shell的方式保留空格和换行符

假设我们使用子shell或反引用的方法将命令的输出读入一个变量中,可以将它放入双引号中,以保留空格和换行符(\n)。例如:cat text.txt

1

2

3

 out=(cat text.txt)echoout

1 2 3 # 丟失了換行符 \nout="(cat tex.txt)"echoout

1

2

3

1.13 以不按回车键的方式读取字符“n”

read是一个重要的Bash命令,用于从键盘或标准输入中读取文本。我们可以使用read以交互的形式读取来自用户的输入,不过read能做的可远不止这些。接着我们来演示read命令的一些重要选项。

1.13.1 预备知识

任何一种编程语言的输入库大多都是从键盘读取输入;但只有当回车键按下的时候,才标志着输入完毕。在有些重要情形下是没法按回车键的,输入结束与否是基于字符数或某个特定字符来决定的。例如,在一个游戏中,当按下+键时,小球就会向上移动。那么若每次都要按下+键,然后再按回车键来确认已经按过+键,这就显然太低效了。read命令提供了一种不需要按回车键就能够搞定这个任务的方法。

1.13.2 实战演练

下面的语句从输入中读取n个字符并存入变量variable_name:

read -n number_of_chars variable_name

例如:read -n 2 varechovar

read还有很多其他选项。让我们来看看它们。

用不回显(non-echoed)的方式读取密码:

read -s var

显示提示信息:

read -p "Enter input:" var

在特定时限内读取输入:

read -t timeout var

例如:read -t 2 var

#在2秒钟內将键入的字符串读入变量var

用定界符结束输入行:

read -d delim_charvar

例如:read -d ":" var

hello: #var被设置为hello

1.14 字段分隔符和迭代器

内部字段分隔符(Internal Field Separator,IFS)是shell脚本中的一个重要概念。在处理文本数据时,它可是相当有用。我们将会讨论把单个数据流划分成不同数据元素的定界符。内部字段分隔符是用于特定用途的定界符。IFS是存储定界符的环境变量。它是当前shell环境使用的默认定界字符串。

考虑一种情形:我们需要迭代一个字符串或CSV(Comma Separated Value,逗号分隔型数值)中的单词。在前者中,我们使用IFS=".";在后者中,则使用IFS=","。让我们看看应该怎么做。

1.14.1 预备知识

考虑CSV数据的情况:

data="name,sex,rollno,location"

#我们可以使用IFS读取变量中的每一个条目

oldIFS=IFS

IFS=,now,

for item indata;

do

echo Item:item

done

 

IFS=oldIFS

输入如下:

Item: name

Item: sex

Item: rollno

Item: location

IFS的默认值为空白字符(换行符、制表符或者空格)。

当IFS被设置为逗号时,shell将逗号解释成一个定界符,因此变量item在每次迭代中读取由逗号分隔的字串作为变量值。

如果没有把IFS设置成",",那么上面的脚本会将全部数据作为单个字符串打印出来。

1.14.2 实战演练

让我们以/etc/passwd为例,看看IFS的另一种用法。在文件/etc/passwd中,每一行包含了由冒号划分的多个条目。文件中的每行都对应一位用户的相关属性。

考虑这样的输入:root:x:0:0:root:/root:/bin/bash。每行的最后一项指定了用户的默认shell。可以按照下面的方法巧妙地利用IFS打印出用户以及他们默认的shell:

#!/bin/bash

#用途:演示IFS的用法

line="root:x:0:0:root:/root:/bin/bash"

oldIFS=IFS;

IFS=":"

count=0

for item inline;

do

 

[count -eq 0 ] ﹠﹠ user=item;

[count -eq 6 ] ﹠﹠ shell=item;

let count++

done;

IFS=oldIFS

echouser\'s shell isshell;

输出为:

root's shell is /bin/bash

对一系列值进行迭代的时候,循环非常有用。Bash提供了多种类型的循环。下面就来看看怎么样使用它们。

for循环

 

for var in list;

do

commands; # 使用变量var

done

list can be a string, or a sequence.

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

echo {1..50}能够生成一个从1到50的数字列表。

echo {a..z}或{A..Z},或是使用{a..h}生成部分列表。类似地,将这些方法结合起来,我们就可以连接(concatenate)数据。

下面的代码中,变量i在每次迭代的过程里都会保存一个字符,范围从a到z:

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

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

for((i=0;i<10;i++))

{

commands; # 使用变量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++; echox;

done

1.15 比较与测试

程序中的流程控制是由比较和测试语句来处理的。Bash同样具备多种与UNIX系统级特性相兼容的执行测试的方法。

1.15.1 预备知识

我们可以用if、if else以及逻辑运算符来执行测试,而用一些比较运算符来比较数据项。另外,有一个test命令也可以用来进行测试。让我们来看看如何使用这些命令。

1.15.2 实战演练

if条件:

if condition;

then

commands;

fi

 

else if和else:

 

if condition;

then

commands;

elif condition;

then

commands

else

commands

fi

if和else语句可以进行嵌套。if的条件判断部分可能会变得很长,但可以用逻辑运算符将它变得简洁一些:

[ condition ] && action; # 如果condition为真,则执行action

[ condition ] || action; # 如果condition为假,则执行action

﹠﹠是逻辑与运算符,||是逻辑或运算符。写Bash脚本时,这是一个很有用的技巧。现在来了解一下条件和比较操作。

算术比较

条件通常被放置在封闭的中括号内。一定要注意在[或]与操作数之间有一个空格。如果忘记了这个空格,脚本就会报错。例如:

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

对变量或值进行算术条件判断:

[var-eq 0 ] # 当var等于0时,返回真

[var-ne 0 ] # 当var为非0时,返回真

其他重要的操作符如下所示。

□ -gt:大于。

□ -lt:小于。

□ -ge:大于或等于。

□ -le:小于或等于。

可以按照下面的方法结合多个条件进行测试:

[var1 -ne 0 -avar2 -gt 2 ] # 使用逻辑与-a

[var -ne 0 -o var2 -gt 2 ] # 逻辑或 -o

文件系统相关测试

我们可以使用不同的条件标志测试不同的文件系统相关属性。

□ [ -ffile_var ]:如果给定的变量包含正常的文件路径或文件名,则返回真。

□ [ -xvar ]:如果给定的变量包含的文件可执行,则返回真。

□ [ -dvar ]:如果给定的变量包含的是目录,则返回真。

□ [ -evar ]:如果给定的变量包含的文件存在,则返回真。

□ [ -cvar ]:如果给定的变量包含的是一个字符设备文件的路径,则返回真。

□ [ -bvar ]:如果给定的变量包含的是一个块设备文件的路径,则返回真。

□ [ -wvar ]:如果给定的变量包含的文件可写,则返回真。

□ [ -rvar ]:如果给定的变量包含的文件可读,则返回真。

□ [ -Lvar ]:如果给定的变量包含的是一个符号链接,则返回真。

使用方法举例如下:

 

fpath="/etc/passwd"

if [ -efpath ]; then

echo File exists;

else

echo Does not exist;

fi

 

字符串比较

使用字符串比较时,最好用双中括号,因为有时候采用单个中括号会产生错误,所以最好避开它们。

可以检查两个字符串,看看它们是否相同。

□ [[str1 =str2 ]]:当str1等于str2时,返回真。也就是说,str1和str2包含的文本是一模一样的。

□ [[str1 ==str2 ]]:这是检查字符串是否相等的另一种写法。也可以检查两个字符串是否不同。

□ [[str1 !=str2 ]]:如果str1和str2不相同,则返回真。我们还可以检查字符串的字母序情况,具体如下所示。

□ [[str1 >str2 ]]:如果str1的字母序比str2大,则返回真。

□ [[str1 <str2 ]]:如果str1的字母序比str2小,则返回真。

□ [[ -zstr1 ]]:如果str1包含的是空字符串,则返回真。

□ [[ -nstr1 ]]:如果str1包含的是非空字符串,则返回真。

 

注意在=前后各有一个空格。如果忘记加空格,那就不是比较关系了,而变成了赋值语句。

 

使用逻辑运算符 ﹠﹠ 和 || 能够很容易地将多个条件组合起来:

if [[ -nstr1 ]] ﹠﹠ [[ -zstr2 ]] ;

then

commands;

fi

例如:

str1="Not empty "

str2=""

if [[ -nstr1 ]] ﹠﹠ [[ -zstr2 ]];

then

echo str1 is non-empty and str2 is empty string.

fi

输出如下:

str1 is non-empty and str2 is empty string.

test命令可以用来执行条件检测。用test有助于避免使用过多的括号。之前讲过的[]中的测试条件同样可以用于test命令。

例如:

if [var -eq 0 ]; then echo "True"; fi

can be written as

if testvar -eq 0 ; then echo "True"; fi第2章 命令之乐

本章内容

□用cat进行拼接          □临时文件命名与随机数

□录制与回放终端会话       □分割文件和数据

□文件查找与文件列表       □根据扩展名切分文件名

□将命令输出作为命令参数(xargs) □用rename和mv批量重命名文件

□用tr进行转换           □拼写检查与词典操作

□校验和与核实          □交互输入自动化

□排序、单一与重复

2.1 简介

各种命令可谓类UNIX系统中优美的部分。它们能帮助我们搞定各种繁杂的任务,使我们的工作变得更轻松。当你将这些命令付诸实践的时候,你肯定会爱上它们。在很多情形下它们都会让你情不自禁地大呼“wow!”一旦你尝试过Linux提供的这些利器,你一定会感到惊讶:以前没有这些命令的时候,自己是怎么熬过来的。在这些使我生活变得更舒适、工作更给力的命令中,我最钟爱的是grep、awk、sed和find。

UNIX/Linux命令行的使用是一门艺术。实践得越多,收益就越大。本章将为你介绍一些最有趣同时也是最实用的命令。

2.2 用cat进行拼接

cat是命令行玩家首先必须学习的命令之一。cat命令简约却不失优美。它通常用于读取、显示或拼接文件内容,不过cat所具备的能力远不止这些。

2.2.1 预备知识

用一个单行命令组合标准输入的数据与文件数据,这可是个让人挠头的难题。通常的解决方法是将stdin重定向到一个文件,然后再将两个文件拼接到一起。但是我们可以使用cat命令一次性完成任务。

2.2.2 实战演练

cat命令是一个日常经常会使用到的简单命令。cat本身表示concatenate(拼接)。

用cat读取文件内容的一般写法是:cat file1 file2 file3 ...

 

这个命令将作为命令行参数的文件内容拼接在一起作为输出。例如:cat file.txt

This is a line inside file.txt

This is the second line inside file.txt

2.2.3 工作原理

cat包含了大量功能。让我们来看一些cat的用法。

cat命令不仅可以读取文件并拼接数据,它还能够从标准输入中进行读取。

要从标准输入中读取,就要使用管道操作符:

OUTPUT_FROM_SOME COMMANDS | cat

类似地,我们可以用cat将输入文件的内容与标准输入拼接在一起。方法如下:echo 'Text through stdin' | cat - file.txt

在上面的代码中, - 被作为来自stdin文本的文件名。

2.2.4 补充内容

cat命令另外还有一些选项可用于查看文件。来看看它们的用法。

1. 压缩空白行

出于可读性或是别的一些原因,有时文本中的多个空行需要被压缩成单个。可以用下面的方法压缩文本文件中连续的空白行:cat -s file

例如:cat multi_blanks.txt

line 1

 

line2

 

line3

 

line4

 cat -s multi_blanks.txt # 压缩连续的空白行

line 1

 

line2

 

line3

 

ine4

我们也可以用tr移除空白行:cat multi_blanks.txt | tr -s '\n'

line1

line2

line3

line4

在tr的这种用法中,它将连续多个'\n'字符压缩成单个'\n'(换行符)。

2. 将制表符显示为 ^|

单从视觉上很难将制表符同连续的空格区分开。而在用Python之类的语言编写程序时,将制表符和空格用于代码缩进,具有特殊含义,并进行区别对待。因此,若在应该使用空格的地方误用了制表符的话,就会产生缩进错误。仅仅在文本编辑器中进行查看是很难发现这种错误的。cat有一个特性,可以将制表符重点标记出来。该特性对排除缩进错误非常有用。用cat命令的-T选项能够将制表符标记成^|。例如:cat file.py

def function():

var = 5

next = 6

third = 7

 cat -T file.py

def function():

^Ivar = 5

next = 6

^Ithird = 7^I

3. 行号

使用cat命令的-n选项会在输出的每一行内容之前加上行号。别担心,cat命令绝不会修改你的文件,它只是根据用户提供的选项在stdout生成一个修改过的输出而已。例如:cat lines.txt

line

line

line

 cat -n lines.txt

1 line

2 line

3 line

2.3 录制与回放终端会话

当你需要为别人在终端上演示某些操作或是需要准备一个命令行教程时,通常你要一边手动输入命令一边演示,或者,你也可以录制一段屏幕演示视频,然后再回放出来。如果我们将输入命令后发生的一切按照先后次序记录下来,再进行回放,从而使得观众好像身临其境一般,这个想法听起来如何?命令的输出会显示在终端上,一直到回放内容播放完毕。听起来好玩吧?所有这些都可以用script和scriptreplay命令来实现。

2.3.1 预备知识

script和scirptreplay命令在绝大多数GNU/Linux发行版上都可以找到。把终端会话记录到一个文件中会很有意思。你可以通过录制终端会话来制作命令行技巧视频教程,也可以与他人分享会话记录文件,共同研究如何使用这些命令行完成某项任务。

2.3.2 实战演练

开始录制终端会话:

 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 # 按播放命令序列输出

2.3.3 工作原理

通常,我们会录制桌面环境视频来作为教程使用。不过要注意的是,视频需要大量的存储空间,而终端脚本文件仅仅是一个文本文件,其文件大小不过是KB级别。

script命令同样可以用于建立可在多个用户之间进行广播的视频会话。这是件很有意思的事。来看看它是如何实现的吧。

打开两个终端,Terminal1和Terminal2。

(1) 在Terminal1中输入以下命令:mkfifo scriptfifo

(2) 在Terminal2中输入以下命令:cat scriptfifo

(3) 返回Terminal1,输入以下命令:script -f scriptfifocommands;

如果需要结束会话,输入exit并按回车键。你会得到如下信息:“Script done, file is scriptfifo”。

现在,Terminal1就成为了广播员,而Terminal2则成为了听众。

不管你在Terminal1中输入什么内容,它都会在Terminal2或者使用了下列命令的任何终端中实时播放:

cat scriptfifo

当需要为计算机实验室或Internet上的用户群演示教程的话,不妨考虑这个方法。它在节省带宽的同时也提供了实时体验。

2.4 文件查找与文件列表

find是UNIX/Linux命令行工具箱中最棒的工具之一。这个命令对编写shell脚本很有帮助,但是多数人由于对它缺乏认识,并不能有效地使用它。这则攻略讨论了find的大多数用法以及如何用它解决各种难度的问题。

2.4.1 预备知识

find命令的工作方式如下:沿着文件层次结构向下遍历,匹配符合条件的文件,并执行相应的操作。下面来看看find命令的各种用例和基本用法。

2.4.2 实战演练

要列出当前目录及子目录下所有的文件和文件夹,可以采用下面的写法:find base_path

bash_path可以是任何位置(例如/home/slynux),find会从该位置开始向下查找。

例如:find . -print

# 打印文件和目录的列表

. 指定当前目录,..指定父目录。这是UNIX文件系统中的约定用法。

-print指明打印出匹配文件的文件名(路径)。当使用-print时,'\n'作为用于分隔文件的定界符。

-print0指明使用'\0'作为定界符来打印每一个匹配的文件名。当文件名中包含换行符时,这个方法就有用武之地了。

2.4.3 补充内容

至此,我们已经学习了find命令最常见的用法。作为一个强大的命令行工具,find命令包含了诸多值得留意的选项。接下来让我们来看find命令的一些其他选项。

1. 根据文件名或正则表达式匹配搜索

选项-name的参数指定了文件名所必须匹配的字符串。我们可以将通配符作为参数使用。*.txt能够匹配所有以.txt结尾的文件名。选项-print在终端中打印出符合条件(例如-name)的文件名或文件路径,这些匹配条件作为find命令的选项给出。find /home/slynux -name "*.txt" -print

find命令有一个选项-iname(忽略字母大小写),该选项的作用和-name类似,只不过在匹配名字的时候会忽略大小写。

例如:

 ls

example.txt EXAMPLE.txt file.txtfind . -iname "example*" -print

./example.txt

./EXAMPLE.txt

 

如果想匹配多个条件中的一个,可以采用OR条件操作:

 ls

new.txt some.jpg text.pdffind . \( -name "*.txt" -o -name "*.pdf" \) -print

./text.pdf

./new.txt

 

上面的代码会打印出所有的 .txt和 .pdf文件,是因为这个find命令能够匹配所有这两类文件。\(以及\)用于将-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是基于正则表达式来匹配文件路径的。

正则表达式是通配符匹配的高级形式,它可以指定文本模式。我们借助这种模式来匹配并打印文本。使用正则表达式进行文本匹配的一个典型例子就是从一堆文本中解析出所有的E-mail地址。一个E-mail地址通常采用name@host.root这种形式,所以,可以将其一般化为a-z0-9]+@[a-z0-9]+.[a-z0-9]+。+指明在它之前的字符类中的字符可以出现一次或多次。

下面的命令匹配.py或.sh文件:ls

new.PY next.jpg test.pyfind . -regex ".*\(\.py\|\.sh\)"

./test.py

类似地,-iregex用于忽略正则表达式的大小写。例如:find . -iregex ".*\(\.py\|\.sh\)"

./test.py

./new.PY

2. 否定参数

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

3. 基于目录深度的搜索

find命令在使用时会遍历所有的子目录。我们可以采用一些深度参数来限制find命令遍历的深度。-maxdepth和 -mindepth就是这类参数。

大多数情况下,我们只需要在当前目录中进行搜素,无须再继续向下查找。对于这种情况,我们使用深度参数来限制find命令向下查找的深度。如果只允许find在当前目录中查找,深度可以设置为1;当需要向下两级时,深度可以设置为2;其他情况可以依次类推。

我们可以用-maxdepth参数指定最大深度。与此相似,我们也可以指定一个最小的深度,告诉find应该从此处开始向下查找。如果我们想从第二级目录开始搜素,那么使用-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

即使当前目录或dir1和dir3中包含有文件,它们也不会被打印出来。

-maxdepth和-mindepth应该作为find的第3个参数出现。如果作为第4个或之后的参数,就可能会影响到find的效率,因为它不得不进行一些不必要的检查。例如,如果-maxdepth作为第4个参数,-type作为第三个参数,find首先会找出符合-type的所有文件,然后在所有匹配的文件中再找出符合指定深度的那些。但是如果反过来,目录深度作为第三个参数,-type作为第四个参数,那么find就能够在找到所有符合指定深度的文件后,再检查这些文件的类型,这才是最有效的搜索顺序。

 

4. 根据文件类型搜索

类UNIX系统将一切都视为文件。文件具有不同的类型,例如普通文件、目录、字符设备、块设备、符号链接、硬链接、套接字以及FIFO等。

-type可以对文件搜素进行过滤。借助这个选项,我们可以为find命令指明特定的文件匹配类型。

只列出所有的目录:find . -type d -print

将文件和目录分别列出可不是个容易事。不过有了find就好办了。例如,只列出普通文件:find . -type f -print

只列出符号链接:find . -type l -print

你可以按照表2-1列出的内容用type参数来匹配所需要的文件类型。表 2-1

5. 根据文件时间进行搜索

UNIX/Linux文件系统中的每一个文件都有三种时间戳(timestamp),如下所示。

□ 访问时间(-atime):用户最近一次访问文件的时间。

□ 修改时间(-mtime):文件内容最后一次被修改的时间。

□ 变化时间(-ctime):文件元数据(metadata,例如权限或所有权)最后一次改变的时间。

在UNIX中并没有所谓“创建时间”的概念。

-atime、-mtime、-ctime可作为find的时间参数。它们可以整数值给出,单位是天。这些整数值通常还带有-或+:-表示小于,+表示大于,如下所示。

□ 打印出在最近七天内被访问过的所有文件:find . -type f -atime -7 -print

□ 打印出恰好在七天前被访问过的所有文件:find . -type f -atime 7 -print

□ 打印出访问时间超过七天的所有文件:find . -type f -atime +7 -print

类似地,我们可以根据修改时间,用-mtime进行搜素,也可以根据变化时间,用-ctime进行搜素。

-atime、-mtime以及-ctime都是基于时间的参数,其计量单位是“天”。还有其他一些基于时间的参数是以分钟作为计量单位的。这些参数包括:

□ -amin(访问时间);

□ -mmin(修改时间);

□ -cmin(变化时间)。

举例如下。

打印出访问时间超过7分钟的所有文件:find . -type f -amin +7 -print

find另一个漂亮的特性是 -newer参数。使用 -newer,我们可以指定一个用于比较时间戳的参考文件,然后找出比参考文件更新的(更长的修改时间)所有文件。

例如,找出比file.txt修改时间更长的所有文件:find . -type f -newer file.txt -print

find命令的时间戳操作处理选项对编写系统备份和维护脚本很有帮助。

6. 基于文件大小的搜索

根据文件的大小,可以这样搜素:

 find . -type f -size +2k

# 大于2KB的文件

 find . -type f -size -2k

# 小于2KB的文件

 find . -type f -size 2k

# 大小等于2KB的文件

 

除了k之外,还可以用其他文件大小单元。

□ b —— 块(512字节)。

□ c —— 字节。

□ w —— 字(2字节)。

□ k —— 千字节。

□ M —— 兆字节。

□ G —— 吉字节。

7. 删除匹配的文件

-delete可以用来删除find查找到的匹配文件。

删除当前目录下所有的 .swp文件:find . -type f -name "*.swp" -delete

8. 基于文件权限和所有权的匹配

文件匹配可以根据文件权限进行。列出具有特定权限的所有文件:

 find . -type f -perm 644 -print

# 打印出权限为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

 

9. 结合find执行命令或动作

find命令可以借助选项-exec与其他命名进行结合。-exec算得上是find最强大的特性之一。

看看应如何使用 -exec选项。

在前一节中,我们用-perm找出了所有权限不当的PHP文件。这次的任务也差不多,我们需要将某位用户(比如root)全部文件的所有权更改成另一位用户(比如Web服务器默认的Apache用户www-data),那么就可以用-user找出root拥有的所有文件,然后用-exec更改所有权。

 

你必须以超级用户的身份执行find命令才能够进行所有权的更改。

 

示例如下:find . -type f -user root -exec chown slynux {} \;

在这个命令中,{ }是一个特殊的字符串,与 -exec选项结合使用。对于每一个匹配的文件,{ }会被替换成相应的文件名。例如,find命令找到两个文件test1.txt和test2.txt,其所有者均为slynux,那么find将会执行:

chown slynux {}

它会被解析为chown slynux test1.txt和chown 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脚本中(例如command.sh),然后在-exec中使用这个脚本:

-exec ./commands.sh {} \;

 

-exec能够同printf结合来生成有用的输出信息。例如:find . -type f -name "*.txt" -exec printf "Text file: %s\n" {} \;

10. 让find跳过特定的目录

在搜素目录并执行某些操作的时候,有时为了提高性能,需要跳过一些子目录。例如,程序员会在版本控制系统(如Git)所管理的开发源码树中查找特定的文件,源代码层级结构总是会在每个子目录中包含一个.git目录(.git存储每个目录相关的版本控制信息)。因为与版本控制相关的目录对我们而言并没有什么用处,所以没必要去搜素这些目录。将某些文件或目录从搜素过程中排除的技术被称为“修剪”,其操作方法如下:find devel/source_path \( -name ".git" -prune \) -o \( -type f -print \)

 

#不使用\( -type -print \),而是选择需要的过滤器

以上命令打印出不包括在 .git目录中的所有文件的名称(路径)。

这里,\( -name ".git" -prune \)的作用是用于进行排除,它指明了.git目录应该被排除掉,而\(-type f -print \)指明了需要执行的动作。这些动作需要被放置在第二个语句块中(打印出所有文件的名称和路径)。

2.5 玩转xargs

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

cat foo.txt | grep "test"

但是,有些命令只能以命令行参数的形式接受数据,而无法通过stdin接受数据流。在这种情况下,我们没法用管道来提供那些只有通过命令行参数才能提供的数据。

那就只能另辟蹊径了。xargs是一个很有用的命令,它擅长将标准输入数据转换成命令行参数。xargs能够处理stdin并将其转换为特定命令的命令行参数。xargs也可以将单行或多行文本输入转换成其他格式,例如单行变多行或是多行变单行。

Bash黑客都喜欢单行命令。单行命令是一个命令序列,各命令之间不使用分号,而是使用管道操作符进行连接。精心编写的单行命令可以更高效、更简捷地完成任务。就文本处理而言,需要具备扎实的理论和实践才能够写出适合的单行命令解决方法。xargs就是构建单行命令的重要组件之一。

2.5.1 预备知识

xargs命令应该紧跟在管道操作符之后。它以标准输入作为主要的源数据流,并使用stdin并通过提供命令行参数来执行其他命令。例如:

command | xargs

2.5.2 实战演练

xargs命令把从stdin接收到的数据重新格式化,再将其作为参数提供给其他命令。

xargs可以作为一种替换方式,作用类似于find命令中的-exec参数。下面介绍一些借助xargs命令能够实现的技巧。

□ 将多行输入转换成单行输出

只需要将换行符移除,再用" "(空格)进行代替,就可以实现多行输入的转换。'\n'被解释成一个换行符,换行符其实就是多行文本之间的定界符。利用xargs,我们可以用空格替换掉换行符,这样一来,就能够将多行文本转换成单行文本:cat example.txt # 样例文件

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

2.5.3 工作原理

xargs命令拥有数量众多且用法简单的选项,这使得它很适用于某些问题场合。让我们来看看如何能够巧妙地用这些选项来解决问题。

我们可以用自己的定界符来分隔参数。用 -d选项为输入指定一个定制的定界符:echo "splitXsplitXsplitXsplit" | xargs -d X

split split split split

在上面的代码中,stdin是一个包含了多个X字符的字符串。我们可以用 -d将X作为输入定界符。在这里,我们明确指定X作为输入定界符,而在默认情况下,xargs采用内部字段分隔符(IFS)作为输入定界符。

同时结合-n,我们可以将输入划分成多行,而每行包含两个参数:echo "splitXsplitXsplitXsplit" | xargs -d X -n 2

split split

split split

2.5.4 补充内容

我们已经从上面的例子中学到了如何将stdin格式化成不同的输出形式以作为参数。现在让我们来学习如何将这些参数传递给命令。

1. 读取stdin,将格式化参数传递给命令

编写一个小型的定制版echo来更好地理解用xargs提供命令行参数的方法:

#!/bin/bash

#文件名: 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 -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扩展时会被替换掉。当-I与xargs结合使用时,对于每一个参数,命令都会被执行一次。

试试下面的用法:cat args.txt | xargs -I {} ./cecho.sh -p {} -l

-p arg1 -l #

-p arg2 -l #

-p arg3 -l #

-I {} 指定了替换字符串。对于每一个命令参数,字符串{}会被从stdin读取到的参数所替换。使用-I的时候,命令就似乎是在一个循环中执行一样。如果有三个参数,那么命令就会连同{}一起被执行三次,而{}在每一次执行中都会被替换为相应的参数。

2. 结合find使用xargs

xargs和find算是一对死党。两者结合使用可以让任务变得更轻松。不过,人们通常却是以一种错误的组合方式使用它们。例如:find . -type f -name "*.txt" -print | xargs rm -f

这样做很危险。有时可能会删除不必要删除的文件。我们没法预测分隔find命令输出结果的定界符究竟是'\n'还是' '。很多文件名中都可能会包含空格符,而xargs很可能会误认为它们是定界符(例如,hell text.txt会被xargs误认为hell和text.txt)。

只要我们把find的输出作为xargs的输入,就必须将-print0与find结合使用,以字符null来分隔输出。

用find匹配并列出所有的.txt文件,然后用xargs将这些文件删除:find . -type f -name "*.txt" -print0 | xargs -0 rm -f

这样就可以删除所有的.txt文件。xargs -0将\0作为输入定界符。

3. 统计源代码目录中所有C程序文件的行数

统计所有C程序文件的行数是大多数程序员都会遇到的活儿。完成这项任务的代码如下:find source_code_dir_path -type f -name "*.c" -print0 | xargs -0 wc -l

4. 结合stdin,巧妙运用while语句和子shell

xargs只能以有限的几种方式来提供参数,而且它也不能为多组命令提供参数。要执行一些包含来自标准输入的多个参数的命令,可采用一种非常灵活的方法。我将这个方法称为子shell妙招(subshell hack)。一个包含while循环的子shell可以用来读取参数,并通过一种巧妙的方式执行命令:cat files.txt | ( while read arg; do catarg; done )

# 等同于 cat files.txt | xargs -I {} cat {}

在while循环中,可以将catarg替换成任意数量的命令,这样我们就可以对同一个参数执行多项命令。我们也可以不借助管道,将输出传递给其他命令。这个技巧能适用于各种问题环境。子shell内部的多个命令可作为一个整体来运行。cmd0 | ( cmd1;cmd2;cmd3) | cmd4

如果cmd1是cd /,那么就会改变子shell工作目录,然而这种改变仅局限于子shell内部。cmd4则完全不知道工作目录发生了变化。

2.6 用tr 进行转换

tr是UNIX命令行行家工具箱中一件精美的小工具。它经常用来编写优美的单行命令,作用不容小视。

tr可以对来自标准输入的字符进行替换、删除以及压缩。它可以将一组字符变成另一组字符,因而通常也被称为转换(translate)命令。

2.6.1 预备知识

tr只能通过stdin(标准输入),而无法通过命令行参数来接受输入。它的调用格式如下:

tr [options] set1 set2

将来自stdin的输入字符从set1映射到set2,并将其输出写入stdout(标准输出)。set1和set2是字符类或字符集。如果两个字符集的长度不相等,那么set2会不断重复其最后一个字符,直到长度与set1相同。如果set2的长度大于set1,那么在set2中超出set1长度的那部分字符则全部被忽略。

2.6.2 实战演练

将输入字符由大写转换成小写,可以使用下面的命令:echo "HELLO WHO IS THIS" | tr 'A-Z' 'a-z''

A-Z' 和 'a-z'都是集合。我们可以按照需要追加字符或字符类来构造自己定制的集合。

'ABD-}'、'aA.,'、'a-ce-x'以及'a-c0-9'等均是合法的集合。定义集合也很简单,不需要去书写一长串连续的字符序列,相反,我们可以使用“起始字符―终止字符”这种格式。这种写法也可以和其他字符或字符类结合使用。如果“起始字符―终止字符”不是一个连续的字符序列,那么它就会被视为一个包含了三个元素的集合(“起始字符―终止字符”)。你可以使用像'\t'、'\n'这种特殊字符,也可以使用其他ASCII字符。

2.6.3 工作原理

通过在tr中使用集合的概念,我们可以轻松地将字符从一个集合映射到另一个集合中。让我们通过一则示例看看如何用tr进行数字加密和解密。echo 12345 | tr '0-9' '9876543210'

87654 #已加密echo 87654 | tr '9876543210' '0-9'

12345 #已解密

再来看另外一个有趣的例子。

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' ' '

2.6.4 补充内容

1. 用tr删除字符

tr有一个选项-d,可以通过指定需要被删除的字符集合,将出现在stdin中的特定字符清除掉:cat file.txt | tr -d '[set1]'

#只使用set1,不使用set2

例如:echo "Hello 123 world 456" | tr -d '0-9'

Hello world

# 将stdin中的数字删除并打印出来

 

2. 字符集补集

我们可以利用选项-c来使用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

在这里,补集中包含了除数字、空格字符和换行符之外的所有字符。因为指定了-d,所以这些字符全部都被删除。

3. 用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

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

15

 

这一招是如何起效的?

在上面的命令中,tr用来将'\n'替换成'+',因此我们得到了字符串"1+2+3+…5+",但是在字符串的尾部多了一个操作符+。为了抵消这个多出来的操作符,我们再追加一个0。[ operation ]执行算术运算,因此得到下面的字符串:

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

如果我们利用循环从文件中读取数字,然后再进行相加,那肯定得用好几行代码。如今我们只用一行就完成任务了。这种编写单行命令的技巧可通过实践才能习得。

4. 字符类

tr可以像使用集合一样使用各种不同的字符类,这些字符类如下所示。

□ alnum:字母和数字。

□ alpha:字母。

□ cntrl:控制(非打印)字符。

□ digit:数字。

□ graph:图形字符。

□ lower:小写字母。

□ print:可打印字符。

□ punct:标点符号。

□ space:空白字符。

□ upper:大写字母。

□ xdigit:十六进制字符。

可以按照下面的方式选择并使用所需的字符类:

tr [:class:] [:class:]

例如:

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

2.7 校验和与核实

校验和(checksum)程序用来从文件中生成校验和密钥,然后利用这个校验和密钥核实文件的完整性。一份文件可以通过网络或任何存储介质分发到不同的地点。出于多种原因,数据有可能在传输过程中丢失了若干位,从而导致文件损坏。这种错误通常发生在从Internet上下载文件时,或者通过网络传输文件时,或者遭遇CD光盘损坏等。

因此,我们需要采用一些测试方法来确定接收到的文件是否存在错误。用于文件完整性测试的特定密钥就被称为校验和。

我们对原始文件和接收到的文件都进行校验和计算。通过比对两者的校验和,就能够核实接收到的文件是否正确。如果校验和(一个来自源位置的原始文件,另一个来自目的地的接收文件)相等,就意味着我们接收到了正确的文件,否则用户就不得不重新发送文件并再次比对校验和。

校验和对于编写备份脚本或系统维护脚本来说非常重要,因为它们都会涉及通过网络传输文件。通过使用校验和核实,我们就可以识别出那些在网络传输过程中出现损坏的文件,并重发这些文件,从而确保数据的完整性。

2.7.1 预备知识

最知名且使用最为广泛的校验和技术是md5sum和sha1sum。它们对文件内容使用相应的算法来生成校验和。下面就来看看如何从文件中生成校验和并核实文件的完整性。

2.7.2 实战演练

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

68b329da9893e34099c7d8ad5cb9c940 filename

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

将输出的校验和重定向到一个文件,然后用这个MD5文件核实数据的完整性:md5sum filename > file_sum.md5

2.7.3 工作原理

md5sum校验和计算的方法如下:md5sum file1 file2 file3 ..

当使用多个文件时,输出中会在每行中包含单个文件的校验和:

[checksum1] file1

[checksum1] file2

[checksum1] file3

可以按照下面的方法用生成的文件核实数据完整性:md5sum -c file_sum.md5

# 这个命令会输出校验和是否匹配的消息

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

与md5sum类似,SHA1是另一种常用的校验和算法。它从给定的输入文件中生成一个长度为40个字符的十六进制串。用来计算SAH1串的命令是sha1sum。其用法和md5sum的非常相似。只需要把先前讲过的那些命令中的md5sum替换成sha1sum就行了,另外,记住要将输入文件名从file_sum.md5改为file_sum.sha1。

校验和对于核实下载文件的完整性非常有帮助。我们从Internet上下载的ISO镜像文件一般更容易出现错误。因而,为了检查接收文件正确与否,校验和得以广泛应用。校验和程序对同样的文件数据始终生成一模一样的校验和。

2.7.4 补充内容

对于多个文件,校验和同样可以发挥作用。现在就看看如何校验并核实多个文件。

对目录进行校验

校验和是从文件中计算得来的。对目录计算校验和意味着我们需要对目录中的所有文件以递归的方式进行计算。

它可以用命令md5deep或sha1deep来实现。首先,需要安装md5deep软件包以确保能找到这些命令。该命令的用法如下:md5deep -rl directory_path > directory.md5

# -r 使用递归的方式

# -l 使用相对路径。默认情况下,md5deep会输出文件的绝对路径

或者,也可以结合find来递归计算校验和:find directory_path -type f -print0 | xargs -0 md5sum >> directory.md5

用下面的命令进行核实:md5sum -c directory.md5

2.8 排序、单一与重复

同文本文件打交道时,总避不开排序,那是因为对于文本处理任务而言,排序(sort)可以起到不小的作用。sort命令能够帮助我们对文本文件和stdin进行排序操作。通常,它会结合其他命令来生产所需要的输出。uniq是一个经常与sort一同使用的命令。它的作用是从文本或stdin中提取单一的行。sort和uniq能够用来查找重复数据。这则攻略将演示sort和uniq命令的大多数用法。

2.8.1 预备知识

sort命令既可以从特定的文件,也可以从stdin中获取输入,并将输出写入stdout。uniq的工作模式和sort一样。

2.8.2 实战演练

我们可以按照下面的方式轻松地对一组文件(例如file1.txt和file2.txt)进行排序:sort file1.txt file2.txt .. > sorted.txt

或是sort file1.txt file2.txt .. -o sorted.txt

找出已排序文件中不重复的行:cat sorted_file.txt | uniq> uniq_lines.txt

2.8.3 工作原理

sort和uniq可以派上用场的地方有很多。让我们来认识一些命令选项和使用方法。

按数字进行排序:sort -n file.txt

按逆序进行排序:sort -r file.txt

按月份进行排序(按照一月、二月、三月……这样的顺序):sort -M months.txt

还可以用下面的方法测试一个文件是否已经被排过序:

#!/bin/bash

#用途: 排序

sort -C file ;

if [? -eq 0 ]; then

echo Sorted;

else

echo Unsorted;

fi

#要检查是否按数字进行排序,应该使用sort -nC

如果需要合并两个排过序的文件,而且不需要对合并后的文件再进行排序,可以使用:sort -m sorted1 sorted2

2.8.4 补充内容

1. 依据键或列进行排序

如果需要将下面的文本排序,我们可以按列来进行。cat data.txt

1  mac         2000

2  winxp      4000

3  bsd   1000

4  linux    1000

有很多方法可以对这段文本排序。目前它是按照序号(第一列)来排序的,我们也可以依据第二列和第三列来排序。

-k指定了排序应该按照哪一个键(key)来进行。键指的是列号,而列号就是执行排序时的依据。-r告诉sort命令按照逆序进行排序。例如:

# 依据第1列,以逆序形式排序sort -nrk 1 data.txt

4  linux       1000

3  bsd         1000

2  winxp     4000

1  mac        2000

# -nr表明按照数字,采用逆序形式排序

 

# 依据第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

7464dfddfdfdsort -nk 2,3 data.txt

突出显示的字符将用作数值键。为了提取这个键,用字符的起止位置作为键的书写格式。

用第一个字符作为键:sort -nk 1,1 data.txt

为了使sort的输出与以\0作为参数终止符的xargs命令相兼容,采用下面的命令:sort -z data.txt | xargs -0

# 终止符\0使得xargs命令的使用更加安全

有时文本中可能会包含一些像空格之类的不必要的字符。如果需要忽略这些字符,并以字典序进行排序,可以使用:sort -bd unsorted.txt

其中,选项-b用于忽略文件中的前导空白字符,选项-d用于指明以字典序进行排序。

2. 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命令的输入的时候,最好为输出的各行添加一个0值字节终止符。在将uniq命令的输入作为xargs的数据源时,同样应当如此。如果没有使用0值字节终止符,那么在默认情况下,xargs命令会用空格作为定界符分割参数。例如,来自stdin的文本行“this isa line”会被xargs当做包含4个不同的参数,但实际上它只是一个单行而已。如果使用0值字节终止符,那么 \0就被作为定界符,此时,包含空格的单行就能够被正确地解析为单个参数。

用uniq命令生成包含0值字节终止符的输出:uniq -z file.txt

下面的命令将删除所有指定的文件,而这些文件的名字是从files.txt中读取的:uniq -z file.txt | xargs -0 rm

如果某个文件名在文件中出现多次, uniq命令只会将这个文件名写入stdout一次。

3. 用uniq生成字符串样式

这里有一个很有意思的问题:我们有一个包含重复字符的字符串,如何才能知道每个字符在字符串中出现的次数,并依照下面的格式输出字符串?

输入:ahebhaaa

输出:4a1b1e2h

任何一个字符只要出现重复,就在之前加上它在字符串中出现过的次数。我们可以结合运用uniq和sort来解决这个问题:

INPUT= "ahebhaaa"

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载