讲两句

文章最后有公众号,求求了关注关注吧 ┭┮﹏┭┮,所有资料都去公众号中获取。

前言

无法想象没有Shell 的 Linux 会是什么样子。从一开始,Linux就是黑客们的玩具。在Linux的世界里,没有什么是不可控的。如果想要成为一名高级Linux用户,那么Shell编程是必须跨过的一道坎。本章将从正则表达式开始,逐步介绍Shell编程的基本知识。这些内容对于没有任何编程经验的读者可能有点困难,不过想一想将要接触到的激动人心的技术,请打起精神来!

Shell 教程 | 菜鸟教程:https://www.runoob.com/linux/linux-shell.html

文章目录

讲两句前言【正则表达式】1.什么是正则表达式?2.不同风格的正则表达式3.快速上手:在字典中查找单词4.字符集和单词5.字符类6.位置匹配7.字符转义8.重复9.子表达式10.反义11.分支12. 逆向引用

【Shell 脚本编程】第一个程序:Hello World变量和运算符1.变量的赋值和使用2.变量替换3.位置变量4.BASH 引号规则5.运算符

表达式求值脚本执行命令和控制语句1.if选择结构2.case多选结构

条件测试1.if判断的依据2.test命令和空格的使用

循环结构1.while语句2.until语句3.for语句

读取用户输入脚本执行命令1.exit命令2.trap命令

创建命令表其他有用的Shell编程工具1.cut命令2.diff3.sort命令4.uniq命令5.tr命令6.wc命令7.substr命令8.seq命令

【定制工具:安全的delete命令】

【正则表达式】

正则表达式广泛地应用在各种脚本编程语言中,包括Perl、PHP、Ruby等。Linux 的各种编程工具也大量采用了正则表达式。可以说,有字符串处理的地方,就有正则表达式的身影。本节简要介绍正则表达式的基本语法。在开始之前,首先关心一下正则表达式的“定义”。

1.什么是正则表达式?

正则表达式(regexps)这个词背后的历史似乎很难考证。一直以来,这个说法被人们广泛应用,没有人关心,或许也没有必要关心它是怎么来的。在很多时候,“正则表达式”又被称做“模式”,所以千万别被这两件不同的外衣搞糊涂了。至少在Linux 中,“模式”和“正则表达式”讲的是同一件事情。

那么究竟什么是正则表达式?简单地说,这是一组对正在查找的文本的描述。例如一个生活中的例子,老师对调皮捣蛋的学生说:“把单词表中 a开头、t结尾的单词抄写10遍交给我。”那么对于正在抄写单词的学生而言,“a开头、t结尾的单词”就是“对正在查找的文本的描述”。同样可以告诉Shell,“把当前目录下所有以e结尾的文件名列出来”,这是正则表达式擅长做的事情。

2.不同风格的正则表达式

正则表达式是一种概念。在此基础上,人们充分发挥想象力,发明了各种风格的正则表达式。在这个领域,至今没有什么标准可言,不同的软件和编程语言支持不同风格的表达式写法,也难怪刚刚接触正则表达式的用户会因此感到困惑。目前在GNU/Linux中有两套库可用于正则表达式编程:POSIX库和PCRE 库。前者是Linux自带的正则表达式库,后者是Perl 的正则表达式库。从功能上看,PCRE风格的正则表达式更强大一些,但也更难掌握一些。本文选择POSIX风格的正则表达式作为讨论对象,POSIX库不需要额外安装,直接使用即可。在工具方面,本文所有的示例都在egrep工具中测试通过。

egrep工具

Linux egrep命令用于在文件内查找指定的字符串。egrep执行效果与"grep-E"相似,使用的语法及参数可参照grep指令,与grep的不同点在于解读字符串的方法。egrep是用extended regular expression语法来解读的,而grep则用basic regular expression语法解读,extended regular expression比basic regular expression的表达更规范。

3.快速上手:在字典中查找单词

现在来考虑本节开始的那个例子。老师要求抄写单词表中“a开头、t结尾”的单词,学生现在很想知道他究竟要花多少时间在这个作业上。/usr/share/dict/words中包含了多达98568个单词,看起来无论老师所说的“单词表”是什么,Linux给出的估算只多不少。

egrep "^a.*t$" /usr/share/dict/words ##列出文件words中a开头t结尾的所有单词

一个个数过来是不可能的了,可以使用wc命令来统计这些单词的数量。

egrep "^a.*t$" words | wc -w

也就是说,这位学生最多要抄写1546个单词。看起来他还能抽空赶写一份检讨。

4.字符集和单词

在正则表达式中,句点.用于匹配除换行符之外的任意一个字符。下面这条正则表达式可以匹配诸如cat、sat、 bat这样的字符串。

.at

.能够匹配的字符范围是最大的。上面这个正则表达式还能够匹配#at、~at,at这样的字符串。很多时候,需要缩小选择范围使匹配更为精确。为了限定at前的那个字符只能是小写字母,可以这样写正则表达式。

[a-z]at

方括号[用于指定一个字符集。]无论中有多少东西,在实际工作时只能匹配其中的一个字符。下面这条表达式用于匹配 a或b或c,而不能匹配 ab、abc、bc 或者3个字母间其他任意的组合。

[abc]

使用连字符-描述一个范围。下面这条表达式匹配所有的英文字母。

[a-zA-z]

数字也可以用范围来指定。

[0-9]

有了字符集的概念,匹配特定的单词就灵活多了。但这里面还是有一点问题,使用下面这条命令查找单词表,看看结果是否和预想的一样。

egrep '[a-z]at' /usr/share/dict/words

可以看到,这条命令不仅会列出 cat、sat这样的单词,而且列出了zupamate、zumatic这样的词。因为这些单词中包含了mat、cat这些符合正则表达式[a-z]at的字符串,于是也被匹配了(尽管这可能不是用户的初衷)。

为了让[a-z]at 能够严格地匹配一个单词,需要为它加上一对分隔符\<和>。

\<[a-z]at\>

下面这条命令在单词表中查找所有符合模式\<[a-z]atl>的行。

egrep '\<[a-z]at\>' /usr/share/dict/words

奇怪的是像 bat's 这样的行也被匹配了。事实上,如果有a#$bat、bat!!!、!@#S#@!SR%@!bat#@!$%^这样的行,它们也同样会被匹配。这就涉及正则表达式中对 “单词” 的定义:

“单词” 指的是两侧由非单词字符分隔的字符串。非单词字符指的是字母、数字、下划线以外的任何字符。

仔细分析一下上面这些例子。第二十九行中 bat 分别由行首和行尾分隔,因此符合单词的定义,可以被匹配。a#$bat中的 bat分别由标点$和行尾分隔,符合单词的定义,可以被匹配;!@#$#@!SR%@!bat#@!$%^中的bat 分别由标点!和#分隔,同样符合单词的定义。而 Alcatraz中的cat分别被字母1和r分隔,就不符合匹配条件了。

