Linux Shell编程从入门到精通(第2版)

978-7-115-40004-8
作者: 张昊 程国钢
译者:
编辑: 李永涛

图书目录:

详情

本书由浅入深、循序渐进地详细讲解了Linux Shell编程的基本知识。这些基本知识包括:Shell编程的基本知识、文本处理的工具和方法、正则表达式、Linux系统知识等。本书旨在通过理清Linux Shell编程的脉络,从基本概念着手,以丰富、实用的范例作为辅助,使读者能够深入浅出地学习Linux Shell编程。

图书摘要

Linux Shell编程从入门到精通(第2版)

张昊 程国钢 编著

人民邮电出版社

北 京

图书在版编目(CIP)数据

Linux Shell编程从入门到精通/张昊,程国钢编著.--2版,北京:人民邮电出版社,2015.9

ISBN 978-7-115-40004-8

Ⅰ.①L… Ⅱ.①张…②程… Ⅲ.①Linux操作系统—程序设计 Ⅳ.①TP316.89

中国版本图书馆CIP数据核字(2015)第191897号

◆编著 张昊 程国钢

责任编辑 李永涛

责任印制 杨林杰

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京昌平百善印刷厂印刷

◆开本:787×1092 1/16

印张:21.25

字数:530千字  2015年9月第2版

印数:5001-7500册  2015年9月北京第1次印刷

定价:59.00元(附光盘)

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

内容提要

本书由浅入深、循序渐进地详细讲解了Linux Shell编程的基本知识,主要包括Shell编程的基本知识、文本处理的工具和方法、正则表达式、Linux系统知识等。

本书旨在通过理清Linux Shell编程的脉络,从基本概念着手,以丰富、实用的实例作为辅助,使读者能够深入浅出地学习Linux Shell编程。本书以丰富的范例、详细的源代码讲解、独到的作者心得、实用的综合案例等为读者全面讲解Linux Shell编程。

本书的每章都配有综合案例,这些综合案例不仅可以使读者复习前面所学知识,还可以增强开发项目的经验。这些案例实用性很强,许多代码可以直接应用到Linux系统管理实践中。

本书附赠超大容量的DVD光盘,包含书中源代码、全程录像的视频讲解光盘,读者可以将视频与书配合使用,可以更快、更好地掌握Linux Shell编程技巧。

本书适合于Linux Shell编程的初学者和有一定Linux Shell编程基础但还需要进一步提高技能的人员。另外,本书对于有一定编程经验的程序员也有很好的参考价值。

前言

大一时,我刚刚开始接触Linux。那时候的我,沉醉于Linux华丽的用户界面,沉醉于使用Beryl软件(现在叫做Compiz Fusion)带来的图形效果,并且自我感觉良好:能够在姑娘们面前炫耀她们从没有见过的操作系统,应该算是一个计算机高手了。

直到某一天,参加一个学长(此人现在为南京大学高性能计算机研究所老师)的一个Linux讲座。他使用的是最简陋的图形界面(gnome默认配置),用两台运行着Ubuntu Linux系统的机器和一个摄像头,各自打开一个命令行进行演示,让我目瞪口呆。首先,他将两台机器配置成联网状态,然后将一台机器(A机器)连接摄像头,对着我们,另一台机器(B机器) 连接投影仪,打到大屏幕上。然后,他在A机器的命令行中输入了一串长长的命令,又在B机器的命令行中输入另一串命令,按回车键。最后,我们发现现场观众的实时动画被投影到大屏幕上!现场一片哗然!他解释道,这是应用管道实现的效果。在A机器上用读取命令将图像从摄像头中读取出来,通过管道连接压缩程序,压缩程序将一帧一帧的图像压缩,再传输到管道中;此时管道就通过无线局域网连接到B机器上。B机器上的解压程序从管道出口将压缩帧解压,通过流媒体播放器播放出来,再投放到大屏幕上!

管道!从此我爱上了黑乎乎的命令行,沉醉于它更强大的功能并且更有利于程序间的交互。这让我有种去除表象抓住实质的感觉。后来再见到Linux用户炫耀他们华丽的图形界面时,我的脑海中总会蹦出一个单词:Fish(菜鸟)。

的确,Linux命令行就是Linux的灵魂。而用户界面只是运行在灵魂上的皮囊而已。和Windows的命令行不同,Linux 命令行的确是一个强大的操纵系统的工具。你可以在命令行里完成几乎一切日常操作,并且比图形界面高效和强大得多。

有许多人用Linux当Windows用,这样的人大约是得了命令行恐惧症,认为那个黑乎乎的交互界面似乎应该是一些计算机Geek(极客)才用的。另外,有的人用了多年的Linux命令行还仅仅只会ls、cp、mv等几个简单命令,如果他的老板让他写一个Linux Shell脚本来完成某批处理任务,就一筹莫展了。而真正的Linux高手应是能够驾驭复杂的命令行和Shell语言的Linux Shell编程强人。

让我们一起走进Linux Shell编程的世界吧!

本书讲的是什么?

本书是Linux Shell编程的入门书籍。与市场上许多介绍Linux的书籍不同的是,这本书偏重于Linux Shell编程,将Shell当作一门语言来讲,而不是只有一两章提到Shell。实际上,一两章是绝对不够介绍Shell编程的,只能算蜻蜓点水而已。

本书内容讲解全面,涵盖了Linux Shell编程的方方面面。

第1章介绍了Shell的一些背景知识。我们从如何运行一个Shell程序开始讲起,循序渐进地介绍Shell的一些背景知识,如Shell运行的环境变量、Shell的本质等。最后,对Shell语言的优势进行探讨。

第2章是一个类似于总括的章节,主要讲解Shell编程的基础。包括Shell脚本参数的传递方式,Shell中命令的重形象与管道,基本文本检索的方法,UNIX/Linux系统的设计思想以及UNIX编程的基本原则。

第3章主要讲编程的基本元素。Linux Shell编程的基本元素包括变量、函数、条件控制和流程控制,以及非常重要的循环。学习本章将会对这些元素的使用有初步的认识。

第4章跳出Shell本身的范畴,介绍了正则表达式。Shell的强大之处在于文本处理,而正则表达式又是文本匹配的利器。关于正则表达式,除了介绍其基本知识外,还以两个案例给出了具体的应用场景,当然是在Linux Shell中完成。

第5章主要讲基本文本处理。大部分Linux Shell脚本都与文本处理相关,因此本章需要读者重点学习掌握。本章主要介绍一些文本处理的功能,如排序、去重、统计、打印、字段处理和文本替换。

第6章讲解文件和文件系统。主要介绍文件的查看、寻找与比较,还介绍了文件系统的定义与选择。

第7章介绍sed。sed也称为流编辑器,它可以对整行文本流进行处理。本章和第8章关系紧密,sed和awk常常被一起使用。

第8章介绍awk。与sed不同,awk往往更善于对字段进行处理。awk也是一门紧凑的语言,包括几乎所有语言的常见属性。

第9章主要介绍关于进程一些相关知识。Linux中的进程很多,本章介绍了进程的查看与管理,进程间通信。此处举了两个例子,一个是Linux中的第一个进程init,另一个是Linux系统中进程间管道的实现。然后介绍了Linux任务管理工具,最后,将Linux中的进程和线程做了一个比较,分析不同的应用场景。

第10章主要介绍Linux中的工具。包括不同的Shell,远程登录的工具SSH,管理多个终端的工具screen,以及文本编辑工具VIM。

第11章主要讲解了几个Linux Shell 编程的实例。通过这些实例,巩固前面所学知识,并加深对Linux Shell编程的理解。

谁适合读这本书?

本书适合Linux Shell编程的初学者和有一定Linux Shell编程基础知识,但还希望在此领域进一步学习的人。

另外,本书还适合在C、C++、JAVA或VB等领域对其中任何一门计算机语言有所了解的专业人员、初学者或爱好者使用。

这本书能帮助你什么?

本书的目标在于,帮助一个Linux Shell新手掌握Linux Shell脚本编程,从而能更深刻地理解与应用Linux系统的交互方式。

当然,仅仅靠本书还是不够的,还需要读者勤加练习。

如何联系作者?

如果您有任何意见或建议,可以通过邮箱联系我们。我们的邮箱是ollir@live.com。我们将会在第一时间给您回复。

感谢

感谢我曾经的导师和学校(南京大学),他们系统地教会我使用Shell编程与实用技巧。

感谢在大学阶段参与创建的一个Linux社团Open Association(http://njuopen.com),是社团促进了我的成长,并带领我走进Linux的广袤世界。

感谢我的女朋友,她做出了一定牺牲,让我周末有时间写稿,而不是陪她逛街。

感谢马泽民、逯永广、吕平、高克臻、张云霞、张璐、许小荣、王冬、王龙、张银芳、周新国、陈可汤、陈作聪、苏静、周艳丽、祁招娣、张秀梅、张玉兰、李爽、卿前华、王文婷、肖岳平、肖斌、蔡娜等同志,他们参与了本书的编写和最终的整理。

感谢出版社对稿件的校对和发行做出了极大努力。没有他们,我不可能完成这本书。

编者

2015年5月

第1章 初识Shell程序

欢迎来到Linux Shell编程世界。让我们开始吧。

在本章中,你将会学习如下知识。

(1)编译型语言与解释型语言的差异,Linux Shell编程的优势。

(2)如何编写和运行Linux Shell程序。

(3)Linux Shell运行在环境变量中,环境变量的设置。

本章涉及的Linux命令有:sh,bash,echo,pwd,chmod,source,rm,more,set,unset,export和env。

1.1 第一道菜

许多的UNIX书籍的开篇都会从各种UNIX版本和分支讲起,内容冗长,缺少实用性,还是让我们跳过这部分吧。

我们先来看一个实例,echo.sh。

实例:echo.sh

1 #! /bin/sh

2 cd /tmp

3 echo“hello world!”

这是一个完整的,可执行的Linux Shell程序。

它是一个相对简单的程序,以至于你一眼就能看出它“葫芦里卖的是什么药”(如果你知道echo命令的话)。别急着往后跳,因为程序并不是本章的重点。

现在运行一下这个程序,看看运行结果。

例1.1 运行实例echo.sh

alloy@ubuntu:~/LinuxShell/ch1$ pwd         #查看当前工作目录

/home/alloy/LinuxShell/ch1              #当前工作目录

alloy@ubuntu:~/LinuxShell/ch1$ chmod +x echo.sh #修改文件权限为可执行

alloy@ubuntu:~/LinuxShell/ch1$ ./echo.sh     #运行可执行文件

“hello world!”                     #Shell程序的执行结果

alloy@ubuntu:~/LinuxShell/ch1$ pwd         #再次查看当前的工作目录

/home/alloy/LinuxShell/ch1              #当前工作目录未发生改变

好,程序已经发挥作用了。很简单,不是吗?别高兴得太早,现在我要出2个问题,接招吧。

(1)程序第一行“#! /bin/sh”是什么意思?

(2)如何运行程序?

1.2 如何运行程序

运行Linux程序有3种方法。

(1)使文件具有可执行权限,直接运行文件。

(2)直接调用命令解释器[1]执行程序。

(3)使用source执行文件。

第三种方法运行结果和前两种是不同的。例1.1中我们运行程序时,采用的是第一种方法。

1.2.1 选婿:位于第一行的#!

当命令行Shell执行程序时,首先判断是否程序有执行权限。如果没有足够的权限,则系统会提示用户:“权限不够”。从安全角度考虑,任何程序要在机器上执行时,必须判断执行这个程序的用户是否具有相应权限。在第一种方法中,我们直接执行文件,则需要文件具有可执行权限。

chmod命令可以修改文件的权限。+x参数使程序文件具有可执行权限。

命令行Shell接收到我们的执行命令,并且判定我们有执行权限后,则调用Linux内核命令新建(fork)一个进程,在新建的进程中调用我们指定的命令。如果这个命令文件是编译型的(二进制文件),则Linux内核知道如何执行文件。不幸的是,我们的echo.sh程序文件并不是编译型的文件,而是文本文件,内核并不知道如何执行,于是,内核返回“not executable format file”(不是可执行的文件类型)出错信息。Shell收到这个信息时说:“内核不知道怎么运行,我知道,这一定是个脚本!”

Shell知道这是个脚本后,启动了一个新的Shell进程来执行这个程序。但是现在的Linux系统往往拥有好几个Shell,到底挑选哪个夫婿呢?这就要看脚本中意哪个了。在第一行中,脚本通过“#! /bin/sh”告诉命令行:“我只和他好,让他来执行吧!”

这种选婿方法有助于执行方式的通用化。用户在编写脚本时,在程序的第一行通过#!来设置运行Shell创建一个什么样的进程来执行此脚本。在我们的echo.sh中,Shell创建了一个/bin/sh(标准Shell)进程来执行脚本。

命令行在扫过第一行,发现#!时,开始试图读取#!之后的字符,搜寻解释器的完整路径。如果在第一行中的解释器也有参数,则一并读取。例如,我们可以这样来引用我们的解释器:

#! /bin/bash-l

这样,命令行Shell会启用一个新的bash进程来执行程序的每一行。并且,-l参数使得这个bash进程的反应与登录Shell相似。

这种选婿方法,使得我们可以调用任何的解释器,并不局限于Linux Shell。例如,我们可以创建这样一个python[2]程序:

1 #! /usr/bin/python

2 print“hello world!”

当这个文件被赋予可执行权限,并且用第一种方式运行时,就像调用了python解释器来执行一样。

NOTE:

填写完整的解释器路径。如果不知道某解释器的完整路径,可使用whereis命令查询。

alloy@ubuntu:~/Linux Shell/ch1$ whereis bash

bash: /bin/bash /etc/bash.bashrc /usr/share/man/man1/bash.1.gz

每个脚本的头都指定了一个不同的命令解释器,为了帮助你打破#!的神秘性,我们可以这样来写一个脚本,如例1.2所示。

例1.2 自删除脚本

1 #!/bin/rm

2 # 自删除脚本

3 # 当你运行这个脚本时, 基本上什么都不会发生……当然这个文件消失不见了

4 WHATEVER=65

5 echo "This line will never print!"

6 exit $WHATEVER # 不要紧, 脚本是不会在这退出的

当然,你还可以试试在一个README文件的开头加上一个#!/bin/more,并让它具有执行权限。结果将是文档自动列出自己的内容。

1.2.2 找碴:程序执行的差异

3种程序运行方法中,如果#!中指定的Shell解释器和第二种指定的Shell解释器相同的话,这两种的执行结果是相同的。我们来看看第三种方法的执行过程。

例1.3

alloy@ubuntu:~/LinuxShell/ch1$ pwd         #查看当前工作目录

/home/alloy/LinuxShell/ch1              #当前工作目录

alloy@ubuntu:~/LinuxShell/ch1$ source echo.sh  #执行echo.sh文件

“hello world!”                     #输出运行结果

alloy@ubuntu:/tmp$ pwd

/tmp                           #工作目录改变

细心的你,一定发现了不同!是的,当前目录发生了改变!

我们再来看例1.4。

例1.4

alloy@ubuntu:~/LinuxShell/ch1$ pwd           #查看当前工作目录

/home/alloy/LinuxShell/ch1

alloy@ubuntu:~/LinuxShell/ch1$cd /tmp         #改变当前工作目录

alloy@ubuntu:/tmp$ pwd

/tmp                              #工作目录改变

为什么例1.3和例1.4的cd命令可以改变工作目录,而例1.1中的工作目录并没有改变呢?

这个问题的答案,我们将在1.2.3小节揭晓。

1.2.3 Shell的命令种类

Linux Shell可执行的命令有3种:内建命令、Shell函数和外部命令。

(1)内建命令就是Shell程序本身包含的命令。这些命令集成在Shell解释器中,例如,几乎所有的Shell解释器中都包含cd内建命令来改变工作目录。部分内建命令的存在是为了改变Shell本身的属性设置,在执行内建命令时,没有进程的创建和消亡;另一部分内建命令则是I/O命令,例如echo命令。

(2)Shell函数是一系列程序代码,以Shell语言写成,它可以像其他命令一样被引用。我们在后面将详细介绍Shell函数。

(3)外部命令是独立于Shell的可执行程序。例如find、grep、echo.sh。命令行Shell在执行外部命令时,会创建一个当前Shell的复制进程来执行。在执行过程中,存在进程的创建和消亡。外部命令的执行过程如下:

①调用POSIX系统fork函数接口,创建一个命令行Shell进程的复制(子进程);

②在子进程的运行环境中,查找外部命令在Linux文件系统中的位置。如果外部命令给出了完全路径,则跳过查找这一步;

③在子进程里,以新程序取代Shell复制并执行(exec),此时父进程进入休眠,等待子进程执行完毕;

④子进程执行完毕后,父进程接着从终端读取下一条命令。过程如图1-1所示。

NOTE:

(1)子进程在创建初期和父进程一模一样,但是子进程不能改变父进程的参数变量。

(2)只有内建命令才能改变命令行Shell的属性设置(环境变量)。

我们回到例1.1。在这个例子中,我们使用cd(内建命令)试图改变工作目录。但是未获成功。为了理解失败的原因,图1-2说明了执行的过程。

在我们运行Shell程序的3种方法中,前两种方法的执行过程都可以用图1-2解释。

(1)父进程接收到命令“./echo.sh”或“/bin/sh echo.sh”,发现不是内建命令,于是创建了一个和自己一模一样的Shell进程来执行这个外部命令。

(2)这个Shell子进程用/bin/sh取代自己,sh进程设置自己运行环境变量,其中包括$PWD变量(标识当前工作目录)。

(3)sh进程依次执行内建命令cd和echo,在此过程中,sh进程(子进程)的环境变量$PWD被cd命令改变,注意:父进程的环境变量并没有改变。

(4)sh子进程执行完毕,消亡。一直在等待的父进程醒来继续接收命令。

这样,例1.1中cd命令失效的原因就可以理解了!聪明的你,一定猜到了例1.3中使用source命令为什么可以改变命令行Shell的环境变量了吧!

这也是在例1.1中目录没有改变的原因:父进程的当前目录(环境变量)无法被子进程改变!

NOTE:

使用source执行Shell脚本时,不会创建子进程,而是在父进程中直接执行!

source

语法:

source file

. file

描述:

使用Shell进程本身执行脚本文件。souce命令也被称为“点命令”,通常用于重新执行刚修改的初始化文件。使之立即生效。

行为模式:

和其他运行脚本不同的是,source命令影响Shell进程本身。在脚本执行过程中,并没有进程创建和消亡。

警告:

当需要在程序中修改当前Shell本身环境变量时,使用source命令。

1.3 Linux Shell的变量

你一定想知道,Shell是如何记忆工作目录的改变的?当Shell接收到外部命令时,在庞大的文件系统中,如何迅速定位到命令文件呢?如何设定Linux Shell的环境变量?如何使黑乎乎的命令行看起来更漂亮?这些问题都是本节要解决的问题。

1.3.1 变量

变量(variable)在许多程序设计语言中都有定义,与变量相伴的有使用范围的定义。Linux Shell也不例外。变量,本质上就是一个键值对。例如,str=“hello”,就是将字符串值(value)“hello”赋予键(key)str。在str的使用范围内,我们都可以用str来引用“hello”值,这个操作叫做变量替换。

Shell变量的名称以一个字母或下划线符号开始,后面可以接任意长度的字母、数字或下划线。和许多其他程序设计语言不同的是,Shell变量名称字符并没有长度限制。Linux Shell并不对变量区分类型。一切值都是字符串,并且和变量名一样,值并没有字符长度限制。神奇的是,bash也允许比较操作和整数操作。其中关键因素是:变量中的字符串值是否为数字。例如1.5所示。

例1.5 Linux Shell中的变量

alloy@ubuntu:~/Linux Shell/ch1$ long_str="Linux_Shell_programming"

alloy@ubuntu:~/Linux Shell/ch1$ echo $long_str

Linux_Shell_programming

alloy@ubuntu:~/Linux Shell/ch1$ add_1=100

alloy@ubuntu:~/Linux Shell/ch1$ add_2=200

alloy@ubuntu:~/Linux Shell/ch1$ echo $(($add_1+$add_2))

300

由例1.5可见,虽然Linux Shell中的变量都是字符串类型的,但是同样可以执行比较操作和整数操作,只要变量字符串值是数字。

变量赋值的方式为:变量名称=值,其中“=”两边不要有任何空格。当你想使用变量名称来获得值时,在名称前加上“$”。例如,$long_str。当赋值的内容包含空格时,请加引号,例如:

alloy@ubuntu:~/Linux Shell/ch1$ with_space="this contains spaces."

alloy@ubuntu:~/Linux Shell/ch1$ echo $with_space

This contains spaces

注意:$with_space事实上只是${with_space}的简写形式,在某些上下文中$with_space可能会引起错误, 这时候你就需要用${with_space}了。

当变量“裸体”出现的时候(没有$前缀的时候),变量可能存在如下几种情况:变量被声明或被赋值;变量被unset;或者变量被export。

变量赋值可以使用“=”(比如var=27), 也可以在read命令中或者循环头进行赋值,例如,for var2 in 1 2 3。

被一对双引号("")括起来的变量替换是不会被阻止的。所以双引号被称为部分引用,有时候又被称为“弱引用”。但是如果使用单引号的话(' '),那么变量替换就会被禁止了,变量名只会被解释成字面的意思,不会发生变量替换。所以单引号被称为“全引用”,有时候也被称为“强引用”。例如:

alloy@ubuntu:~/LinuxShell/ch1$var=123

alloy@ubuntu:~/LinuxShell/ch1$ echo '$var'    #此处是单引号

alloy@ubuntu:~/LinuxShell/ch1$ echo "$var"    #此处是双引号

$var

123

在这个例子中,单引号中的$var没有替换成变量值123,也就是说,变量替换被禁止了;而双引号中的$var发生了变量替换。即:单引号为全引用(强应用),双引号为弱引用。

在Shell的世界里,变量值可以是空值(“NULL”值),就是不包含任何字符。这种情况很常见,并且也是合理的。但是在算术操作中,这个未初始化的变量常常看起来是 0。但是这是一个未文档化(并且可能是不可移植)的行为。例如:

alloy@ubuntu:~/LinuxShell/ch1$ echo "$uninit"       #未初始化变量

#此行为空,没有输出

alloy@ubuntu:~/LinuxShell/ch1$ let "uninit+=5"      #未初始化变量加5

alloy@ubuntu:~/LinuxShell/ch1$ echo "$uninit"

5                                #此行输出结果为5

alloy@ubuntu:~/LinuxShell/ch1$

Linux Shell中的变量类型有两种:局部变量和全局变量。

顾名思义,局部变量的可见范围是代码块或函数中。这一点与大部分编程语言是相同的。

但是,局部变量必须明确以local声明,否则即使在代码块中,它也是全局可见的。

环境变量是全局变量的一种。全局变量在全局范围内可见,在声明全局变量时,不需要加任何修饰词。

例1.6 测试全局变量和局部变量的适用范围

1 #! /bin/sh

2 # 测试全局变量和局部变量的适用范围

3 num=123

4 func1 ()

5 {

6 num=321             #在代码块中声明的变量

7 echo $num

8 }

9 func2()

10 {

11  local num=456        #声明为局部变量

12 echo $num

13 }

14 echo $num           #显示初始时的num变量

15 func1              #调用func1,在函数体中赋值(声明?)变量

16 echo $num           #测试num变量是否被改变

17 func2              #调用func2,显式声明局部变量

18 echo $num           #测试num变量是否被改变

我们看看例1.5的运行结果:

123         #初始值

321         #func1内被改变

321         #func1内的赋值影响到函数体外

456         #func2内声明局部变量

321         #函数体外的num未改变

例1.6的解释如下。

我们设置了一个变量num,初始值赋值为123。

调用func1,func1中的赋值命令num=321将num的123覆盖。注意,此处虽然位于函数体内,但是还是能够修改全局变量,此处的num变量就是全局环境中的num。

调用func2,func2中定义了局部(local)变量num,并且赋值456。在func2内部,num变量的值为456,此时为局部的;当func2返回后,回到全局作用区,此时num的值并未改变,为321。

1.3.2 用echo输出变量

例1.7:

alloy@ubuntu:~/Linux Shell/ch1$ echo $PATH

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/arm/4.3.3/bin

alloy@ubuntu:~/Linux Shell/ch1$ echo "hello world!"

Hello world!

在例1.7中,我们展示了echo的使用。echo命令的任务就是输出一行文本。多用于提示用户或产生数据。

我们将在echo的manpage中显示更多选项。

echo

echo [OPTION]... [STRING]...

语法:

描述:

允许在标准输出上显示STRING(s)。

主要选项:

-n不输出行尾的换行符。

行为模式:

echo将各个参数打印到标准输出。参数间以一个空格隔开,在输出结束后,换行。它会解释每个字符串里的转义序列(escape sequences)。转义序列可以用来表示特殊字符,以及控制其行为模式。

警告:

echo命令的-n选项并不被所有Linux版本支持。POSIX标准中并未包含此选项。

转义字符可以表示程序中难以看得见或者难以输入的特殊字符。当echo遇到转义序列时,就会打印相应的字符。echo支持的转义字符如表1-1所示。

1.3.3 环境变量的相关操作

在通常情况下,每个进程都有自己的“环境”,这个环境是由一组变量组成的,这些变量中存有进程可能需要引用的信息。在这种情况下,Shell与一般的进程没什么区别。

每次当一个Shell启动时,它都将创建适合于自己环境变量的Shell变量。更新或者添加一个新的环境变量的话,这个Shell都会立刻更新它自己的环境(换句话说,更改或增加的变量会立即生效),并且所有后继生成的Shell子进程(即这个Shell所执行的命令)都会继承这个环境。

如果一个脚本要设置一个环境变量,那么需要将这些变量“export”出来,也就是需要通知到脚本本地的环境。这是export命令的功能。

一个脚本只能够export变量到这个脚本所产生的子进程,也就是说只能够对这个脚本所产生的命令和进程起作用。如果脚本是从命令行中调用的,那么这个脚本所export的变量是不能影响命令行环境的。也就是说,子进程是不能够export变量来影响产生自己的父进程的环境的。但是,当使用source命令执行脚本时,因为没有子进程的产生,此时脚本中的export命令将会影响父进程的环境。

export

语法:

export [-fnp][变量名称]=[变量设置值]。

描述:

export命令用于设置或显示环境变量。

主要选项:

-f 代表[变量名称]中为函数名称。

-n 删除指定的变量。变量实际上并未删除,只是不会输出到后续指令的执行环境中。

-p 列出所有的Shell赋予程序的环境变量。

行为模式:

export命令修改当前Shell进程的环境变量。若将export命令置于脚本中被调用执行,则export命令对父Shell进程的环境变量没有影响。

警告:

Shell中执行程序时,Shell会提供一组环境变量。export可新增,修改或删除环境变量,供后续执行的程序使用。export的效力仅及于该此登录操作。

export命令用于设置当前进程的环境变量。但是有效期仅维持到当前进程消亡为止。下次重新登录到命令行Shell时,以前对Shell的export设置都无法恢复。如果想要把对环境变量的设置永久保存,则可以将export命令置于Shell登录时执行的启动文件中。例如:

# 设置环境变量PATH

export PATH=/bin:/usr/bin:/usr/X11R6/bin:/usr/local/bin

启动文件包含别名和环境变量,正是这些别名和环境变量才使得Shell可以作为一个用户Shell来运行。当系统初始化之后,这些别名和变量也可被其他的Shell脚本调用。

对于从bash来说,启动文件在表1-2中列出。

① 参见Shell的发展史

注意,此处的$HOME为环境变量,$HOME变量的值是登录者的用户目录。$HOME目录下存放有许多用户个人相关的文件和数据,还有对用户定制的配置文件。这些配置文件往往以“.”开头(隐藏文件)。例如:

alloy@ubuntu:~/Linux Shell/ch1$ echo $HOME #显示环境变量HOME

/home/alloy

对于其他Shell的启动文件,请参阅相关章节。

export命令设置适用于当前Shell的环境变量值。修改后维持不变,直到当前Shell消亡。env命令则可以临时改变环境变量值。

alloy@ubuntu:~/Linux Shell/ch1$ env–i PATH=./:$PATH echo.sh

“-i”选项使Shell在执行echo.sh时,清空所有由父Shell继承来的环境变量,仅仅设置命令中指定的PATH变量(将“./”也添加到命令搜寻路径里)。这样,在执行echo.sh时,就不需要给出完全路径(./echo.sh),直接给出命令文件名,系统就知道在哪里找该命令了。

unset命令从当前Shell中删除函数或变量。删除变量时,使用“-v”选项(默认情况),删除函数时,使用“-f”选项。例如:

alloy@ubuntu:~/Linux Shell/ch1$ echo $vari

123

alloy@ubuntu:~/LinuxShell/ch1$ unset vari       #删除变量(unset–v vari)

alloy@ubuntu:~/LinuxShell/ch1$ echo $vari

#此行为空,因为vari为空

alloy@ubuntu:~/LinuxShell/ch1$ hello() {echo“hello, world!”}   #定义函数

alloy@ubuntu:~/LinuxShell/ch1$ unset–f hello             #删除函数

env

语法:

env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]