5.字符类

除了字符集,POSIX风格的正则表达式还提供了预定义字符类来匹配某些特定的字符。例如,下面的命令列出文件中所有以大写字母开头,以小写t结尾的行。

egrep "^[[:upper:]]t$" /usr/share/dict/words

正则表达式[L:upper:]就是一个字符类,表示所有的大写字母,等价于[A-Z]。

POSIX正则表达式中的字符类

类匹配字符[[:alnum:]]文字,数字字符[[:alpha:]]字母字符[[:lower:]]小写字母[[:upper:]]大写字母[[:digit:]]小数[[:xdigit:]]十六进制数字[[:punct:]]标点符号[[:blank:]]制表符和空格[[:space:]]空格[[:cntrl:]]所有控制符[[:pring:]]所有可打印的字符[[:graph:]]除空格外所有可打印的字符

6.位置匹配

字符^和S分别用于匹配行首和行尾。下面这条正则表达式用于匹配所有以 a开头、t结尾、a和t之间包含一个小写字母的行。

^a[a-z]t$

A和$不必同时使用。下面这条表达式匹配所有以数字开头的行。

^[0-9]

可以想像,$这样的写法将匹配空行。而$^则是没有意义的,系统不会对这个表达式报错,但也不会输出任何东西。

7.字符转义