描述:

在重建的环境中运行程序,设置环境中的每个NAME为VALUE,并且运行COMMAND。

主要选项:

-i,--ignore-environment

不带环境变量启动

-u,--unset=NAME

从环境变量中删除一个变量

行为模式:

未提供COMMAND时,显示环境中所有变量的名称和值。提供COMMAND时,根据参数重建环境变量后,在新的环境中运行COMMAND。

unset

语法:

unset [-v] variable…

unset–f function…

描述:

从当前Shell删除变量或函数。

主要选项:

-f

删除指定的函数:

-v

删除指定的变量。在没有提供任何选项的情况下,默认此选项。

行为模式:

如果没有提供任何选项,则默认unset为删除变量(-v选项)。如果使用-f选项,则被视为删除函数操作,参数为函数名称。

NOTE:

Env函数和set函数不同。Env函数显示的是环境变量,而set函数则显示所有的本地变量,包括用户的环境变量。例如,当用户在命令行中设置var=123时,set函数将显示var变量,而env函数则不显示(var此时是本地变量,不是环境变量)。如果使用export var=123命令,则set命令和env命令都可以显示var变量。

1.3.4 Shell中一些常用环境变量

Linux 是一个多用户的操作系统。每个用户登录系统后,都会有一个专用的运行环境。通常用户默认的环境都是相同的,这个默认环境实际上就是一组环境变量的定义。用户可以对自己的运行环境进行定制,其方法就是修改相应的系统环境变量。

表1-3列出了一些常见的环境变量。

在这些变量中,PATH变量中存储有一系列路径,路径中以冒号(:)分隔开。格式例如:

/bin:/usr/bin:/usr/X11R6/bin:/usr/local/bin

一般来说,PATH路径中至少包含/bin和/usr/bin两个目录。大部分时候还有/usr/X11R6/bin等X相关的目录。当Shell接收到一个命令,并且这个命令非内建命令,也没有给出完整路径时,Shell则在PATH变量中依次从左到右搜索目录,直到找到该命令为止。如果一个命令在PATH中两个不同目录下都存在,则位于PATH前端的目录中的命令会被执行。

PS1/PS2变量可以改变Shell的提示符。默认情况下,普通用户的提示符是“$”,root用户的提示符是“#”。可以通过修改这两个变量让Shell的交互界面更友善。

1.4 Linux Shell是解释型语言

计算机不能直接理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言编写的程序。

1.4.1 编译型语言与解释型语言

翻译的方式有两种,一种是编译(compile),另一种是解释(interpret)。两种方式只是翻译的时间不同。编译型语言写在程序执行之前,需要一个专门的编译过程,把程序编译成为机器语言的文件,例如,Windows系统中的EXE文件。编译好后运行该文件的话就不用重新翻译了,直接使用编译的结果就行。因为翻译只做了一次,运行时不需要翻译,所以编译型语言的程序执行效率高。Linux中的许多外部命令都是这种类型,它们的文件格式是二进制文件。

解释型语言则不同,解释型语言的程序不需要编译,省了道工序,但在其运行程序的时候需要翻译,例如,Linux Shell语言中,专门有一个解释器能够直接执行程序(/bin/sh或者bash, zsh, csh等),每个语句都是执行的时候才翻译。这样解释型语言每执行一句就要翻译一次,效率比较低。

编译型语言与解释型语言的差异如下。

(1)许多中型、大型的程序都是用编译型语言写成。例如,C/C++,Java[3],Fortran 等。这些大型语言的源代码(source code)需要经过编译才能转化为目标代码(object code),机器才可读、可执行。

(2)编译型语言的优点是高效。缺点是难以执行上层的一些简单操作,因为编译型语言运行于机器底层。例如,在C++中就难以对某目录下的所有文件执行批量重命名。

(3)脚本语言都是解释型语言。解释型语言在UNIX系统中很常见,例如Shell、Perl、Python、awk、Ruby等。

(4)解释型语言的执行层面高于编译型语言,因此可以轻松地进行一些高级操作。功能强大的解释性语言往往被称为胶水语言(如python),可以迅速地利用各种工具和语言属性搭建想要的功能。脚本语言被广泛应用于系统管理、模型搭建等领域。但解释型语言的劣势也十分明显:执行效率低。

这里要特别讲一下Python。Python是一种解释型的语言。但是为了效率考虑,Python也提供了编译的方法。编译之后是bytecode的形式。Python也提供了和Java类似的VM来执行这样的bytecode。不同的是,因为Python是一种解释型的语言,所以编译(compile)不是一个强制的操作。事实上,编译是一个自动的过程。多数情况下,你甚至不会留意到它的存在。编译成bytecode可以节省加载模块的时间,从而提高效率。

1.4.2 Linux Shell编程的优势

使用Linux Shell作为编程工具的优势在于,它运行在高于系统内核的环境,能够简单地执行一些文件系统级的高级操作。因此,迅速地搭建系统,维护需要的功能变成可能。这种特性,也使得Linux Shell的编程效率十倍、百倍的高于其他编译型语言。由于Linux众多工具的支持,往往用编译型语言需要若干天的工作,熟练的Linux Shell程序员只要几个小时就可以让程序运行地很好。Linux Shell编程的优势有以下几点。

1.简洁性

Linux Shell所处的内核外层环境使得任何高级操作成为可能。

2.开发容易

GNU 多年的千锤百炼使得 UNIX/Linux 的工具集变成程序员手中的利器,并很好地遵循了UNIX哲学使开发前人的积累上变得容易。

3.便于移植

由于POSIX接口的支持,只要你不使用一些危险特性(被部分操作系统支持,但不被POSIX接口支持),Linux Shell只要写一次,往往能无障碍地运行于众多UNIX/Linux版本上。

1.5 小结

我们终于结束了第一章!在本章,我们学习到了如下知识。

Linux Shell脚本应该以“#!”开始,这个机制告诉命令行Shell应该选择哪个解释器来解释这个脚本。这种机制提供了一种编程规范,提高编写脚本的灵活性,例如,你可以选择使用其他语言来编写脚本。

Linux Shell运行在环境中。环境变量在用户登录启动第一个Shell(登录Shell)时从启动文件中读取。不同Shell的启动文件不相同。环境变量在运行过程中可以通过export命令改变。env命令为了运行命令,能够临时创造全新的环境变量。

Linux Shell运行命令时,会创建一个和父进程一模一样的子进程。子进程的环境变量继承父进程。所有在子进程中对其环境变量的操作都不会影响到父进程。例如,cd命令的执行仅仅改变子进程的环境变量。

$PATH是Linux的环境变量之一。$PATH往往包含了Linux各个可执行文件的所在目录。当 Shell 接收到命令发现此命令为非内部命令,并未给出了完整路径时,就会依次在$PATH变量中以从前到后的顺序搜寻命令文件,直到找到为止。

可以将环境变量的改变用export写入/etc/profile或$HOME/.profile中。后者的优先级高于前者。例如,export PATH=$HOME/bin:$PATH,即向$PATH路径中添加$HOME/bin目录。这样,在用户每次登录时都会自动加载环境变量的改变。

编译型语言的执行效率高于解释型语言。但是解释型语言在开发的容易度、可移植性和简洁性等方面都高于编译型语言。因为Linux Shell运行于内核之上,因此可以方便地进行一些文件系统的高级操作。具体使用哪种语言,则视程序的需求而定。

UNIX/Linux的Shell语言是被广泛使用的脚本语言,常见于系统维护中。如果合理地使脚本维持在POSIX接口支持的范围内,则可能获得高度的可移植性。即使有问题,也可以通过很小的改动来完成想要做的事情。

最后,欢迎进入Linux Shell编程世界!

[1].参见1.4节“Linux Shell是解释型语言”。

[2].参见http://www.python.org。

[3].Java并不是严格地被翻译成机器语言,而是被编译成字节码,然后用解释方式执行字节码。

第2章 Shell编程基础

欢迎回来。

在第1章里,我们演示了一个全世界最简单的Shell脚本,并且讲解了如何运行Shell脚本。我们比较了Shell编程与其他高级语言(如C/C++、Java语言)编程的不同,此外,我们还介绍了Shell脚本运行的环境。

在本章你将学到如下知识。

(1)在命令行Shell交互时,向脚本传递参数的方法,使脚本的可定制性更强。

(2)Shell 的部分基本也是最核心的元素,例如,重定向与管道,以及一切皆文件的思想。这些往往具有一些UNIX哲学(UNIX philosophy)的意味。

本章涉及的Linux命令有:grep,>,|,mv,mkdir,ps,cat,head,read,ls。

2.1 向脚本传递参数

为什么要向Shell脚本传递参数?参数传递可以将外部的值传递到脚本的内部函数中,提高脚本的灵活性;参数传递可以添加脚本的适用选项,增加脚本的可定制性,以应付不同的情况。在本节中,我们将介绍参数传递的方法,以及bash Shell中的各种参数扩展。

2.1.1 Shell脚本的参数

废话不多说,我们先来看一个例子吧。

例2.1 Shell编程中的函数

alloy@ubuntu:~/Linux Shell/ch2$ testfunc ()    #从这里开始进入编写函数的状态

>{                             #此时有提示符>,开始编写函数

>echo "$# parameters";

>echo "$@";

>}

alloy@ubuntu:~/Linux Shell/ch2$ testfunc      #执行函数,没有参数

0 parameters                       #0个参数

#此行为空,因为没有参数输出

alloy@ubuntu:~/Linux Shell/ch2$ testfunc a b c

3 parameters

a b c

alloy@ubuntu:~/Linux Shell/ch2$ testfunc a "b c"

2 parameters

a b c

alloy@ubuntu:~/Linux Shell/ch2$

例2.1演示了Shell函数的参数传递。在交互Shell中,我们定义了testfunc()函数,函数输出两行引用参数,一行是$#的值,一行是$@的值。你一定猜到了吧,$#代表传入函数的参数个数,而$@代表所有参数的内容,这个函数的用途只是告诉我们它所拥有的参数数量并显示这些参数。

Shell脚本处理参数的方式与函数处理参数的方式相同。实际上,我们会经常发现,脚本往往由很多小型的函数装配而成。

与例2.1的函数参数相同,我们还有Shell脚本的参数传递,见例2.2。

例2.2 Linux Shell中脚本的参数传递

alloy@ubuntu:~/Linux Shell/ch2$ cat testfunc.sh            #注释1

#!/bin/bash

echo "$# parameters"

echo "$@";

alloy@ubuntu:~/Linux Shell/ch2$ ./testfunc.sh a "b c"        #注释2

2 parameters

a b c

alloy@ubuntu:~/Linux Shell/ch2$

例2.2的解释如下。

注释1:testfunc.sh脚本输出参数的个数和参数内容。

注释2:在这里,我们通过命令行传递给testfunc.sh脚本两个参数,一个是“a”,另一个是由引号引起的参数“b c”,可以看到,正确输出了结果。

在例2.1和例2.2中,我们都了解了$@和$#的用法。那么,这样的用法还有哪些呢?我们看表2-1。

① 位置参数(positional parameters),即Shell脚本的命令行参数(command-line arguments)。

NOTE:

如果拥有的参数多于9个,则不能使用$10来引用第10个参数。首先,必须处理或保存第一个参数($1),然后使用shift命令删除参数1并将所有剩余的参数下移一位,因此,$10就变成了$9,依此类推。$#的值将被更新以反映参数的剩余数量。在实践中,最常见的情况是将参数迭代到函数或Shell脚本,或者迭代到命令替换使用for语句创建的列表,因此这个约束基本不成问题。

在表2-1中会发现,Shell可能将传递参数的列表引用为$*或$@,而是否将这些表达式用引号引用将影响它们的解释方式。对于例2.1中的函数而言,使用$*、“$*”、$@或“$@”输出的结果差别不大,但是如果函数更复杂一些,就没有那么肯定了,当分析参数或将一些参数传递给其他函数或脚本时,使用或不用引号的差别就很明显。

2.1.2 参数的用途

讲了这么多,到这里,你可能还是一头雾水:参数传递有那么大用处吗?我们来看些例子吧。

例2.3 ps.sh

1 #! /bin/sh

2 # Shell参数传递演示,查看系统中某进程是否正在运行

3 # 记得使用chmod +x使ps.sh可运行

4

5 ps-e Lf | grep $1 #ps命令查看当前系统进程

运行脚本情况如下:

alloy@ubuntu:~/Linux Shell/ch2$ chmod +x ps.sh #将脚本加上可执行权限

alloy@ubuntu:~/Linux Shell/ch2$ ./ps.sh firefox #测试firefox浏览器是否正在运行

alloy 6116 2521 6116 0 1 15:41 pts/0 00:00:00 /bin/sh ./ps.sh firefox

alloy 6118 6116 6118 0 1 15:41 pts/0 00:00:00 grep firefox

alloy@ubuntu:~/Linux Shell/ch2$ mv ./ps.sh $HOME/bin

alloy@ubuntu:~/Linux Shell/ch2$

在例2.3中,我们使用了ps命令列出所有系统当前进程(ps的用法,我们在本书的第9章进程中会详细介绍)。另外,我们使用了管道和grep命令[1]。这两个命令让我们从众多的进程输出中检索出包含了firefox的行。

要使ps.sh可以直接运行,chmod +x使之具有可执行权限。然后,将“firefox”字符串作为参数传递给ps.sh,在ps.sh脚本中,使用$1位置变量来访问命令行的第一个参数。

因此,命令的效果就相当于:

ps–e Lf | grep firefox

例2.3程序中的部分行,我们以#开头,为程序添加注释。这样做有助于理解脚本的用途。当脚本中出现正常的#(不被引号括起或反义)时,#后面的文本在执行时都被忽略。

为程序添加上注释总不会错。注释有助于阅读程序的人理解程序,或一年后的你理解当初那个天才的自己是怎样思考的。注释的添加应当遵循“刚刚好”原则,不啰唆,但也不过分粗略。总之恰到好处。

我们在运行的最后把ps.sh文件移到了$HOME目录下的bin文件夹内。是时候创建一个自己的可执行文件库了!通过不断积累,它将成为你成长为高手的标志。

你可以通过如下命令创建这个文件夹:

alloy@ubuntu:~/Linux Shell/ch2$ mkdir ~/bin

alloy@ubuntu:~/Linux Shell/ch2$

符号“~”在运行时将被扩展为你的$HOME目录。我的机器上是/home/alloy。

这个程序还没有达到完美。试想,如果我们不给ps.sh传递任何参数,将会发生什么事情?

alloy@ubuntu:~/Linux Shell/ch2$ ./ps.sh

用法: grep [选项]... PATTERN [FILE]...

试用‘grep--help’来获得更多信息。

alloy@ubuntu:~/Linux Shell/ch2$

在这个应用中,我们没有传递任何参数给ps.sh。所以,程序出错了。在后续章节我们将详细讲解如何测试参数数目,如何处理参数数目不符的异常情况。

下面是mv命令的manpage。

mv

语法:

mv [options]... Source Dest

mv [options]... Source... Directory

描述:

移动或重命名文件或目录。

如果最后一个参数是一个已经存在的目录,则“mv”命令将所有前面提到的文件移动到这个目录中。文件名不变;如果给出了两个文件,“mv”命令将第一个文件重命名为第二个文件。

如果最后一个文件不是目录并且文件名个数超过两个,“mv”命令就会报错。

主要选项:

-E

-E选项需要下列参数之一。如果省略-E选项,warn是默认行为。

force

如果文件的固定范围大小或者空间保留无法保存,则对文件的mv操作失败。

ignore

在保存范围属性时忽略任何错误。

warn

如果文件的空间保留或者固定范围大小无法保存就发出警告。

-f

在覆盖现有文件之前不提示。

-i

移动文件或目录到现有的路径名称之前进行提示,通过后跟问号显示文件名。如果以y或语言环境中y的相等物开始的一行应答,移动就继续。其他任何应答都阻止移动发生。

警告:

许多经验丰富的程序员也会常常使用mv命令覆盖已有文件。所以,最好使用alias命令将mv命令绑定到mv-i:

alias mv="mv-i"

下面是mkdir的manpage。

mkdir

语法:

mkdir [-m Mode ] [-p ] Directory ...

描述:

创建一个或多个新的目录。

主要选项:

-m Mode

设置新创建的目录的许可位,其值由变量Mode指定。Mode变量的值与chmod命令的Mode参数的值一样,以符号形式或者数字形式表现。

当使用符号格式指定-m标志时,操作符号+(加)和−(减)都是相对于假设的许可权设置a=rwx来进行解释的。+ 向默认方式添加许可权,并且−从默认方式删除许可权。请参阅chmod命令以获取许可权的位和格式的完整描述。

-p

创建丢失中间路径名称目录。如果没有指定-p标志,则每个新创建的父目录必须已经存在。

中间目录是通过自动调用以下的mkdir命令来创建的:

mkdir-p-m $(umask-S),u+wx $(dirname Directory) &&

mkdir [-m Mode] Directory

其中,[-m Mode]表示随mkdir命令的原始调用所提供的任何选项。

mkdir命令忽略任何命名现有的目录的Directory参数。不发出错误提示。

行为模式:

mkdir命令创建由Directory参数指定的一个或多个新的目录。每个新目录包含标准项(. 点)和..(点-点)。我们可以使用-m Mode标志为新的目录指定许可权,可以使用umask子例程为mkdir命令设置默认方式。

将新目录的拥有者标识和组标识分别设置为进程的有效用户标识和组标识。setgid位是从父目录中继承下来的。要更改setgid位,可以指定-m Mode标志或者在目录创建后发出chmod命令。

注:要创建新目录,必须在父目录中具有写入权限。

2.2 I/O重定向

程序总难免输入输出,与外界的交互是程序功能强大与灵活的必要条件。我们在第1章中讲解了使用echo命令输出参数值的方法,在交互Shell界面,你一定看到了echo命令将参数的值输出在交互命令行下一行的情况。当我们使用echo命令输出时,按下回车键的一瞬间,echo命令的行为是这样的:

(1)读取echo参数中的变量,将所有变量替换成值,变成字符串输出;

(2)在输出的末尾追加换行符,退出程序。

此时,控制又回到了交互Shell手中,我们就看到交互Shell的提示符。

问题来了:echo命令从哪里输入?输出到哪里?如果程序运行过程中遇到错误怎么办?

我们将在下面给出答案,请继续往下阅读。

2.2.1 标准输入、标准输出与标准错误

程序是什么?这是个仁者见仁的问题。UNIX 程序员都不会否认这样一个看法:程序读取输入(数据的来源),运算后输出(数据的目的端),以及报告异常和错误。这三者就是标准输入(Standard Input),标准输出(Standard Output)和标准错误(Standard Error)了。

为什么如此肯定UNIX程序员都会赞同这个观点呢?啊哈!因为这正是管道[2]在UNIX世界里地位如此崇高的原因!程序不需要知道它的输入和输出背后是什么在支持着,是磁盘上的文件、终端设备、另一个程序还是你的手机与电脑的网络连接端口?程序不想知道。在程序运行的时候,标准输入应该已经打开,供其使用。

许多UNIX的程序都遵循这个原则。从标准输入读入,经过处理,再从标准输出输出。遵循这个原则的程序往往被称为过滤器(filter)。过滤器加管道,这是UNIX的世界!并且,宽进严出的程序是受人称赞的。

对于交互命令行来说,标准输入,标准输出都是终端。我们来看cat的例子:

例2.4 cat命令

alloy@ubuntu:~/Linux Shell/ch2$ cat

I am cat, not a cat.                    #由用户输入