大家可能已经有了这样的疑问,既然句点.在正则表达式中表示 除换行符之外的任意一个字符,那么如何匹配句点.本身呢?这就需要用到转义字符\。\可以取消所有元字符的特殊含义。例如,\.匹配句点.,\[ 匹配左方括号[……而为了匹配\,就要用\\来指定。例如,下面这条正则表达式匹配www.google.cn。

www\.google\.cn

8.重复

用户有时候希望某个字符能够不止一次地出现。正则表达式中的星号*表示在它前面的模式应该重复0次或者多次。下面这条正则表达式用于匹配所有以a开头、以t结尾的行。

^a.*t$

简单地讲解一下这条表达式: ^a匹配以a开头的行。.匹配一个字符(除了换行符),*指定之前的那个字符(由.匹配)可以重复0次或多次;t$匹配以t结尾的行。

与此相类似的两个元字符是+和?。+指定重复1次或更多次;?指定重复0次或1次。

下面这条正则表达式匹配所有在单词hi后面隔了一个或几个字符后出现单词Jerry的行。

\.+\

使用花括号{ }可以明确指定模式重复的次数。例如,{3}表示重复3次,{3,}表示重复3次或者更多次,{5,12}则表示重复的次数不少于5次,不多于12次。下面这条正则表达式匹配所有不少于8位的数。

\<[1-9][0-9]{7,}\>

这条表达式之所以以[1-9]开始,是因为没有哪个超过7位的数是以0开头的。相应地,最高位后面的数字应该重复7次或更多次。

用于重复模式的元字符

元字符描述*重复0次或更多次+重复1次或更多次?重复0次或1次{n}重复n次{n,}重复n次或更多次{n,m}重复不少于n次,不多于m次

9.子表达式

子表达式也被称为分组,这不是什么新的概念。小学生都知道,为了计算1+3的和与4的乘积,必须用括号把1+3括起来。正则表达式也一样。

请看下面这个例子:

正则表达式(or){2,}匹配所有or重复2次或更多次的行。如果去掉or两边的括号,那么这条正则表达式匹配的将是字母o后面紧跟两个或更多个字母r的行。

egrep "or{2,}" /usr/share/dict/words

10.反义

很多时候用户想说的是“除了这个字符,其他什么都可以”,这就需要用到“反义”。下面这条正则表达式匹配除了字母y的任何字符。

[^y]

与此相似的是,下面这条正则表达式匹配除了字母a、e、i、o、u的所有字符。

[^aeiou]

注意:^在表示行首和反义时在位置上的区别。下面的例子匹配所有不以字母y开头的行。

^[^y]

11.分支

大家已经看到,正则表达式对用户提交的信息简单地执行 “与” 的组合。举例来说,下面的这条语句匹配所有以字母h开头,“并且” 以字母t结尾的行。

^ht$

那么,如何匹配以字母h开头,“或者” 以字母t结尾的行? 分支(以竖线|表示)就用来完成 “或” 的组合。下面这条正则表达式用于匹配以字母h开头,“或者” 以字母t结尾的行。

^h|t$

再看一个稍微复杂一些的例子。下面这一长串正则表达式可以匹配1~12月的英文写法,包括完整拼写和缩写形式。

Jan(uary| |\.) |Feb(uary| |\.)|Mar(ch| |\.)|Apr(il| |\.)|May( |\.)|Jun(e|\.)|Jul(y| |\.)|Aug(ust| |\.)|Sep(tember| |\.)|Oct(ober| |\.)|Nov(ember| |\.)|Dec(ember| |\.)

如果合在一起很难看清楚的话,下面以一月份为例分析这个正则表达式的写法。

Jan(uary| |\.)

紧跟着开头3个字母Jan的是一个子表达式(用括号限定),两个分支元字符|分隔了3个字符(串),分别是uary、空格、句点(注意,描述句点需要使用转义符号\)。这意味着January或者Jan或者Jan.这样的字符串都能够被匹配。5月份May是比较特殊的一个。由于May的完整写法和缩写形式是一样的,因此只使用一个分支字符匹配空格或者句点。不同的月份之间使用分支字符I来分隔。

12. 逆向引用

在子表达式(分组)中捕获的内容可以在正则表达式中的其他地方再次使用,用户可以使用反斜杠\加上子表达式的编号来指代该分组匹配到的内容。这样的说法看上去有点不知所措,不妨来看几个例子。

(\<.*\>).?( )*\1

上面这行正则表达式匹配所有在某个单词出现后,紧跟着0个或1个标点符号,以及任意个空格之后再次出现这个单词的行。例如,cart cart、long,long ago、ha!ha!……为了便于理解,下面对这个正则表达式断句做些解释。

(\<.*\>):匹配任意长度的单词。第 1 个子表达式。 .?:匹配 0 个或 1 个标点符号。由于在句点之前匹配的是单词,因此句点.在这里只能匹配标点。 ( )*:匹配 0 个或多个空格。第 2 个子表达式。 \1:指代第 1 个子表达式匹配到的模式。如果第 1 个子表达式匹配到单词 cart,那么这里也自动成为 cart。

当然,用户也可以使用\2、\3……来指代编号为2、3……的子表达式匹配到的模式。子表达式的编号规则是:从左至右,第 1 个出现的子表达式编号为1,第2个编号为2……依次类推。

【Shell 脚本编程】

博主的建议是,不必陷入编程工具优劣的争论。Vim、Emacs、gedit、kate或其他文本编辑器都是不错的编写Shell 脚本的工具,只要用的顺手就可以。如果找不到足够的理由学习Vim和Emacs,那么就先放在一边吧。工具永远只是工具,使用工具做出些什么才是真正重要和值得去关心的。

第一个程序:Hello World

这是最古老、最经典的入门程序,用于在屏幕上打印一行字符串Hello World!。借用这个程序,来看一看一个基本的 Shell程序的构成。

使用文本编辑器建立一个名为hello的文件。

vim hello

包含以下内容:

#! /bin/bash

#Display a line

echo "Hello World"

要执行这个Shell 脚本,首先应该要为它加上可执行权限。完成操作后,就可以运行脚本了。

chmod +x hello ##为脚本加上可执行权限

./hello ##执行脚本

下面逐行解释这个脚本程序。

#! /bin/bash

这一行告诉Shell,运行这个脚本时应该使用哪个Shell程序。本例中使用的是/bin/bash,也就是BASH。一般来说,Shell程序的第一行总是以“#!”开头,指定脚本的运行环境。尽管在当前环境就是BASH SHELL时可以省略这一行,但这并不是一个好习惯。

Display a line

以#号开头的行是注释,Shell 会直接忽略#号后面的所有内容。保持写注释的习惯无论对别人(在团队合作时)还是对自己(几个月后回来看这个程序)都是很有好处的。

和几乎所有编程语言一样,Shell脚本会忽略空行。用空行分割一个程序中不同的任务代码是一个良好的编程习惯。

echo "Hello"

echo命令把其参数传递给标准输出,在这里就是显示器。如果参数是一个字符串的话那么应该用双引号把它包含起来。echo命令最后会自动加上一个换行符。

变量和运算符

本节介绍变量和运算符的使用。变量是任何一种编程语言所必备的元素,运算符也是。通过将一些信息保存在变量中,可以留作以后使用。通过本节的学习,读者将学会如何操

1.变量的赋值和使用

修改我们的hello程序,将一个字符串赋给变量,并在最后将其输出。

# ! /bin/bash

#将一个字符串赋给变量 output

log="monday"

echo "The value of logfile is:"

#美元符号 ($) 用于变量替换

echo $log

下面是这个脚本程序的运行结果。

在Shell中使用变量不需要事先声明。使用等号=将一个变量右边的值赋给这个变量时,直接使用变量名就可以了(注意在这赋值变量时=左右两边没空格)。

log ="monday"

当需要存取变量时,就要使用一个字符来进行变量替换。在BASH中,美元符号$用于对一个变量进行解析。Shell在碰到带有$的变量时会自动将其替换为这个变量的值。例如上面这个脚本的最后一行,echo最终输出的是变量 log 中存放的值。需要指出的是,变量只在其所在的脚本中有效。在上面这个脚本退出后,变量 log 就失效了,此时在 Shell中试图查看log的值将什么也得不到。

export 让脚本可以影响其子Shell环境。下面这一段命令在子Shell中显示变量的值。

使用unset命令可以手动注销一个变量。这个命令的使用就像下面这样简单。

unset count

2.变量替换

前面已经提到,美元提示符$用于解析变量。如果希望输出这个符号,那么就应该使用转义字符),告诉Shell 忽略特殊字符的特殊含义。

注意这两个的区别

Shell提供了花括号{}来限定一个变量的开始和结束。在紧跟变量输出字母后缀时,就必须要使用这个功能。

3.位置变量

Shell脚本使用位置变量来保存参数。当脚本启动的时候,就必须知道传递给自己的参数是什么。考虑cp命令,这个命令接受两个参数,用于将一个文件复制到另一个地方。传递给脚本文件的参数分别存放在$符号带有数字的变量中。简单地说,第一个参数存放在$1,第二个参数存放在$2……依次类推。当存取的参数超过10个的时候,就要用花括号把这个数字括起来,例如${13}、${20}等。一个比较特殊的位置变量是$0,这个变量用来存放脚本自己的名字。有些时候,例如创建日志文件时这个变量非常有用。下面来看一个脚本,用于显示传递给它的参数。

#! /bin/bash

echo "\$0 = *$0*"

echo "\$1 = *$1*"

echo "\$2 = *$2*"

echo "\$3 = *$3*"

下面是这个程序的运行结果。注意:因为没有第3个参数,因此$3的值是空的。

除了以数字命名的位置变量,Shell 还提供了另外3个位置变量。

如下:

字符说明$*包含参数列表$@包含参数列表,同上$#包含参数的个数

下面这个脚本listfiles 显示文件的详细信息。尽管还没有学习过for命令,但这里可以先体验一下,这几乎是$@最常见的用法。

#! /bin/bash

#显示有多少文件需要列出

echo "$# file(s) to list"

#将参数列表中的值逐一赋给变量file

for file in $@

do

ls -l $file

done

for 语句每次从参数列表($@)中取出一个参数,放到变量file 中。脚本运行的结果如下:

4.BASH 引号规则

尽管还没有正式介绍引号的使用规则,但之前的脚本程序已经大量使用了引号(不过也只是双引号而已)。现在弥补这个空缺还来得及。在 Shell 脚本中可以使用的引号有如下3种。

双引号:阻止 Shell对大多数特殊字符(例如#)进行解释。但$、` 和 ” 仍然保持其特殊含义。单引号:阻止 Shell 对所有字符进行解释。倒引号:`,这个符号通常位于键盘上Esc键的下方。当用倒引号括起一个 Shell命令时,这个命令将会被执行,执行后的输出结果将作为这个表达式的值。倒引号中的特殊字符一般都被解释。

下面的脚本quote显示这3个引号的不同之处。

[root@localhost ~]# vim quote

#! /bin/bash

log=Saturday

#双引号会对其中的 “$” 字符进行解释

echo "Today is $log"

#单引号不会对特殊字符进行解释

echo 'Today is $log'

#倒引号会运行其中的命令,并把命令输出作为最终结果

echo "Today is 'date'"

以下是该脚本的运行结果。注意脚本的最后一行,双引号也会对 ` 做出解释。

5.运算符

运算符是类似于+、-这样的符号,用于告诉计算机执行怎样的运算。Shell定义了一套运算符,其中的大部分读者应该已经非常熟悉了。和数学中一样,这些运算符具有不同的优先级,优先级高的运算更早被执行。

Shell 中用到的运算符

运算符含义-,+单目负,单目正!,~逻辑非,按位取反*,/,%乘,除,取模+加,减<<,>>按位左移,按位右移<,>,<=,>=大于,小于 ,大于等于,小于等于==,!=等于,不等于&按位与^按位异或|按位或&&逻辑与||逻辑或=,+=,-=,*=,/=,%=,&=,^=,|=,<<=,>>=赋值,运算并赋值

这里无法对其中的每一个运算符做详细解释。Shell完全复制了C语言中的运算符及其优先级规则。在日常使用中,只需要使用其中的一部分就可以了,数学运算并不是Shell 的强项。运算符的优先级并不需要特别地记忆。如果使用的时候搞不清楚,只要简单地使用括号就可以了,就像小学里学习算术时一样。

(

7

+

8

)

/

(

6

3

)

(7 + 8) / (6 - 3)

(7+8)/(6−3)

值得注意的是,在Shell中表示相等时,==和=在大部分情况下不存在差异,大家将会在后文中逐渐熟悉如何进行表达式的判断。

表达式求值

之所以单独列出这一节,因为这是让很多初学者感到困惑的地方。Shell中进行表达式求值有和其他编程语言不同的地方。首先来看一个例子,这个例子可以“帮助”读者产生困惑。 为什么结果不是2?原因很简单,Shell脚本语言是一种“弱类型”的语言,它并不知道变量num中保存的是一个数值,因此在遇到num=$num+1这个命令时,Shell 只是简单地把Snum和“+1”连在一起作为新的值赋给变量 num(在这方面,其他脚本语言——例如PHP似乎表现得更“聪明”一些)。为了让 Shell得到“正确”的结果,可以试试下面这条命令。

num=$[$num+1]

$[]这种表示形式告诉 Shell应该对其中的表达式求值。如果上面这条命令不太容易能看清楚的话,那么不妨对比一下下面这两条命令的输出。

$[]的使用方式非常灵活,可以接受不同基数的数字(默认情况下使用十进制)。可以采用[base#]n来表示从二到三十六进制的任何一个n值,例如2#10就表示二进制数10(对应于十进制的2)。

下面的几个例子显示了如何在$[]中使用不同的基数求值。

expr命令也可以对表达式执行求值操作,这个命令允许使用的表达式更为复杂,也更为灵活,这里无法介绍 expr的高级用法。下面的例子是用expr计算1+2的值,注意expr会同时把结果输出。 注意:在1、+和2之间要有空格,否则expr会简单地将其当做字符串输出。另一种指导Shell进行表达式求值的方法是使用let命令。更准确地说,let命令用于计算整数表达式的值。下面这个例子显示了let命令的用法。

脚本执行命令和控制语句

本节将介绍Shell脚本中的执行命令以及控制语句。在正常情况下,Shell 按顺序执行每一条语句,直至碰到文件尾。但在多数情况下,需要根据情况选择相应的语句执行,或者对一段程序循环执行。这些都是通过控制语句实现的。

1.if选择结构

if命令判断条件是否成立,进而决定是否执行相关的语句。这也许是程序设计中使用频率最高的控制语句了。

最简单的if结构如下:

if test-commands

then

commands

fi

上面这段代码首先检查表达式test-commands是否为真。如果是,就执行commands所包含的命令——commands可以是一条,也可以是多条命令。如果test-commands为假,那么直接跳过这段if结构(以fi作为结束标志),继续执行后面的脚本。

下面这段程序提示用户输入口令。如果口令正确,就显示一条欢迎信息。

#! /bin/bash

echo "Enter password:"

read password

if ["$password" = "mypassswd"]

then

echo "Welcome!!"

fi

注意:这里用于条件测试的语句[ $password = “mypasswd” ],在[、Spassword、=、"mypasswd"和]之间必须存在空格。条件测试语句将在随后介绍,大家暂时只要能“看懂”就可以了。该脚本的运行效果如下:

if 结构的这种形式在很多时候显得太过“单薄”了,为了方便用户做出“如果……如果……否则……”这样的判断,if结构提供了下面这种形式。

if test-command-1

then

commands-1

elif test-command-2

then

commands-2

elif test-command-3

then

commands-3

...

else

commands

fi

上面这段代码依次判断test-command-1、test-command-2、test-command-3……如果上面这些条件都不满足,就执行else语句中的commands。注意这些条件都是“互斥”的。也就是说,Shell依次检查每一个条件,其中任何一个条件一旦匹配,就退出整个if结构。现在修改上面刚才的脚本,根据不同的口令显示不同的欢迎信息。

下面显示了这个脚本的运行结果。在输入fjn之后,Shell 发现 if语句的第一个条件成立,于是Shell就执行命令echo “Hello, 方加你”,然后跳出if语句块结束脚本,而不会继续去判断"Spassword" = "mike"这个条件。从这个意义上,if-elif-else语句和连续使用多个if语句是有本质区别的。

2.case多选结构

Shell中另一种控制结构是case语句。casc用于在一系列模式中匹配某个变量的值,这个结构的基本语法如下:

case word in

pattern-1)

commands-1

;;

pattern-2)

commands-2

;;

...

pattern-N)

commands-N

;;

esac

变量word逐一同从pattern-1到pattern-2的模式进行比较,当找到一个匹配的模式后,就执行紧跟在后面的命令commands(可以是多条命令)﹔如果没有找到匹配模式,case语句就什么也不做。命令;;只在case结构中出现,Shell一旦遇到这条命令就跳转到case结构的最后。也就是说,如果有多个模式都匹配变量word,那么Shell 只会执行第一条匹配模式所对应的命令。与此类似的是,C语言提供了break语句在switch结构中实现相同的功能,Shell只是继承了这种书写“习惯”。区别在于,程序员可以在C程序的switch结构中省略break语句(用于实现一种几乎不被使用的流程结构),而在Shell的case结构中省略;;则是不允许的。相比较if语句而言,case语句在诸如a=b这样判断上能够提供更简洁、可读性更好的代码结构。在Linux 的服务器启动脚本中,case结构用于判断用户究竟是要启动、停止还是重新启动服务器进程。下面是从 openSUSE 中截取的一段控制SSH 服务器的脚本(/etc/init.d/sshd) 。

在这个例子中,如果用户运行命令/etc/init.d/sshd start,那么Shell将执行下面这段命令:通过startproc启动SSH守护进程。

echo -n "Starting SSH daemon"

## Start daemon with startproc(8). If this fails

## the echo return value is set appropriate.

startproc -f -p $SSHD_PIDFILE $SSHD_BIN $SSHD_OPTS -o "PidFile=$SSHD_PIDFILE"

#Remember status and be verbose

rc_status -v

值得注意的是最后使用的*),星号(*)用于匹配所有的字符串。在上面的例子中,如果用户输入的参数不是start、stop或是restart 中的任何一个,那么这个参数将匹配*),脚本执行下面这行命令,提示用户正确的使用方法。

echo "Usage: $0 {start|stop|restart|}"

由于case语句是逐条检索匹配模式,因此*)所在的位置很重要。如果上面这段脚本将*)放在case 结构的开头,那么无论用户输入什么,脚本只会说Usage: $o{start/stopprestart}这一句话。

条件测试

几乎所有初学Shell编程的人都会对这部分内容感到由衷的困惑。Shell和其他编程语言在条件测试上的表现非常不同。读者在C/C++积累的经验甚至可能会帮倒忙。理解本节对顺利进行Shell编程至关重要,因此如果读者是第一次接触的话,请耐心地读完这冗长的一节。

1.if判断的依据

和大部分人的经验不同的是,if 语句本身并不执行任何判断。它实际上接受一个程序名作为参数,然后执行这个程序,并依据这个程序的返回值来判断是否执行相应的语句。如果程序的返回值是0,就表示“真”,if语句进入对应的语句块;所有非0的返回值都表示“假”,if语句跳过对应的语句块。下面的这段脚本 testif很好地显示了这一点。

#!/bin/bash

if ./testscript -1

then

echo "test exit -1"

fi

if ./testscript 0

then

echo "test exit 0"

fi

if ./testscript 1

then

echo "test exit 1"

fi

脚本的运行结果如下:

这段脚本依次测试返回值-1、0和1,最后只有返回值为0所对应的echo语句被执行了。脚本中调用的testscript 接受用户输入的参数,然后简单地把这个参数返回给其父进程。testscript脚本只有两行代码,其中的exit语句用于退出脚本并返回一个值。

#!/bin/bash

exit $@

现在大家应该能够大致了解if语句(包括后面将要介绍的 while、until等语句)的运行机制。也就是说,if语句事实上判断的是程序的返回值,返回值0表示真,非0值表示假。

2.test命令和空格的使用

既然if语句需要接受一个命令作为参数,那么像"$password" = "john"这样的表达式就不能直接放在 if语句的后面。为此需要额外引入一个命令,用于判断表达式的真假。test命令的语法如下:

test expr

其中 expr是通过test命令可以理解的选项来构建的。例如下面这条命令用于判断字符串变量password是否等于"john"。

test "$password" = "john"

如果两者相等,那么test命令就返回值0;反之则返回1。作为test的同义词,用户也可以使用方括号[进行条件测试。后者的语法如下:

[ expr ]

必须提醒读者注意的是,在Shell编程中,空格的使用绝不仅仅是编程风格上的差异。现在来对比下面3条命令:

password="john"

test "$password" = "john"

[ "$password" = "john"]

第一条是赋值语句,在password、= 和"john"之间没有任何空格;第2条是条件测试命令,在test、“Spassword”、=和"john"之间都有空格;第3条也是条件测试命令(是test命令的另一种写法),在[、"$password"、=、"john"和]之间都有空格。

一些C程序员喜欢在赋值语句中等号“=”的左右两边都加上空格,因为这样看上去会比较清晰,但是在 Shell中这种做法会导致语法错误。

password = "john"

bash: password: 找不到命令

同样地,试图去掉条件测试命令中的任何一个空格也是不允许的。去掉“[”后面的空格是语法错误,去掉等号(=)两边的空格会让测试命令永远都返回0(表示真)。

之所以会出现这样的情况是因为Shell首先是一个命令解释器,而不是一门编程语言。空格在Shell这个“命令解释器”中用于分隔命令和传递给它的参数(或者用于分隔命令的两个参数)。使用whereis命令查找test和“[”可以看到,这是个存放在/usr/bin目录下的“实实在在”的程序文件。

因此在上面的例子中,“Spassword”、=和"john"都是test命令和[命令的参数,参数和命令、参数和参数之间必须要使用空格分隔。而单独的赋值语句password="john"不能掺杂空格的原因也就很明显了。password是变量名,而不是某个可执行程序。

test和[命令可以对以下3类表达式进行测试;

字符串比较文件测试数字比较

字符串比较

test和[命令的字符串比较主要用于测试字符串是否为空,或者两个字符串是否相等。

用于字符串比较选项

选项描述-z str当字符串 str 长度为 0 时返回真-n str当字符串 str 长度大于 0 时返回真str1 = str2当字符串 str1 和 str2 相等时返回真str1 != str2当字符串 str1 和 str2 不相等时返回真

下面这段脚本用于判断用户的输入是否为空。如果用户什么都没有输入,就显示一条要求输入口令的信息。

#!/bin/bash

read password

if [ -z "$password"]

then

echo "Please enter the password"

fi

注意,在$password两边加上了引号(""),这在Bash中并不是必要的。Bash 会自动给没有值的变量加上引号,这样变量看上去就像是一个空字符串一样。但有些Shell并不这样做,如果 Shell简单地把空的password变量替换为一个空格,那么上面的判断语句就会变成这样。

if [ -z ]

毫无疑问,在这种情况下Shell就会报错。从清晰度和可移植性的角度考虑,为字符串变量加上引号是一个好的编程习惯。

用于比较两个字符串是否相等的操作在上文中已经作了介绍。不过需要注意的是,Shell对大小写敏感,只有两个字符串完全相等才会被认为是相等的。下面的例子说明了这一点。

#!/bin/bash

if [ "ABC" = "abc" ]

then

echo "ABC"=="abc"

else

echo "ABC"!="abc"

fi

if [ "ABC" = "ABC" ]

then

echo "ABC"=="ABC"

else

echo "ABC"!="ABC"

fi

运行结果显示,ABC和ABC是相等的,而ABC和 abc 则是不相等的。

文件测试

文件测试用于判断一个文件是否满足特定的条件。

选项描述-b file当file是块设备文件时返回真-c file当file是块设备文件时返回真-d pathname当file是块设备文件时返回真-e pathname当file是块设备文件时返回真-f file当file是块设备文件时返回真-g pathname当file是块设备文件时返回真-h file当file是块设备文件时返回真-p file当file是块设备文件时返回真-r pathname当file是块设备文件时返回真-s file当file是块设备文件时返回真-u pathname当file是块设备文件时返回真-w pathname当file是块设备文件时返回真-x pathname当file是块设备文件时返回真-o pathname当file是块设备文件时返回真

文件测试选项的使用非常简单。下面的例子取自系统中的 rc脚本。如果/sbin/unconfigured.sh文件存在并且可执行,就执行这个脚本。否则什么也不做。

if [ -x /sbin/unconfigured.sh ]

then

/sbin/unconfigured.sh

fi

数字比较

test和[命令在数字比较方面只能用来比较整数(包括负整数和正整数)。其基本的语法如下:

test int1 option int2

或者

[ int1 option int2 ]

其中的option表示比较选项。

用于数字比较的选项

选项对应的英语单词描述-eqequal如果相等,返回真-nenot equal如果不相等,返回真-ltlower than如果 int1 小于 int2,返回真-lelower or equal如果 int1 小于或等于 int2,返回真-gtgreater than如果 int1 大于 int2,返回真-gegreater or equal如果 int1 大于或等于 int2,返回真

下面这段代码取自Samba服务器的启动脚本。脚本使用-eq选项测试变量status是否等于0。如果是,就调用log_success_msg 显示Samba已经运行的信息,否则就调用log_failure_msg 显示Samba没有运行。

if [ $status -eq 0 ]; then

log_success_msg "SMBD is running"

else

log_failure_msg "SMBD is not running"

fi

复合表达式

到目前为止,所有的条件判断都是只有单个表达式。但在实际生活中,人们总是倾向于组合使用几个条件表达式,这样的表达式就被称为复合表达式。test和[命令本身内建了操作符来完成条件表达式的组合。

操作符描述! expr“非” 运算,当expr为假时返回真expr1 -a expr2“与” 运算,当expr1和expr2同时为真时才返回真expr1 -o expr2“或” 运算,expr1或expr2为真时返回真

下面这段脚本接受用户的输入,如果用户提供的文件存在,并且vi编辑器存在,就先复制(备份)这个文件,然后调用vi编辑器打开;如果用户文件不存在,或者没有vi编辑器,就什么都不做。

#!/bin/bash

if [ -f $@ -a -x /usr/bin/vi ]

then

cp $@ $@.bak

vi $@

fi

具体来说,该if语句依照下面的步骤执行。

首先执行-f $@测试命令,如果S@变量(也就是用户输入的参数)对应的文件存在,那么该测试返回真(0)﹔否则整条测试语句返回假,直接跳出if语句块。如果第一个条件为真,就执行-x /usr/bin/vi测试命令。如果/usr/bin/vi文件可执行,那么该测试返回真(0),同时整条测试语句返回真(0)。否则整条测试语句返回假,直接跳出if语句块。如果整条测试语句返回真,那么就执行if语句块中的两条语句。

再来看一个使用-o(或)和!(非)运算的例子。下面这段脚本在变量password非空,或者密码文件.private key存在的情况下向父进程返回0。否则提示用户输入口令。

if [ ! -z "$password" -o -f ~/.public_key ]

then

exit 0

else

echo "Please enter the password:"

read password

fi

该if语句依照下面的步骤执行。

首先执行! -z "Spassword"测试命令,如果字符串 password 不为空,那么该测试语句返回真(0),同时整条测试语句返回真(0),不再判断-f~/.public_key。如果第一个条件为假,就执行-f ~/.public_key测试命令。如果主目录下的.public_key文件存在,那么该测试返回真(0),同时整条测试语句返回真(0)﹔否则整条测试语句返回假,直接跳出if语句块。如果整条测试语句返回真,那么就执行exit 0﹔否则执行else语句块中的语句。

提示:注意-a (与)和-o(或)在什么情况下会判断第2条语句。前者在第1条语句为真的时候才判断第2条语句(因为如果第1条语句就不成立,那么整条测试语句一定不会成立)﹔后者在第1条语句为假的情况下才判断第2条语句(因为如果第1条语句为真,那么整条测试语句一定成立)。记住这一点,在后面的“复合命令”中还会碰到。

Shell的条件操作符&&和||可以用来替代test和[命令内建的-a和-o。如果选择使用Shell的条件操作符,那么上面的第一个例子可以改写成这样:

if [ -f $@ ] && [ -x /usr/bin/vi ]

then

cp $@ $@.bak

vi $@

fi

注意,&&连接的是两条[(或者test)命令,而-a操作符是在同一条[(或者test)命令中使用的。类似地,上面使用-o操作符的脚本可以改写成这样:

if [ ! -z "$password" ] || [ -f ~/.public_key ]

then

exit

else

echo "Please enter the password:"

read password

fi

究竟是使用Shell的条件操作符(&&、||)还是test/[命令内建的操作符(-a、-o),并没有“好”与“不好”的差别,这只是“喜欢”和“不喜欢”的问题。一些程序员偏爱“&&”和“I”是因为这样可以使条件测试看上去更清晰。而另一方面,由于-a和-o只需要用到一条test语句,因此执行效率会相对高一些。鱼和熊掌不可兼得,谁说不是呢?

循环结构

循环结构用于反复执行一段语句,这也是程序设计中的基本结构之一。Shell 中的循环结构有3种: while、until和 for。下面逐一介绍这3种循环语句。

1.while语句

while语句重复执行命令,直到测试条件为假。该语句的基本结构如下。注意, commands可以是多条语句组成的语句块。

while test=commands

do

commands

done

运行时, Shell首先检查test-commands是否为真(为0),如果是,就执行命令commands。commands执行完成后, Shell再次检查test-commands,如果为真,就再次执行commands……这样的“循环”一直持续到条件test-commands为假(非0)。为了更好地说明这一过程,下面这个脚本让 Shell 做一件著名的体力活:计算1+2+3+……+100。

#!/bin/bash

sum=0

number=1

while test $number -le 100

do

sum=$[ $sum + $number ]

let number=$number+1

done

echo "The summary is $sum"

简单地分析一下这段小程序。在程序的开头,首先将变量 sum和 number初始化为О和1,其中变量sum保存最终结果,number则用于保存每次相加的数。测试条件$number-le 100告诉Shell 仅当number 中的数值小于或等于100的时候才执行包含在do和done之间的命令。注意,每次循环之后都将number 的值加上1,循环在number达到101的时候结束。

保证程序能在适当的时候跳出循环是程序员的责任和义务。在上面这个程序中,如果没有let number=$number+1这句话,那么测试条件将永远为真,程序就陷在这个死循环中了。

while语句的测试条件未必要使用test(或者[ ])命令。在 Linux 中,命令都是有返回值的。例如,read 命令在接收到用户的输入时就返回0,如果用户用Ctrl+D快捷键输入一个文件结束符,那么read命令就返回一个非0值(通常是1)。利用这个特性,可以使用任何命令来控制循环。下面这段脚本从用户处接收一个大于0的数值n,并且计算1+2+3+……+n。

#!/bin/bash

echo -n "Enter a number(>0):"

while read n

do

sum=0

count=1

if [ $n -gt 0 ]

then

while [ $count -le $n ]

do

sum=$[ $sum + $count ]

let count=$count+1

done

echo "The summary is $sum"

else

echo "Please enter a number greater than zero"

fi

echo -n "Enter a number(>0):"

done

这段脚本不停地读入用户输入的数值,并判断这个数是否大于0。如果是,就计算从1一直加到这个数的和。如果不是,就显示一条提示信息,然后继续等待用户的输入,直到用户输入快捷键Ctrl+D(代表文件结束)结束输入。下面显示了这个脚本的执行效果。

2.until语句

until 是 while语句的另一种写法——除了测试条件相反。其基本语法如下:

until test-commands

do

commands

done

单从字面上理解,whilc说的是当test-commands为真(值为0),就执行commands。而until 说的是执行commands,直到 test-commands为真(值为0),这句话顺过来讲可能更容易理解。当test-commands为假(非0值),就执行commands。

但愿大家没有被上面这些话搞糊涂。下面这段脚本麻烦Shell 再做一次那个著名的体力劳动,不同的是这次改用until语句。

#! /bin/bash

sum=0

number=1

until ! test $number -le 100

do

sum=$[ $sum + $number ]

let number=$number+1

done

echo "The summary is $sum"

注意,下面这两句话是等价的

while test $number -le 100

until ! test $number -le 100

3.for语句

使用while语句已经可以完成Shell编程中的所有循环任务了。但有些时候用户希望从列表中逐一取一系列的值(例如取出用户提供的参数),此时使用while和until就显得不太方便。Shell提供了for语句,这个语句在一个值表上迭代执行。for 的基本语法如下:

for variable [in list]

do

commands

done

这里的值表是一系列以空格分隔的值。Shell每次从这个列表中取出一个值,然后运行do/done之间的命令,直到取完列表中所有的值。下面这段程序简单地打印出1~9之间(包括1和6)所有的数。

#! /bin/bash

for i in 1 2 3 4 5 6 7 8 9

do

echo $i

done

每次循环开始的时候,Shell 从列表中取出一个值,并把它赋给变量i,然后执行命令块中的语句(即echo $i)。下面显示了这个脚本的运行结果,注意 Shell是按顺序取值的。

用于存放列表数值的变量并不一定会在语句块中用到。如果某件事情需要重复N次的话,只要给for 语句提供一个包含N个值的列表就可以了。不过这种“优势”听上去有些可笑,如果N是一个特别大的数,难道需要手工列出所有这些数字吗?Shell 的简便性在于,所有已有的工具都可以在Shell 脚本中使用。Shell本身带了一个叫做seq的工具,该命令接受一个数字范围,并把它转换为一个列表。如果要生成1~9的数字列表,那么可以这样使用seq。

seq 9

这样,上面这个程序就可以改写成下面这样:

#! /bin/bash

for i in 'seq 9'

do

echo $i

done

这里使用了倒引号,表示要使用Shell 执行这条语句,并将运行结果作为这个表达式的值。用户也可以指定seq输出的起始数字(默认是1),以及“步长”。seq命令将在后续文章中详细讨论。for语句也可以接受字符和字符串组成的列表,下面这个脚本统计当前目录下文件的个数。

#! /bin/bash

count=0

for file in 'ls'

do

if ! [ -d $file ]

then

let count=$count+1

fi

done

echo "There are $count files"

这段脚本每次从ls生成的文件列表中取出一个值存放在file变量中,并给计数器增加1。下面是这段脚本的执行效果。

读取用户输入

Shell程序并不经常和用户进行大量的交互,但有些时候接受用户的输入仍然是必须的。read 命令提供了这样的功能,从标准输入接收一行信息。在前面的几节中,读者已经在一些程序中使用了read命令,这里将进一步解释其中的细节。read命令接受一个变量名作为参数,把从标准输入接收到的信息存放在这个变量中。如果没有提供变量名,那么读取的信息将存放在变量 REPLY中。下面的例子说明了这一点。

可以给read命令提供多个变量名作为参数。在这种情况下,read命令会将接收到的行“拆开”分别赋予这些变量。当然,read需要知道怎样将一句话拆成若干个单词,默认情况下,Bash只认识空格、制表符和换行符。下面这个脚本将用户输入拆分为两个单词分别放入变量first 和 second 中。

#! /bin/bash

read first second

echo $first

echo $second

下面是输入Hello World!后该脚本的输出。

脚本执行命令

下面介绍另两条用于控制脚本行为的命令exit和 trap。前者退出脚本并返回一个特定的值,后者用于捕获信号。合理地使用这两条命令,可以使脚本的表现更为灵活高效。

1.exit命令

exit命令强行退出一个脚本,并且向调用这个脚本的进程返回一个整数值。例如:

#! /bin/bash

exit 1

在一个进程成功运行后,总是向其父进程返回数值0。其他非零返回值都表示发生了某种异常。这条规则至少被广泛地应用,因此不要轻易去改变它。至于说父进程为什么需要接受这样一个返回值,这是父进程的事情——可以定义一些操作来处理子进程的异常退出(通过判断返回值是什么),也可以只是简单地丢弃它。

2.trap命令

trap命令用来捕获一个信号。信号是进程间通信的一种方式。可以简单地使用trap命令捕捉并忽视一个信号。下面这个脚本忽略INT信号,并显示一条信息提示用户应该怎样退出这个程序(INT信号当用户在Shell 中按Ctrl+C快捷键时被发送)。

#! /bin/bash

trap 'echo "Type quit to exit"' INT

while [ "$input" != 'quit' ]

do

read input

done

下面是这段脚本的执行效果。

有时候忽略用户的中断信号是有益的。某些程序不希望自己在执行任务的时候被打断,而要求用户依照正常手续退出。trap还可以捕捉其他一些信号,下面这段脚本在用户退出脚本的时候显示“Goodbye!”,就像ftp客户端程序做的那样。

#! /bin/bash

trap 'echo "Goodbye"; exit' EXIT

echo "Type 'quit' to exit"

while [ "$input" != "quit" ]

do

read input

done

注意,在 trap命令中使用了复合命令echo "Goodbye"; exit,即先执行echo"Goodbye"显示提示信息,再执行exit退出脚本。这条复合命令在脚本捕捉到EXIT信号的时候执行。EXIT信号在脚本退出的时候被触发。下面是该脚本的执行效果。

Linux中还有很多其他信号,用于执行不同的操作。并不是所有的信号都可以被捕捉(比如 kill信号就不能被操纵或者忽略)。

创建命令表

test命令的-a和-o参数执行第2条测试命令的情况是不同的。这一点同样适用于Shell内建的&&和||。事实上,&&和||更多地被用来创建命令表,命令表可以利用一个命令的退出值来控制是否执行另一条命令。下面这条命令取自系统的rc脚本。

[ -d /etc/rc.boot ] && run-parts /etc/rc.boot

这条命令首先执行[ -d /etc/rc.boot ],判断目录/etc/rc.boot是否存在。如果该测试命令返回真,就继续执行run-parts /etc/rc.boot调用run-parts命令执行/etc/rc.boot 目录中的脚本。如果测试命令[ -d /etc/rc.boot ]返回假(即/etc/rc.boot目录不存在),那么run-parts命令就不会执行。因此上面这条命令等价于:

if [ -d /etc/rc.boot ]

then

run-parts /etc/rc.boot

fi

显然,使用命令表可以让程序变得更简洁。Shell 提供了3种形式的命令表。

表示形式说明a && b“与” 命令表。当且仅当a执行成功,才执行ba || b“或” 命令表。当且仅当a执行失败,才执行ba;b顺序命令表。先执行a,再执行b

其他有用的Shell编程工具

本节介绍一些有用的 Shell工具。这些工具在之前的章节中没有出现,但是可能对从事Shell编程的用户会很有用。其中一些和脚本编程密切相关,另一些则是关于文件操作的。

其他常用的Shell命令

命令描述cut以指定的方式分割行,并输出特定的部分diff找出两个文件的不同点sort对输入的进行排序uniq删除已经排好序的输入中的重复行tr转换或删除字符wc统计字符,单词和行的数量substr提取字符串中的一部分seq生成整数数列

1.cut命令

cut命令用于从输入的行中提取指定的部分(不改变源文件)。以下面这个文件 city.txt为例,简单地演示cut命令的分割效果。该文件包含了4个城市的长途电话区号,城市名和区号之间使用空格分隔。

Beijing 010

Shanghai 021

Tianjin 022

Hangzhou 0571

带有-c选项的cut命令提取一行中指定范围的字符。下面这条命令提取city.txt 中每行的第3~6个字符。

更有用的一个选项是-f。-f选项提取输入行中指定的字段,字段和字段间的分隔符由-d参数指定。如果没有提供-d参数,那么默认使用制表符(TAB)作为分隔符。下面这条命令提取并输出city.txt中每一行的第2个字段(城市区号)。

2.diff

diff命令通常被程序员用来确定两个版本的源文件中存在哪些修改。下面这条命令比较badpro脚本的两个版本。

diff badpro badpro2

7c7

< sleep 2s

---

> sleep 6s

diff命令输出的第一行指出了发生不同的位置,7c7表示 badpro的第7行和 badpro2的第7行是不同的。紧跟着diff列出了这两行不同的地方,左箭头<后面紧跟着badpro中的内容,右箭头>后面紧跟着badpro2中的内容,两者之间使用一些短划线分隔。

3.sort命令

sort命令接受输入行,并对其按照字母顺序进行排列(不改变源文件)。仍然以4个城市的区号表为例,下面这条命令按照字母升序排列后输出这张表。

用户也可以使用-r选项颠倒排列的顺序,即以字母降序排列。

默认情况下,sort是按照第1个字段执行排序的。可以使用-k选项指定按照另一个字段排序。下面这个例子按照city.txt每一行的第2个字段(区号)对输出行执行逆向排序。

4.uniq命令

uniq命令可以从已经排好序的输入中删除重复的行,把结果显示在标准输出上(不改变源文件)。作为例子,在city.txt的最后加入重复的一行,使其看起来如下:

Beijing 010

Shanghai 021

Tianjin 022

Hangzhou 0571

Shanghai 021

注意,uniq命令必须在输入已经排好序的情况下才能正确工.作(这说的是相同的几行必须连在一起)。可以使用sort命令结合管道做到这一点。

# sort city.txt | uniq

Beijing 010

Hangzhou 0571

Shanghai 021

Tianjin 022

5.tr命令

tr命令按照用户指定的方式对字符执行替换,并将替换后的结果在标准输出上显示(不改变源文件)。以下面这个文件alph.txt为例:

ABC DEF GHI

jkl mno pqr

StU vwx yz

12A Cft pOd

Hct Yoz cc4

下面这条命令将文件中所有的A转换为H,B转换为C,H转换为A。

将几个字符转换为同一个字符非常容易,和使用正则表达式一样。下面的例子将alph.txt中所有的A、B和C都转换为Z。

可以为需要转换的字符指定一个范围,上面命令等价于:

tr "A-C" "[Z*]" < alph.txt

还可以指定tr删除某些字符。下面的命令删除alph.txt中所有的空格。

6.wc命令

小写的wc是word counts的意思,用来统计文件中字节、单词以及行的数量。例如:

这表示city.txt文件中总共有5行(在讲解uniq命令的时候添加了重复的一行)、10个单词(以空格分隔的字符串)和71个字节。如果3个数字同时显示不太好辨认,可以指定wc只显示某几项信息。

wc命令的常用选项

选项描述-c 或 --bytes显示字节数-l 或 --lines显示行数-L 或 --max-linc-length显示最长 行的长度-w 或 --words显示单词数–help显示帮助信息

7.substr命令

substr命令从字符串中提取一部分。在编写处理字符串的脚本时,这个工具非常有用。substr接受3个参数,依次是字符串(或者存放有字符串的变量)、提取开始的位置(从1开始计数)和需要提取的字符数。下面这条命令从Hello World中提取字符串Hello。

注意,substr 必须使用expr进行表达式求值,因为这并不是一个程序,而是Shell 内建的运算符。如果不使用expr,那么系统会提示找不到substr命令。

8.seq命令

seq命令用于产生一个整数数列。seq最简单的用法莫过于在介绍for语句时看到的那样。

默认情况下,seq从1开始计数。也可以指定一个范围。

可以明确指定一个步长。下面的命令生成0~9的数列,递减排列,每次减3。

【定制工具:安全的delete命令】

系统的rm命令常常导致一些不愉快的事情。默认情况下rm不会在删除文件前提示用户是否真的想这么做,删除后也不能再从系统中恢复。这意味着用户不得不为自己的一时糊涂付出惨痛的代价。Shell编程总是能帮助用户摆脱类似的烦恼。系统没有的,就自己动手创造。本节将设计一个相对“安全”的delete命令来替代rm。好吧,废话少说,首先来看一下究竟有哪些事情需要去做。

在用户的主目录下添加目录.trash 用作“回收站”﹔在每次删除文件和目录前向用户确认;将需要“删除”的文件和目录移动到~/.trash中。

下面是这个脚本的完整代码

##建立回收站机制

##将需要删除的文件移动到~/.trash中

#!/bin/bash

if [ ! -d ~/.trash ]

then

mkdir ~/.trash

fi

if [ $# -eq 0 ]

then

#提示 delete 的用法

echo "Usage: delete filel [file2 file3 ...]"

else

echo "You are about to delete these files:"

echo $@

#要求用户确认是否删除这些文件,回答 N 或 n 放弃删除,其他字符表示确认

echo -n "Are you sure to do that [Y/n]:"

read reply

if [ "$reply" != "n" ] && [ "$reply" != "N" ]

then

for file in $@

do

#判断文件或目录是否存在

if [ -f "$file" ] || [ -d "$file" ]

then

mv -b "$file" ~/.trash/

else

echo "$file: No such file or directory"

done

#如果用户回答 N或n

else

echo "No file removed"

fi

fi

注意,在使用mv命令移动文件时使用了-b选项。这样当~/.trash中已经存在同名文件的时候,mv不会简单地把它覆盖,而是先改名,然后把文件移动过去,最后把 delete 脚本复制到/bin目录下,这样用户就不需要每次使用时都指定一个绝对路径了。

cp delete /bin/

不过,这个delete并不是那么完美。例如它不能够处理文件名中存在空格的情况。

大家可以尝试改进这个脚本程序,来满足自己的需求。事实上,如果感到Linux 中的某些命令不够顺手,完全可以“改造”它。然后通过定义别名和环境变量让系统认识这些修改。

整合不易还请大家一键三连评论一下~