I am cat, not a cat.                    #由程序返回

I read from the standard input, or this Shell.

I read from the standard input, or this Shell.

I write to the standard output, or this Shell.

I write to the standard output, or this Shell.

^D                             #“Ctrl+D”组合键,文件结尾

alloy@ubuntu:~/Linux Shell/ch2$           #程序返回

这个命令很简单,cat从标准输入读取数据源,再将数据标准输出。因为对于命令行来说,标准输入和标准输出都对应到终端上,所以你可以看到cat像是一只应声虫一样,你说一句,它重复一句。

你可能觉得神奇,既然cat只从标准I/O操作,那么是谁替执行中的程序初始化了标准输入、标准输出和标准错误呢?

答案是当你登录UNIX/Linux系统时,系统便将标准输入、标准输出和标准错误安排到了你的终端,让你能够直接与终端交互。所有从终端派生的程序都继承了这种标准输入和标准输出,直到使用重定向或管道功能将此改变。

接下来的章节要讲的内容就是这方面内容。

下面是cat的manpage。

cat

语法:

cat [- q ] [ -r ] [- s ] [- S ] [- u ] [- n [- b ] ] [- v [- e ] [- t ] ] [ -| File ... ]

描述:

连接或显示文件。

行为模式:

cat命令按顺序读取每个File参数并将它写至标准输出。如果未指定文件名,cat命令会从标准输入读取。也为标准输入可以指定-(短划线)的文件名。

注意:不要使用重定向符号>(caret)将输出重新定向到输入文件之中。如果这么做了,将会丢失输入文件中的原始数据,因为Shell在cat命令可读取该文件之前先将它截断了。有关更多信息,请参阅章节2.2.2小节“重定向与管道”。

主要参数:

-b 当与-n标志一起指定时,省略来自空行的行号。

-e 当与-v标志一起指定时,在每行末尾显示一个$(美元符号)。

-n 显示在行号之后的输出行,按顺序从1开始编号。

-q 如果cat命令无法找到输入文件,则不显示消息。该标志等同于-s标志。

-r 以一个空行来替代多个连续的空行。该标志等同于-S标志。

-s 如果cat命令无法找到输入文件,则不显示消息。该标志等同于-q标志。

-S 以一个空行来替代多个连续的空行。该标志等同于-r标志。

-t 如果与-v标志一起指定,则将跳格字符显示为^I。

-u 不要缓冲输出。默认值为缓冲的输出。

-v

将非打印字符显示为可视字符,除了跳格符、换行符和换页符。ASCII控制字符(八进制000-037)打印成^n。其中,n是八进制范围100-137(@, A, B, C,..., X, Y, Z, [, \, ], ^, 和_)内对应的ASCII字符;而DEL字符(八进制0177)则打印成^?。其他非打印字符打印成M-x,其中,x是由最低七位指定的ASCII字符。

当与-v选项一起使用时,可使用以下选项。

-e

在新行之前的每行末尾将打印一个 $字符。

-t

跳格符打印成^I而换页符打印成^L。

如果未指定-v选项,会忽略-e和-t选项。

- 允许cat命令的标准输入。

2.2.2 管道与重定向

从标准输入读入,从标准输出输出,将异常及错误报告到标准错误。这是遵循UNIX哲学的软件正确行为。但是,我们总不能将所有的输入和输出都集中在命令行黑乎乎的字符界面,我们还要读写文件、看视频、听音乐、还要打游戏,这些人机交互的输入和输出都来自不同设备或文件。

因此,Shell提供了数种语法和标记,用以改变默认输入端和输出端。在此处,我们暂时介绍基本使用方法,在后面提供高级应用。

1.以>改变标准输出

Command >file将command的标准输出重定向到文件中,而不是打印在控制台上。

alloy@ubuntu:~/Linux Shell/ch2$ echo“redirect to file.” > /tmp/a.txt

alloy@ubuntu:~/Linux Shell/ch2$ cat /tmp/a.txt

redirect to file.

alloy@ubuntu:~/Linux Shell/ch2$

2.以<改变标准输入

Command <file将command的标准输入修改为file。

alloy@ubuntu:~/Linux Shell/ch2$ cat < /tmp/a.txt > /tmp/b.txt

alloy@ubuntu:~/Linux Shell/ch2$

这条命令将会复制/tmp/a.txt文件到/tmp/b.txt,输入重定向符号改变cat命令从文件a.txt读入,输出到b.txt文件。a.txt文件不会有任何变化。

如果/tmp目录下没有b.txt文件,则输出重定向命令会新建一个,如果/tmp目录下已经存在b.txt文件,则文件会被覆盖,原来的数据丢失。

3.以>>追加文件

Command >> file可将command的输出追加到文件file末尾。

for line in /etc/passwd

do

echo $line >> /tmp/b.txt

done

这条命令依次读取/etc/passwd 文件的每一行,追加到/tmp/b.txt 末尾。效果就相当于复制/etc/passwd全部内容到/tmp/b.txt末尾一样!

在追加操作中,如果b.txt不存在,则系统会创建一个新的文件,如果存在,不会覆盖原有数据。

关于for循环的介绍,请关注第3章“编程的基本元素”。

4.以|建立管道

Command1 | command2将command1的标准输出与command2的标准输入相连。

alloy@ubuntu:~/Linux Shell/ch2$ head–n10 /etc/passwd | grep "prince"

alloy@ubuntu:~/Linux Shell/ch2$

这条命令读取/etc/passwd文件中的前10行,将读取的内容从输出端(管道)输出到grep命令的输入端,grep读取内容后,在其中检索包含文本“prince”的行。

从理论上讲,管道的功效完全可以用<和>实现。通过创建临时文件,而输入和输出重定向可以分别读取临时文件和写入临时文件。但是管道相较这种做法更高效,它可以直接连接程序的输入、输出,并且没有程序使用个数限制,只要你尚未获得最终处理结果,都可以在命令后继续添加管道。

在使用管道时,你可以想象两根水管拼接起来的样子,而数据就是水管中流动的水。一个程序处理的结果数据(水)通过输出端的水管流入另一个程序输入端的水管。这样做使得我们可以任意拼接程序,来完成更强大的功能。命令行高手对管道和重定向的使用都出神入化!

而重定向,你可以将它想象成一个漏斗。数据(水)从漏斗大的一端流入,而从小的一端流出。

NOTE:

管道的数据共享在Linux内核中是通过内存复制实现的。相较于CPU的运算,数据的移动往往更消耗时间。因此,在设计管道时,尽量把能够减少数据量的操作置于管道的前端。这样一来数据复制快速,二来程序运算量减少。

例如,在sort之前,使用grep找出相关数据,可以减少许多sort的运算量。

下面展示head的manpage。

head

语法:

head [ - Count | -c Count |-n Number ] [ File ... ]

描述:

显示一个文件或多个文件的前几行或前几个字节。

主要参数:

-Count

从每一要显示的指定文件的开头指定行数。Count变量必须是一个正的十进制整数。此标志等价于-n Number标志,但如果考虑到便携性,就不应该使用。

-c End

指定要显示的字节数。Number变量必须是一个正的十进制整数。

-n Number

指定从每一要显示的指定文件的开头的行数。Number 变量必须是一个正的十进制整数。此标志等价于- Count标志。

行为模式:

head命令把每一指定文件或标准输入的指定数量的行或字节写入标准输出。如果不为head命令指定任何标志, 默认显示前10行。File参数指定了输入文件名。输入文件必须是文本文件。当指定多个文件时每一文件的开始应与下列一致。

显示一组短文件并对其进行识别,请输入:

example% head-9999 filename1 filename2...

2.2.3 文件描述符

内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。

理解文件描述符、系统文件表和内存索引节点表3个概念至关重要。

文件描述符表 用户区的一部分,除非通过使用文件描述符的函数,否则程序无法对其进行访问。对进程中每个打开的文件,文件描述符表都包含一个条目。

系统文件表 为系统中所有的进程共享。对每个活动的open, 它都包含一个条目。每个系统文件表的条目都包含文件偏移量、访问模式(读、写、或读-写)以及指向它的文件描述符表的条目计数。

每个进程的文件表在系统文件表中的区域都不重合。理由是,这种安排使每个进程都有它自己的对该文件的当前偏移量。

内存索引节点表 对系统中的每个活动的文件(被某个进程打开了),内存中索引节点表都包含一个条目。几个系统文件表条目可能对应于同一个内存索引节点表(不同进程打开同一个文件)。

每个进程维护自己的文件描述表。当进程调用文件描述符相关的函数或命令时,会对其进行修改操作;文件描述表中的每一项指向系统文件表;系统文件表被所有进程共享,处于内核区,它与内存中的索引节点表对应。这样,进程通过对文件描述表的操作,访问被内存中的索引节点表控制的文件。

习惯上,标准输入(Standard Input)的文件描述符是0,标准输出(Standard Output)是1,标准错误(Standard Error)是2。尽管这种习惯并非UNIX内核的特性,但是因为一些Shell和很多应用程序都使用这种习惯,因此,如果内核不遵循这种习惯的话,很多应用程序将不能使用。这也是当我们重定向标准错误时,使用(2>)的原因。

POSIX定义了STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO来代替0、1、2。这3个符号常量的定义位于头文件unistd.h。

文件描述符的有效范围是0到OPEN_MAX。一般来说,每个进程最多可以打开64个文件(0~63)。对于Free BSD 5.2.1、Mac OS X 10.3和Solaris 9来说,每个进程最多可以打开文件的多少取决于系统内存的大小、int的大小以及系统管理员设定的限制。Linux 2.4.22强制规定最多不能超过1 048 576。

文件描述符是由无符号整数表示的句柄,进程使用它来标识打开的文件。文件描述符与包括相关信息(如文件的打开模式、文件的位置类型、文件的初始类型等)的文件对象相关联,这些信息被称作文件的上下文。

进程获取文件描述符最常见的方法是通过本机子例程open或create获取或者从父进程继承。后一种方法允许子进程同样能够访问由父进程使用的文件。文件描述符对于每个进程一般是唯一的。当用fork子例程创建某个子进程时,该子进程会获得其父进程所有文件描述符的副本,这些文件描述符在执行fork时打开。在由fcntl、dup和dup2子例程复制或复制某个进程时,会发生同样的复制过程。

2.2.4 特殊文件的妙用

Linux系统中有些神奇的文件。例如,/dev/null、/dev/zero还有/dev/tty。对UNIX/Linux文件系统比较熟悉的朋友对/dev目录一定不会陌生,它是系统中所有设备文件存放的地方。那么,null和zero是什么设备文件呢?

1./dev/null

我们可以把/dev/null想象为一个“黑洞”。它类似于一个只写文件。所有写入它的内容都不可读取。但是,对于命令行和脚本来说,/dev/null却非常有用。

我们来看两个实例吧。

例2.5 /dev/null的应用

1 #! /bin/sh

2 # 这个脚本演示/dev/null的应用

3 # 读取/tmp/b.txt文件,但是将读取的内容输出到/dev/null

4 cat /tmp/b.txt >/dev/null

5 # 检索/etc下所有包含alloy字符串的文件行,但是如果有错误信息,则输出到/dev/null

6 grep "alloy" /etc/* 2> /dev/null

7 # 下面的命令不会产生任何输出

8 # 如果b.txt文件存在,则读取的内容输出到/dev/null

9 # 如果b.txt文件不存在,则错误的信息输出到/dev/null

10 cat /tmp/b.txt >/dev/null 2>/dev/null

11 # 这个命令和上一条命令是等效的

12 cat /tmp/b.txt &>/dev/null

13 # 清空messages和wtmp文件中的内容,但是让文件依然存在并且不改变权限

14 cat /dev/null > /var/log/messages

15 cat /dev/null > /var/log/wtmp

此脚本只作为演示文件。

脚本存在的问题。

随便举了一些文件例子,可定制性的脚本,至少应该传入文件检索的参数。

如果要把输出写到/dev/null,干嘛还要用cat读取。

脚本中出现的一些新的知识。

所有写入/dev/null的信息都消失了。而如果将标准输出和标准错误重定向到/dev/null,你就能让终端闭嘴。

如果是重定向标准输出,直接使用>就可以了,或者也可以用(1>)表示,而如果是重新向标准错误,则用 2>。如果是标准输入呢?那就要用(0<)表示。而(&>)则代表标准输出和标准错误。

如果从/dev/null读取信息,你什么也读不到。但是可以用这个性质在保持文件权限不变的情况下清空文件内容。

来看看/dev/null文件的一些妙用吧!

例2.6 delete_cookie.sh[3]

1 #! /bin/sh

2 # 演示/dev/null文件的妙用

3 # 自动删除cookie,并且禁止以后网站再写入cookie

4

5 # 自动清空日志文件的内容(特别适用于处理那些由商业站点发送的, 令人厌恶的"cookie")

6 if [-f ~/.mozilla/cookies ] # 如果存在, 就删除

7 then

8 rm-f ~/.mozilla/cookies

9 fi

10

11 # 以后所有的cookie都被自动扔到黑洞里去, 这样就不会保存在我们的磁盘中了

12 ln-s /dev/null ~/.mozilla/cookies

这个应用很神奇,不是吗?另外,此应用还需要注意以下几点。

你系统中的cookie文件不一定存放在~/.mozilla目录中,需要自己寻找。

记得曾经做过的操作,否则,当发现无法记录的cookie时,很难找到原因。

把delete_cookie.sh文件移动到$HOME/bin目录下吧,记得加上可执行权限。

2./dev/zero

我们看过了/dev/null文件,那么,/dev/zero文件有什么用呢?类似于/dev/null,/dev/zero也是一个伪文件,但事实上它会产生一个null流(二进制的0流,而不是ASCII类型)。如果你想把其他命令的输出写入/dev/zero文件的话,那么写入的内容会消失,而且如果你想从/dev/zero文件中读取一连串null的话,也非常的困难,虽然可以使用od或者一个16进制编辑器来达到这个目的。/dev/zero文件的主要用途就是用来创建一个指定长度,并且初始化为空的文件,这种文件一般都用作临时交换文件。

例2.7演示了如何使用/dev/zero来建立一个交换文件。

例2.7 用/dev/zero创建交换文件

1 #!/bin/bash

2 # 创建一个交换文件

3

4 ROOT_UID=# Root用户的$UID为0

5 E_WRONG_USER=# 不是root

6

7 FILE=/swap

8 BLOCKSIZE=1024

9 MINBLOCKS=40

10 SUCCESS=0

11

12 # 这个脚本必须获得root权限才能运行,使用sudo命令

13 if [ "$UID"-ne "$ROOT_UID" ]

14 then

15 echo; echo "You must be root to run this script."; echo

16 exit $E_WRONG_USER

17 fi

18

19 blocks=${1:-$MINBLOCKS} # 如果没在命令行上指定

20 #+ 默认设置为40块

21 # 上边这句等价于下面这个命令块

22 #--------------------------------------------------

23 # if [-n "$1" ]

24 # then

25 # blocks=$1

26 # else

27 # blocks=$MINBLOCKS

28 # fi

29 #--------------------------------------------------

30

31 if [ "$blocks"-lt $MINBLOCKS ]

32 then

33 blocks=$MINBLOCKS # 至少要有40块

34 fi

35

36 echo "Creating swap file of size $blocks blocks (KB)."

37 dd if=/dev/zero of=$FILE bs=$BLOCKSIZE count=$blocks # 用零填充文件38

39 mkswap $FILE $blocks # 将其指定为交换文件(译者注:或称为交换分区)

40 swapon $FILE # 激活交换文件

41

42 echo "Swap file created and activated."

43

44 exit $SUCCESS

/dev/zero文件还有其他的应用场合,例如,当你出于特殊目的,需要“用0填充”一个指定大小的文件时,就可以使用它。

3./dev/tty

/dev/tty是一个很实用的文件。当程序打开这个文件时,UNIX/Linux会自动将它重定向到当前所处的终端。输出到此的信息只会显示在当前工作的终端显示器上。在某些时候例如,设定了脚本输出到/dev/null时,而你又想在当前终端上显示一些很重要的信息,你就可以调用这个设备,写入重要信息。这样做可以强制信息显示到终端(像不像流氓做法)。

/dev/tty文件作为输入端时,也非常有用,见例2.8。

例2.8 /dev/tty的使用

printf“Enter new passwd:” #提示输入

stty–echo #关闭自动打印输入字符的功能

read pass < /dev/tty #读取密码

printf“Enter again”

read pass2< /dev/tty #再读一次,以便确认

stty echo #记得重新打开自动打印输入字符功能

例2.8中两次从终端读入密钥用于比对。关于stty的用法,请参见stty(1)的manpage。

下面展示read的manpage。

read

语法:

read [-p ][ -r ][-s ][-u[ n ] ] [ Variable Name?Prompt ]

[ Variable Name ... ]

描述:

从标准输入中读取一行。

主要参数:

-p

用|&(管道,& 的记号名称)读取由Korn Shell运行进程的输出作为输入。

注:-p标志的文件结束符引起该进程的清除,因此将产生另外一个进程。

-r

指定读取命令把一个\(反斜杠)处理为输入行的一部分,而不把它作为一个控制字符。

-s

把输入作为一个命令保存在Korn Shell的历史记录文件中。

-u [ n ]

读取一位数的文件描述符号码n作为输入。文件描述符可以用ksh exec内置命令打开。n的默认值是0,表示的是键盘。数值2表示标准错误。

Variable Name?Prompt

指定一个变量的名称和一个要使用的提示符。当Korn Shell是交互式时,它将把提示符写到标准错误,并执行输入。当Prompt中包含多字时,必须用单引号或双引号引起来。

Variable Name...

指定一个或多个由空格分隔的变量名。

行为模式:

read命令从标准输入中读取一行,并把输入行的每个字段的值指定给Shell变量,用IFS (内部字段分隔符)变量中的字符作为分隔符。Variable Name参数指定Shell变量的名称,Shell变量获取输入行一个字段的值。由Variable Name参数指定的第一个Shell变量指定给每一个字段的值,由Variable Name参数指定的第二个Shell变量指定给第二个字段的值,以此类推,直到最后一个字段。如果标准输入行的字段比相应的由Variable Name参数指定的Shell变量的字段个数多,把全部余下的字段的值赋给指定的最后的Shell变量。如果比Shell变量的个数少,则剩余的Shell变量被设置为空字符串。

警告:

如果省略了Variable Name参数,变量REPLY用作默认变量名。

由read命令设置的Shell变量影响当前Shell执行环境。

2.3 基本文本检索

Linux设计中有这样一种关键思想:“一切皆文件”。Linux文件系统中有各式各样的文本文件,他们既便于阅读,又便于修改。特别是在/etc 目录下,是系统配置文件的集中点。在这样的情况下,Linux系统的文本处理就变得至关重要。本书有将近一半的内容是关于文本处理的。

在本节中,我们将讲解最简单的文本检索,grep的用法。

grep命令检索文本

在前面的章节中,我们多次用到grep命令。例如:

alloy@ubuntu:~/Linux Shell/ch2$ ps–e Lf | grep firefox

grep命令提供了在文本中检索特定字符串的方法。此命令的强大之处在于grep命令支持正则表达式。

grep命令常常与管道连用,用于在文本流中过滤出符合条件的文本行。

历史上出现的grep程序有3种:grep、egrep和fgrep。

grep 最早的文本匹配程序。支持 POSIX 定义的基本正则表达式(Basic Regular Expression,简称写为BRE)。

egrep egrep和fgrep的命令只跟grep有很小的差别。egrep是grep的扩展,支持更多的re元字符。

fgrep fgrep就是fixed grep或fast grep,它们把所有的字母都看作单词,也就是说,正则表达式中的元字符表示回其自身的字面意义,不再特殊。

Linux使用GNU版本的grep。它的功能更强大,可以通过-G、-E、-F命令行选项来使用egrep和fgrep的功能。

grep的工作方式是在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到屏幕,不影响原文件内容。

grep可用于Shell脚本,因为grep通过返回一个状态值来说明搜索的状态。如果模板搜索成功,则返回0;如果搜索不成功,则返回1;如果搜索的文件不存在,则返回2。我们利用这些返回值可进行一些自动化的文本处理工作。

grep最强大的地方在于对正则表达式的支持。正则表达式将在第4章中详细讲解。

下面来看一个简单的grep实例。

例2.9 grep实例

alloy@ubuntu:~/Linux Shell/ch2$ cat /etc/passwd

root:x:0:0:root:/root:/bin/bash

daemon:x:1:1:daemon:/usr/sbin:/bin/sh

bin:x:2:2:bin:/bin:/bin/sh

sys:x:3:3:sys:/dev:/bin/sh

sync:x:4:65534:sync:/bin:/bin/sync

games:x:5:60:games:/usr/games:/bin/sh

man:x:6:12:man:/var/cache/man:/bin/sh

lp:x:7:7:lp:/var/spool/lpd:/bin/sh

mail:x:8:8:mail:/var/mail:/bin/sh

news:x:9:9:news:/var/spool/news:/bin/sh

uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh

proxy:x:13:13:proxy:/bin:/bin/sh

www-data:x:33:33:www-data:/var/www:/bin/sh

backup:x:34:34:backup:/var/backups:/bin/sh

list:x:38:38:Mailing List Manager:/var/list:/bin/sh

irc:x:39:39:ircd:/var/run/ircd:/bin/sh

gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh

nobody:x:65534:65534:nobody:/nonexistent:/bin/sh

libuuid:x:100:101::/var/lib/libuuid:/bin/sh

syslog:x:101:103::/home/syslog:/bin/false

messagebus:x:102:105::/var/run/dbus:/bin/false

colord:x:103:108:colord colour management daemon,,,:/var/lib/colord:/bin/false

lightdm:x:104:111:Light Display Manager:/var/lib/lightdm:/bin/false

whoopsie:x:105:114::/nonexistent:/bin/false

avahi-autoipd:x:106:117:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false

avahi:x:107:118:Avahi m DNS daemon,,,:/var/run/avahi-daemon:/bin/false

usbmux:x:108:46:usbmux daemon,,,:/home/usbmux:/bin/false

kernoops:x:109:65534:Kernel Oops Tracking Daemon,,,:/:/bin/false

pulse:x:110:119:Pulse Audio daemon,,,:/var/run/pulse:/bin/false

rtkit:x:111:122:Realtime Kit,,,:/proc:/bin/false

speech-dispatcher:x:112:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh

hplip:x:113:7:HPLIP system user,,,:/var/run/hplip:/bin/false

saned:x:114:123::/home/saned:/bin/false

alloy:x:1000:1000:alloy,,,:/home/alloy:/bin/bash

sshd:x:115:65534::/var/run/sshd:/usr/sbin/nologin

alloy@ubuntu:~/Linux Shell/ch2$ cat /etc/passwd | grep bash

#看看那些用户的登录Shell是bash

root:x:0:0:root:/root:/bin/bash

alloy:x:1000:1000:alloy,,,:/home/alloy:/bin/bash

alloy@ubuntu:~/Linux Shell/ch2$

在例2.9中,grep从/etc/passwd文件中读取所有行,检索出行内含有bash字符串的行。在这里,是以固定字符串bash进行查找。

下面是grep命令的manpage。

grep

语法:

grep [-E |-F ] [-i ] [-h ] [-s ] [-v ] [-w ] [-x ] [-y ] [ [ [-b ] [-n ] ] | [-c |-l|-q ] ] [-p [ Separator ] ] { [-e Pattern List ... ] [-f Pattern File ... ] | Pattern List ... }[文件... ]

描述:

搜索文件中的模式。

主要参数:

-b 在每行之前添加找到该行时所在的块编号。使用这个参数有助于通过上下文来找到磁盘块号码。-b参数不能用于来自标准输入和管道的输入的命令。

-c 仅显示匹配行的计数。

-E 将每个指定模式视作扩展正则表达式(ERE)。ERE的空值将匹配所有的行。

注:带有-E参数的grep命令等价于egrep命令,只不过它们的错误和使用信息不同以及-s参数的作用不同。

-e Pattern List 指定一个或多个搜索模式。其作用相当于一个简单模式,但在模式以−(减号)开始的情况下,这将非常有用。模式之间应该用换行符分隔。连续使用两个换行符或者在引号后加上换行符("\n)可以指定空模式。除非同时指定了-E或-F参数,否则每个模式都将被视作基本正则表达式(BRE)。grep可接受多个-E和-F参数。在匹配行时,所有指定的模式都将被使用,但评估的顺序没有指定。

-F 将每个指定的模式视作字符串而不是正则表达式。空字符串可匹配所有的行。

注:带有-F参数的grep命令等价于fgrep命令,只不过它们的错误和使用信息不同以及-s参数具有不同的作用。

-f Pattern File 指定包含搜索模式的文件。模式之间应该用换行符加以分隔,空行将被认为是空模式。每种模式都将被视作基本正则表达式(BRE),除非同时指定了-E或-F参数。

-h 禁止在匹配行后附加包含此行的文件的名称。当指定多个文件时,将禁止文件名。

-i 在进行比较时忽略字母的大小写。

-L 仅列出(一次)包含匹配行的文件的名称。文件名之间用换行符加以分隔。如果搜索到标准输入,将返回标准输入的路径名。-L参数同-c和-n参数任意一个组合一起使用时,其作用类似于仅使用了-L参数。

-n 在每一行之前放置文件中相关的行号。每个文件的起始行号为1,在处理每个文件时,行计数器都将被复位。

-p[ Separator] 显示包含匹配行的整个段落。段落之间将按照Separator参数指定的段落分隔符加以分隔,这些分隔符是与搜索模式有着相同格式的模式。包含段落分隔符的行将仅用作分隔符,它们不会被包含在输出中。默认段落分隔符是空白行。

-q 禁止所有写入到标准输出的操作,不管是否为匹配行。如果选择了输入行,则以零状态退出。-q 参数同 -c、-l 和-n 参数中的任意一个组合一起使用时,其作用类似于仅使用了-q参数。

-s 禁止通常因为文件不存在或不可读取而写入的错误信息。其他的错误信息并未被禁止。

-v 显示所有与指定模式不匹配的行。

-w 执行单词搜索。

-x 显示与指定模式精确匹配而不含其他字符的行。

-y 当进行比较时忽略字符的大小写。

Pattern List 指定将在搜索中使用的一个或多个模式。这些模式将被视作如同是使用-e 参数指定的。

File 指定将对其进行模式搜索的文件的名称。如果未给出File变量,将使用标准输入。

行为模式:

grep命令用于搜索由Pattern参数指定的模式,并将每个匹配的行写入标准输出中。这些模式是具有限定的正则表达式,它们使用ed或egrep命令样式。grep命令使用压缩的不确定算法。

如果在File参数中指定了多个名称,grep命令将显示包含匹配行的文件的名称。对Shell有特殊含义的字符($, *, [, |, ^, (, ), \)出现在Pattern参数中时必须带双引号。如果Pattern参数不是简单字符串,通常必须用单引号将整个模式括起来。在如[a-z], 之类的表达式中,−(减号) cml可根据当前正在整理的序列来指定一个范围。整理序列可以定义等价的类以供在字符范围中使用。如果未指定任何文件,grep会假定为标准输入。

警告:

行被限制为2 048个字节。

段落(使用-p参数时)长度当前被限制为5 000个字符。

请不要对特殊文件运行grep命令,这样做可能产生不可预计的结果。

输入行不应包含空字符。

输入文件应该以换行符作为结束。

正则表达式不会对换行符进行匹配。

虽然一些参数可以同时被指定,但其中的某些参数会覆盖其他参数。例如,-l选项将优先于其他参数。另外,如果同时指定了-E和-F参数,则后指定的那个会有优先权。

2.4 UNIX/Linux系统的设计与Shell编程

这一节,我们要涉及一些与操作系统相关的东西。虽然在PC市场上,Windows还是一家独大,但是在服务器市场,Linux/UNIX占有绝对优势。是什么样的设计系统带来了这样的优势呢?它的优势对Shell编程有什么帮助?本节,我们将讲述UNIX系统的精髓,或者说UNIX哲学。

2.4.1 一切皆文件

你一定注意到了,我们在前面的章节中多次提到Linux系统设计的一个设计思想:一切皆文件。

UNIX/Linux认为,系统和所有硬件设备的交互都应当如同文件操作一般简单易行。例如,我们可以和/dev/tty交互操作终端,和/dev/sda交互操作硬盘,和/dev/sound交互操作音响。没有什么不是文件。

UNIX/Linux 还认为,文件是不应该和应用程序绑定的。没有人规定 doc 文件就应该被Microsoft Word打开,也没有人规定notepad软件只能读取txt文件。事实上,你可以用cat命令读取任何文件(如果有意义的话)。例如:

alloy@ubuntu:~/Linux Shell/ch2$ cat /dev/sound > /tmp/record_sound.wav #开始录音

^D #“Ctrl-D”组合键结束录音

alloy@ubuntu:~/Linux Shell/ch2$ mplayer /tmp/record_sound.wav #播放录音

而doc文件也能被用于和管道交互:

alloy@ubuntu:~/Linux Shell/ch2$ antiword file.doc | grep Linux #检索doc文件中包含字符串“Linux”的行

antiword软件读取doc文件的内容,需要另外安装。

在本小节里,我们将讲解与Linux文件相关的一些知识:Linux文件后缀名的规范,Linux下的5种文件类型等。

1.Linux文件的后缀名

UNIX/Linux 系统中文件的概念和 Windows 有很大不同。一谈到文件类型,大家就能想到Windows的文件类型,例如,file.txt、file.doc、file.sys、file.mp3、file.exe等,根据文件的后缀就能判断该文件的类型。但在Linux中一个文件是否能被执行,和后缀名没有太大的关系,主要与文件的属性有关。但了解Linux文件的后缀名还是有必要的,特别是当我们创建一些文件时,最好还是加后缀名,这样做的目的仅仅是为了我们在应用时方便。

现在的Linux桌面环境和Windows一样智能化,文件的类型是和相应的程序关联的。在我们打开某个文件时,系统会自动判断用哪个应用程序打开。如果从这方面来说,Linux 桌面环境和Windows桌面没有太大的区别。

在Linux中,带有扩展名的文件,只能代表程序的关联,并不能说明文件是可执行的,由此可以看出,Linux的扩展名没有太大的意义。

file.tar.gz file.tgz file.tar.bz2 file.rar file.gz file.zip ...

这些大家都熟悉,是归档文件。要通过相应的工具来解压或提取。

file.php 这类文件能用php语言解释器进行解释,能用浏览器打开的文件。

file.so 这类是库文件。

file.doc file.obt 这是Open Office能打开的文件。

... ...

用不同工具创建的文件,其后缀也不相同。例如,Gimp、gedit、Open Office等工具,创建出来的文件后缀名是不同的。

2.Linux文件类型

Linux文件类型和Linux文件的文件名所代表的意义是两个不同的概念。我们通过一般应用程序创建如file.txt、file.tar.gz等,虽然要用不同的程序来打开,但放在Linux文件类型中衡量的话,大多是常规文件(也称为普通文件)。

Linux文件类型常见的有:普通文件、目录、字符设备文件、块设备文件、符号链接文件等。接下来我们这些文件类型进行一个简要的说明。

(1)普通文件

alloy@ubuntu:~/Linux Shell/ch2$ ls-lh install.log

-rw-r--r-- 1 root root 53K 5月14 11:39 install.log

我们用ls-lh来查看某个文件的属性,可以看到有类似-rw-r--r-- ,值得注意的,它的第一个符号是-,这样的文件在Linux中就是普通文件。这些文件一般是用一些相关的应用程序创建,例如图像工具、文档工具、归档工具或cp工具等。这类文件的删除方式是用rm命令。

(2)目录

alloy@ubuntu:~/Linux Shell/ch2$ ls–lh

总用量20K

-rwxrwxr-x 1 alloy alloy 452 5月14 16:38 delete_cookie.sh

-rwxrwxr-x 1 alloy alloy 743 5月14 16:36 null.sh

-rwxrwxr-x 1 alloy alloy 180 5月14 15:40 ps.sh

-rwxrwxr-x 1 alloy alloy 49 5月14 15:38 testfunc.sh

-rwxrwxr-x 1 alloy alloy 1.2K 5月14 16:49 zero.sh

alloy@ubuntu:~/Linux Shell/ch2$

当我们在某个目录下执行命令,看到有类似drwxr-xr-x命令时,这样的文件就是目录,目录在Linux是一个比较特殊的文件。注意,它的第一个字符是d。创建目录可以用mkdir命令或cp命令。cp可以把一个目录复制为另一个目录。删除目录用rm或rmdir命令。

(3)字符设备或块设备文件

如果进入/dev目录,列一下文件,会看到类似如下的格式:

alloy@ubuntu:~/Linux Shell/ch2$ ls-la /dev/tty

crw-rw-rw- 1 root tty 5, 0 5月14 16:47 /dev/tty

crw-rw-rw- 1 root tty 5, 0 04-19 08:29 /dev/tty

alloy@ubuntu:~/Linux Shell/ch2$ ls-la /dev/sda1

brw-rw---- 1 root disk 8, 1 5月14 11:39 /dev/sda1

我们看到/dev/tty的属性是crw-rw-rw-。注意,前面第一个字符是c,表示字符设备文件,如猫等串口设备。

我们看到/dev/sda1的属性是brw-r-----。注意,前面的第一个字符是b,表示块设备,如硬盘光驱等设备。

这种文件,是用mknode来创建,用rm来删除。目前,在最新的Linux发行版本中,一般不用自己来创建设备文件,因为这些文件是和内核是相关联的。

(4)套接口文件

当我们启动My SQL服务器时,会产生一个mysql.sock的文件。

alloy@ubuntu:~/Linux Shell/ch2$ ls-lh /var/lib/mysql/mysql.sock

srwxrwxrwx 1 mysql mysql 0 5月14 11:39 /var/lib/mysql/mysql.sock

注意,这个文件的属性的第一个字符是s。我们了解一下就行了。

(5)符号链接文件

alloy@ubuntu:~/Linux Shell/ch2$ ls-lh setup.log

lrwxrwxrwx 1 root root 11 5月14 11:39 setup.log-> install.log

当我们查看文件属性时,会看到有类似lrwxrwxrwx的命令。注意,第一个字符是l,这类文件是链接文件。是通过ln-s源文件产生新文件名 。上面的例子,表示setup.log是install.log的软链接文件。这和Windows操作系统中的快捷方式有点相似。

符号链接文件的创建方法举例:

alloy@ubuntu:~/Linux Shell/ch2$ ls-lh kernel-2.6.15-1.2025_FC5.i686.rpm

-rw-r--r-- 1 root root 14M 5月14 11:39 kernel-2.6.15-1.2025_FC5.i686.rpm

alloy@ubuntu:~/Linux Shell/ch2$ ln-s kernel-2.6.15-1.2025_FC5.i686.rpm kernel.rpm

alloy@ubuntu:~/Linux Shell/ch2$ ls-lh kernel*

-rw-r--r-- 1 root root 14M 5月14 11:39 kernel-2.6.15-1.2025_FC5.i686.rpm

lrwxrwxrwx 1 root root 33 5月14 11:39 kernel.rpm-> kernel-2.6.15-1.2025_FC5.i686.rpm

更多文件方面的知识,请参见第6章“文件和文件系统”。

下面是ls的manpage。

ls

语法:

显示目录或文件名的内容。

ls [-A ] [-C ] [-F ] [-a ] [-c ] [-d ] [-i ] [ File ... ]

显示目录内容:

ls-f [-C ] [-d ] [-i ] [-m ] [-s ] [-x ] [-1 ] [ Directory ... ]

描述:

显示目录内容。

行为模式:

ls 此命令将每个由Directory参数指定的目录或者每个由File参数指定的名称写到标准输出,以及您所要求的和参数一起的其他信息。如果不指定File或Directory参数,ls命令将显示当前目录的内容。

主要参数:

-A 列出所有条目,除了 .(点)和 ..(点-点)。

-a 列出目录中所有项,包括以 .(点)开始的项。

-c 使用索引节点中最近一次修改的时间,用以排序(当带-t 参数使用时)或者用以显示(当带-l参数使用时)。该参数必须和-t或-l参数或者两者一起使用。

-C 以多列纵向排序输出。当往终端输出时,此输出方式为默认方法。

-d 仅仅显示指定目录信息。目录和文件一样处理,这在当使用-l 参数获取目录状态时非常有用。

-F 如果文件是目录,在文件名后面放置一个/(斜杠),如果文件可执行,则放置一个*(星号),如果文件为套接字,则放置一个=(等号),如果为FIFO,则放置一个|(管道)符号,如果文件是符号链接,则放置一个 @。

-i 显示每个文件报告第一列中的索引节点数目。

如果文件是符号链接,打印所链接到的文件的路径名,其前放置->,显示符号链接的属性。-n、-g、和-o参数覆盖-l参数。

警告:

符号链接文件后跟一个箭头,然后是符号链接的内容。

2.4.2 UNIX编程的基本原则

我们常常思考,为什么UNIX来到世界将近40年(发明于1971年),硬件更新了一代又一代,各种天才程序员层出不穷,而UNIX上的经典工具和命令还是被广泛使用着,几十年没有更改,还是工作得很好?不仅如此,UNIX还在Linux的发展中获得了新生。而今,绝大多数的大型机和巨型机上运行着Linux系统,而天才的程序员们不断为UNIX/Linux添砖加瓦,来自世界各地的UNIX/Linux程序往往不经调谐就能在UNIX上很好地协作(通过管道),为什么?他们之间有什么合作协议吗?答案是:他们都是UNIX“教徒”!

UNIX奉行一些教条,所有UNIX/Linux的爱好者,在接触系统的同时,就开始受到发明这些系统和软件的巨人的影响。当他们步步成长,不断发明创造时,他们又成了 UNIX“宗教的传教士”,于是,UNIX的哲学在一代代程序员中得以传承。

这些思想,使UNIX的软件都有着同样的风格(一股UNIX的味道),软件之间可以很好地协作(即使他们从来没有想象过的协作方式),使软件都健壮易用。

这些思想和原则是什么呢?

(1)所有的UNIX哲学可以浓缩为一条铁律,那就是被各地编程大师们奉为圭臬的“K.I.S.S”原则。

UNIX哲学起源于Ken Thompson早期关于如何设计一个服务接口简洁、小巧精干的操作系统的思考,随着UNIX文化在学习如何尽可能发掘Thompson设计思想的过程中不断成长,同时一路上还从其他地方博采众长。

(2)UNIX管道的发明人、传统的奠基人之一Doug Mc Ilroy在[Mc Ilroy78]中曾经说过以下原则。

让每个程序就做好一件事。如果有新任务,就重新开始,不要往原程序中加入新功能而搞得复杂。

假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序还是未知的。

输出中不要有无关的信息干扰。避免使用严格的分栏格式和二进制格式输入。不要坚持使用交互式输入。

尽可能早地将设计和编译的软件投入试用, 哪怕是操作系统也不例外,理想情况下, 应该是在几星期内。对拙劣的代码别犹豫,扔掉重写。

优先使用工具而不是拙劣的帮助来减轻编程任务的负担。工欲善其事,必先利其器。

UNIX 哲学是:一个程序只做一件事,并做好;程序之间要能协作;程序要能处理文本流,因为这是最通用的接口。

(3)Rob Pike, 最伟大的C语言大师之一, 在Notes on C Programming一书中从另一个稍微不同的角度表述了UNIX的哲学。

原则 1 你无法断定程序会在什么地方耗费运行时间。问题经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是问题所在。

原则2 估量。在你没对代码进行估量,特别是没找到最耗时的那部分之前,别去优化速度。

原则3 花哨的算法在n很小时通常很慢,而n通常很小。花哨算法的常数复杂度很大。除非你确定n总是很大,否则不要用花哨算法(即使n很大,也优先考虑原则2)。

原则4 花哨的算法比简单算法更容易出bug,更难实现。尽量使用简单的算法配合简单的数据结构。

原则 5 数据压倒一切。如果已经选择了正确的数据结构并且把一切都组织得井井有条,正确的算法也就不言自明。编程的核心是数据结构,而不是算法。

原则6 没有原则6。

(4)Ken Thompson——UNIX最初版本的设计者和实现者,禅宗偈语般地对Pike的原则4作了强调:拿不准就穷举。

UNIX 哲学中更多的内容不是这些先哲们口头表述出来的,而是由他们所作的一切和 UNIX本身所作出的榜样体现出来的。从整体上来说,可以概括为以下几点。

模块原则 使用简洁的接口拼合简单的部件。

清晰原则 清晰胜于机巧。

组合原则 设计时考虑拼接组合。

分离原则 策略同机制分离,接口同引擎分离。

简洁原则 设计要简洁,复杂度能低则低。

吝啬原则 除非确无它法,不要编写庞大的程序。

透明性原则 设计要可见,以便审查和调试。

健壮原则 健壮源于透明与简洁。

表示原则 把知识叠入数据以求逻辑质朴而健壮。

通俗原则 接口设计避免标新立异。

缄默原则 如果一个程序没什么好说的,就沉默。

补救原则 出现异常时,马上退出并给出足够错误信息。

经济原则 宁花机器一分,不花程序员一秒。

生成原则 避免手工hack,尽量编写程序去生成程序。

优化原则 雕琢前先要有原型,跑之前先学会走。

多样原则 决不相信所谓“不二法门”的断言。

扩展原则 设计着眼未来,未来总比预想来得快。

如果从实际实现上来说,UNIX的设计主要有这几点魅力:接口的设计、策略与机制的分离、程序的简单与健壮性。

1.接口的设计

在接口的设计上,UNIX 程序员认为:如果程序彼此之间不能有效通信,那么软件就难免会陷入复杂的泥淖。

在输入输出方面,UNIX传统极力提倡采用简单、文本化、面向流、设备无关的格式。

在经典的UNIX下,多数程序都尽可能采用简单过滤器的形式,即将一个输入的简单文本流处理为一个简单的文本流输出。

抛开世俗眼光,UNIX 程序员偏爱这种做法并不是因为他们仇视图形用户界面,而是因为如果程序不采用简单的文本输入输出流,它们就极难衔接。

要想让程序具有组合性,就要使程序彼此独立。在文本流这一端的程序应该尽可能不要考虑文本流另一端的程序。将一端的程序替换为另一个截然不同的程序,而完全不惊扰另一端应该很容易做到。

图形用户界面(Graphical Users Interface,以下简称GUI)是个好东西。有时竭尽所能也不可避免复杂的二进制数据格式。但是,在做GUI前,最好还是想想可不可以把复杂的交互程序跟干粗活的算法程序分离开,每个部分单独成为一块,然后用一个简单的命令流或者是应用协议将其组合在一起。

程序之间的组合往往通过文本作为中间数据传输格式。在构思精巧的数据传输格式前,有必要实地考察一下,是否能利用简单的文本数据格式;以微小的格式解析的代价,换得可以使用通用工具来构造或解读数据流的好处是值得的。当程序无法自然地使用序列化、协议形式的接口时,正确的UNIX设计至少是把尽可能多的编程元素组织为一套定义良好的应用程序编程接口(API)。这样,至少你可以通过链接调用应用程序,或者可以根据不同任务的需求粘合使用不同的接口。

2.策略与机制

将策略同机制剥离,就有可能在探索新策略的时候不足以打破机制。另外,我们也可以更容易为机制写出较好的测试(因为策略太短命,不值得花太多精力在这上面)。

一种方法是将应用程序分成可以协作的前端和后端进程,通过套接字上层的专用应用协议进行通讯;前端实现策略,后端实现机制。比起仅用单个进程的整体实现方式来说,这种双端设计方式大大降低了程序整体复杂度和bug出现的概率,从而降低程序的寿命周期成本。

3.程序简单强壮

来自多方面的压力常常会让程序变得复杂(由此产生的代价更高,bug更多),其中一种压力就是来自技术上的虚荣心理。程序员们都很聪明,常常以能玩转复杂东西和耍弄抽象概念的能力为傲,这一点也无可厚非。但正因如此,他们常常会与同行比试,看看谁能够设计出最错综复杂的美妙程序。正如我们经常所见,他们的设计能力大大超出他们的实现和排错能力,结果便是设计出代价高昂的废品。

UNIX程序员还总结出一个“吝啬原则”。说:除非确无它法,不要编写庞大的程序。

“大”有两重含义:体积大,复杂程度高。程序大了,维护起来就困难。由于人们对花费了大量精力才做出来的东西难以割舍,结果导致在庞大的程序中把投资浪费在注定要失败或者并非最佳的方案上。大多数软件禁不起磕碰,毛病很多,就是因为过于复杂,很难通盘考虑。如果不能够正确理解一个程序的逻辑,就不能确信其是否正确,也就不能在出错的时候修复它。

让程序健壮的方法就是让程序的内部逻辑更易于理解。要做到这一点设计时主要有两个原则:透明化和简洁化。

软件的透明性就是指一眼就能够看出来是怎么回事。如果人们不需要绞尽脑汁就能够推断出所有可能的情况,那么这个程序就是简洁的。程序越简洁,越透明,也就越健壮。数据要比编程逻辑更容易驾驭。所以接下来,如果要在复杂数据和复杂代码中选择一个,那么选择前者。在设计过程中,我们应该主动将代码的复杂度转移到数据之中去。

最易用的程序就是用户需要学习新东西最少的程序——或者,换句话说,最易用的程序就是最切合用户已有知识的程序。

因此,接口设计应该避免毫无来由的标新立异和自作聪明。如果你编制一个计算器程序,‘+’应该永远表示加法。而设计接口的时候,尽量按照用户最可能熟悉的同样功能接口和相似应用程序来进行建模。

UNIX 中最古老最持久的设计原则之一就是:若程序没有什么特别之处可讲,就保持沉默。行为良好的程序员应该默默工作,决不唠唠叨叨,碍手碍脚。沉默是金。

其实,操作系统的先哲和大师们早就对UNIX的哲学有所阐述。例如,下面三位就一语中的地说明了UNIX的风格。

“错综复杂的美妙事物”听起来自相矛盾。UNIX程序员相互比的是谁能够做到“简洁而漂亮”并以此为荣,这一点虽然只是隐含在这些规则之中,但还是很值得公开提出来强调一下。

——Doug Mc Ilroy

我认为“简洁”是UNIX程序的核心风格。一旦程序的输出成为另一个程序的输入,就很容易把需要的数据挑出来。站在人的角度上来说——重要信息不应该混杂在冗长的程序内部行为信息中。如果显示的信息都是重要的,那就不用找了。

——Ken Arnold

我最有成效的一天就是扔掉了1 000行代码。

——Ken Thompson

2.5 小结

我们结束了第2章。本章重要的知识如下。

UNIX/Linux的参数传递是一个有用的机制。让Shell的使用者将外部参数传入到Shell脚本内部,通过位置参数变量访问。这种机制提高了Shell脚本编程的灵活性和可定制性,使脚本被真正广泛应用成为可能。

UNIX哲学:一个程序只做一件事,并做好;程序之间要能协作;程序要能处理文本流,因为这是最通用的接口。这种哲学规则奠定了管道在UNIX系统中的地位。我们通过管道拼接程序,协同UNIX下的程序为我们工作。我们还通过重定向改变标准输入,标准输出和标准错误。在这样的哲学领导下,UNIX/Linux 的软件大都简洁、健壮。符合“K.I.S.S”原则。

UNIX系统进程使用文件描述符来访问文件。对于进程来说,有3个文件描述符往往在进程创建初期就已经打开。他们分别是标准输入(0)、标准输出(1)和标准错误(2)。

UNIX/Linux下存在一些特殊的文件。例如,/dev下的null和zero文件。null文件相当于UNIX系统的一个黑洞,它非常接近于一个只写文件。所有写入null文件的内容都会永远丢失。而如果想从它那读取内容,则什么也读不到。而zero文件则可以读出一串二进制的0。tty文件与当前的控制终端直接相连。

grep命令用于文本检索。它读入文本,输出包含有特定字符串的文本行。支持正则表达式是它最强大的功能。grep命令常常与管道连用。

你是否被本章内容弄糊涂了?没关系,UNIX的哲学需要在不断实践中渐渐体会。休息一下,让我们进入第3章吧。

[1].参照本章2.2.2小节“管道和重定向”;2.3.1小节“grep命令检索文本”。

[2].管道的知识,请参见2.2.2小节“管道与重定向”。

[3].Shell编程的if条件判断语句,请参照第3章“编程的基本元素”。

相关图书

Linux常用命令自学手册
Linux常用命令自学手册
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
Linux后端开发工程实践
Linux后端开发工程实践
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程