文章目录
- 前言
- 变量与算术
- 变量赋值与环境
- 参数展开
- 展开运算符
- 位置参数
- 特殊变量
- 算术展开
- 退出状态
- 退出状态值
- if-else-else-fi语句
- 逻辑的NOT、AND与OR
- test命令
- case语句
- 循环
- for循环
- while与until循环
- break与continue
- shift与选项处理
- 函数
前言
变量对于正规程序而言很重要。处理维护有用的值作为数据,变量还用于管理程序状态。由于Shell主要是字符串处理语言,所以你可以利用Shell变量对字符串做很多事。然而,因为算术运算也是必要的,所以POSIX Shell也提供利用Shell变量执行算术运算机制。
流程控制的功能造就了程序语言:如果你有的只是命令语句,是不可能完成任何工作的。后面介绍了用来测试结果、根据这些记过做出判断以及加入循环的功能。
最后介绍的是函数:它可以将相关工作的语句集中在同一处。这么一来就可以在脚本里的任何位置,轻松执行此工作。
变量与算术
Shell变量如同传统程序语言的变量一样,是用来保存某个值,直到你需要它们为止。我们在2.5.2节里已介绍过Shell变量名称与值的基本概念,但除此之外,Shell脚本与函数还有位置参数
的功能;传统的说法应该是“命令行参数”。
Shell脚本里经常出现一些简单的算术运算,例如没经过一次循环,变量就会加1。POSIX Shell为内嵌算术提供了一种标记法,称为算术展开。Shell会对$((...))
里的算术表达式进行计算,再将计算后的结果放回到命令的文本内容。
变量赋值与环境
Shell变量的赋值与使用方式已在2.5.2节中提到过,但这个小节将解释之前未提及的内容。有两个相似的命令提供变量的管理,一个是readonly
,它可以使用变量成为只读模式;而赋值给它们是被禁止的。在Shell程序中,这是创建符号常量的一个好方法:
hours_per_day=24 seconds_per_hour=3600 day_per_week=7 #赋值
readonly hours_per_day seconds_per_hour day_per_week #设置为只读模式
较常见的命令是export
,其用法是将变量放进环境变量里。环境是一个名称与值的简单列表,可供所有执行中的程序使用。新的进程会从其父进程集成环境,也可以建立新的子进程之前修改它。export
命令可以将新变量添加到环境中:
PATH=$PATH:/usr/local/bin #更新PATH
export PATH #导出它
最初的Bourne Shell会要求你使用一两个步骤的进程;也就是,将赋值与导出(export
)或只读(readonly
)的操作分开。POSIX标准运行你将赋值与命令的操作结合在一起:
readonly hours_per_day=24 seconds_per_hours=3600 days_per_week=7
export PATH=$PATH:/usr/local/bin
export
命令可用于显示当前环境:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qd43yXLz-1669863681529)(file://C:\Users\g700382\AppData\Roaming\marktext\images\2022-11-28-14-12-56-image.png)]
变量可以添加到程序环境中,但是对Shell或接下来的命令不会一直有效:将该(变量)赋值,置于命令名称与参数前即可:
PATH=/bin:/usr/bin awk '...' file1 file2
这个PATH
值的改变仅针对单个awk
命令的执行。任何接下来的命令,所看到的都是在他们环境中PATH
的当前值。
export
命令仅将变量加到环境中,如果你要从程序的环境中删除变量,则要用env
命令,env
也可以临时地改变环境变量值:
env -i PATH=$PATH HOME=$HOME LC_ALL=c awk '...' file1 file2
-i
选项是用来初始化环境变量的;也就是丢弃任何的继承值,仅传递命令行上指定的变量给程序使用。
unset
命令从执行中的Shell中删除变量与函数。默认情况下,它会解除变量设置,也可以加上-v
来完成:
unset full_name #删除full_name变量
unset -v first midlle last #删除其他变量
使用unset -f
删除函数
who_is_on(){ #定义函数
who | awk '{print $1}' | sort -u # 产生排序后的用户列表
}
...
unset -f who_is_on #删除函数
Shell早期版本没有函数功能或unset
命令;POSIX加入-f
选项,以执行删除函数的操作,之后还加入-v
选项,以便与-f
相对应。
参数展开
参数展开是Shell提供变量值在程序中使用的过程;例如,作为给新变量的值,或是作为命令行的部分或全部参数。最简单的形式如下所示:
reminder="Time to go to the dentist" #将值存储在reminder中
sleep 120 #等待两分钟
echo $reminder #显示信息
在Shell下,有更复杂的形式可用于更特殊的情况。这些形式都是将变量名称括在花括号里(${variable})
,然后再增加额外的语法以告诉Shell该做什么。花括号本身也是很好用的,当你需要在变量名称之后马上跟着一个可能会解释为名称的一部分的字符时,它就派上用场了:
reminder="Tome to go to the dentist!" #将值存储在reminder中
sleep 120 #等待两分钟
echo _${reminder}_ #加下划线符号强调显示的信息
展开运算符
第一组字符串处理运算符用来测试变量的存在状态,且为在某种情况下允许默认值的替换。如下所示:
表里的每个运算符内的冒号:
都是可选的。如果省略冒号,则将每个定义中的”存在且非NULL“部分改为”存在“,也就是说,运算符仅用于测试变量是否存在。
表中的运算符已在Bourne Shell下使用了20多年。POSIX标准化额外的运算符,用来执行模式匹配于删除变量值里的文本。新的模式匹配运算符,通常是用来切分路径名称的组成部分,例如目录前缀与文件名后缀。除了列出Shell的模式匹配运算符之外,下表也展示了这些运行范例。在这些例子里,我们假设变量path
的值为/home/tolstoy/mem/lobg.file.name
.
这些看起来很难记,我们提供一个帮助记忆的好方法:#
匹配的是前面,因为数字正负号总是在数字前面;%
匹配的是后面,因为百分比符号总是跟在数字的后面。另外一种帮助记忆的方式是看传统的键盘配置(当然,指的是在美式键盘上):#
位置靠左,%
靠右。
在这里用到的两种模式分别是:/*/
,匹配任何位于两个斜杠之间的元素;.*
,匹配点号之后接着的任何元素。
最后,POSIX标准化字符串长度运算符:${#variable}
返回$variable
值里的字符长度:
$ x=supercailfagilisticexpialidocious
$ echo There are ${#x} characters in $x
There are 33 characters in supercailfagilisticexpialidocious
位置参数
所谓位置参数,指的是Shell脚本的命令行参数;同时也表示在Shell函数内的函数参数。它们的名称是以单个的整数来命名。出于历史的原因,当这个整数大于9时,就应该以花括号({})
括起来。
echo first arg is $1
echo tenth arg is ${10}
你也可以将前一节介绍的值测试与模式匹配运算符,应用到位置参数:
filename=${1:-/dev/tty} #如果给定参数则使用它,如果无参数则使用/dev/tty
下面介绍的特殊"变量"提供了对传递的参数的总数的访问,以及一次对所有参数的访问:
-
$#
- 提供传递到Shell脚本或函数的参数总数。当你是为了处理选项和参数而建立循环时,它会很有用。举例如下
while[ $# != 0] #以shift逐渐减少$#,循环将会终止
do
case $1 in #处理第一个参数
...
esac
shift #移开第一个参数
done
-
$*,$@
- 一次表示所有的命令行参数。这两个参数可用来把命令行参数传递给脚本或函数所执行的程序。
-
"$*"
- 将所有命令行参数视为单个字符串。等同于
"$1 $2 ..."
。$IFS
的第一个字符用来作为分隔字符,以分隔不同的值来建立字符串。举例如下:
- 将所有命令行参数视为单个字符串。等同于
printf "The arguments were%s\n" "$*"
-
"$@"
- 将所有命令行参数视为单个的独体,也就是单个字符串。等同于
"$1" "$2" ...
。这是将参数传递给其他程序的最佳方式,因为它会保留所欲内嵌在每个参数里的任何空白,举例如下:
- 将所有命令行参数视为单个的独体,也就是单个字符串。等同于
lpr "$@" #显示每一个文件
set
命令可以做的事很多。调用此命令而为给予任何选项,则它会设置位置参数的值,并将之前存在的任何值丢弃:
set -- hi there how do you do # -- 会结束选项部分,自hi开始新的参数
shift
命令是用来截去来自列表的位置参数,由左开始。一旦执行shift
,$1
的初始值会永远消失,取而代之的是$2
的旧值。$2
的值,变成$3
的旧值,以此类推。$#
值则会逐次减1。shift
也可使用一个可选的参数,也就是要位移的参数的计数。单纯的shift
等同于shift 1
。以下范例将这些操作串联在一起,并添加了注释
$ set -- hello "hi there" greetings #设置新的位置参数
$ echo there are $# total arguments #显示计数值
there are 3 total arguments
$for i in $* #循环处理每个参数
> do echo i is $i
> done
i is hello #注意内嵌的空白已消失
i is hi
i is there
i is greetings
$ for i in $@ #在没有双引号的情况下,$*与$@是一样的
> do echo i is $i
> done
i is hello
i is hi
i is there
i is greetings
$ for i in "$*" #加了双引号,$*表示一个字符串
> do echo i is $i
> done
i is hello hi there greetings
$ for i in "$@" #加了双引号,$@保留真正的参数值
> do echo i is $i
> done
i is hello
i is hi there
i is greetings
$ shift #截去第一个参数
$ echo there are now $# arguments #证明它已消失
there are now 2 arguments
$ for i in "$@"
> do echo i is $i
> done
i is hi there
i is greetings
特殊变量
除了我们看过的特殊变量(例如$#及$*
)之外,Shell还有很多额外的内置变量。有一些也具有单一字符、非文字或数字字母的名称;其他则是全由大写字母组成的名称。
下表列出内置于Shell内的变量,以及影响其行为的变量。所有Bourne风格的Shell提供的变量都比这里所列的多很多,他们会影响交互模式下的使用,也可以在处理Shell程序时用于其他的用途。不过下面要说的这些,是在写Shell程序时,可以完全依赖于实现可移植性脚本编程的变量。
变量 | 意义 |
---|---|
# | 目前进程的参数个数 |
@ | 传递给当前进程的命令行参数。置于双引号内,会展开为个别的参数 |
* | 当前进程的命令行参数。置于双引号内,则展开为一单独参数 |
? | 前一命令的退出状态 |
$ | Shell进程的进程编号(process ID) |
0(零) | Shell程序的名称 |
! | 最近一个后台命令的进程编号。以此方式存储进程编号,可通过wait命令以供稍后使用。 |
ENV | 一旦引用,则仅用于交互式Shell中;$ENV的值是可展开的参数。结果硬要读取和在启动时要执行一个文件的完整路径名称。这是一个XSI必须的变量 |
HOME | 根(登录)目录 |
IFS | 内部的字段分隔器;例如,作为单词分隔器的字符列表。一般设为空格、制表符(Tab),以及换行(newline)。 |
LANG | 当前locale的默认名称;其他的LC_*变量会覆盖其值 |
LC_ALL | 当前local的名称;会覆盖LANG与其他LC_*变量 |
LC_COLLATE | 用来排序字符的当前locale名称 |
LC_CTYPE | 在模式匹配期间,用来确定字符类别的当前locale的名称 |
LC_MESSAGES | 输出信息的当前语言名称 |
LINENO | 刚执行过的行在脚本或函数内的行编号 |
NLSPATH | 在$LC_MESSAGE(XSI)所给定的信息语言里,信息目录的位置 |
PATH | 命令的查找路径 |
PPID | 父进程的进程编号 |
P$1 | 主要的命令提示字符串。默认为$ |
P$2 | 行继续的提示字符串,默认为> |
P$4 | 以set -x 设置的执行跟踪提示字符串,默认为+ |
PWD | 当前工作目录 |
特殊变量$$
可在编写脚本时用来建立具有唯一性的文件名(多半是临时的),这是根据Shell的进程编号建立文件名。不过,系统里还有一个mktemp
命令也能做同样的事。
算术展开
Shell的算术运算符和C语言里的差不多,优先级与顺序也相同。下表列出了支持的算术运算符,优先级由最高到排列至最低。虽有些是(或包含)特殊字符,不过它们不需要以反斜杠转义,因为它们都置于$((...))
语法中。这一语法如同双引号功能,除了内嵌双引号无须转义。
类比C语言
,使用方法几乎一致。
退出状态
每一条命令,不管是内置的,Shell函数,还是外部的,当它退出时,都会返回一个小的整数值给引用它的程序,这就是大家所熟知的程序的退出状态(exit status
)。在Shell下执行程序时,有许多方式可取用程序的退出状态。
退出状态值
以惯例来说,退出状态为o
表示“成功”,也就是,程序执行完成且未遭遇到任何问题。其他任何的退出状态都为失败(我们稍后将会介绍如何使用退出状态)。内置变量?
(以$?
访问它)包括了Shell最近一次所执行的一个程序的退出状态。
例如,当你输入ls
时,Shell找到ls
并执行该程序。当ls
结束时,Shell会恢复ls
的退出状态。请见下面的例子:
$ ls -l
-rw-r--r-- 1 root root 0 Nov 29 16:21 test
$ echo $?
0
$ls -l root
ls: root: No such file or directory
$echo $?
1
POSIX标准定义了退出状态及其含义,如下表
令人好奇的是,POSIX留下退出状态128未定义,仅要求它表示某种失败。因为只有低位的8个位会返回给父进程,所以大于255的退出状态都会替换成该值除以256之后的余数。
你的Shell脚本可以使用exit
命令传递一个退出值给它的调度者。只要将一个数字传递给它,作为第一个参数即可。脚本会立即退出,并且调用者会收到该数字且作为脚本的退出值:
if-else-else-fi语句
使用程序的退出状态,最简单的方式就是使用if语句。一般语法如下:
if pipeline
[pipeline ....]
then
statements-if-true-1
[elif pipeleine
[pipeline ...]
then
statements-if-true-2
...]
[else
statement-if-all-else-fails]
fi
(方括号表示的是可选部分,并非逐字输入)
以我们手边的例子来看,你应该大致猜得到它的工作方式:Shell执行第一组介于if
与then
之间的语句块。如果最后一条执行的语句成功地退出,它便执行statements-if-true-1,否则,如果有elif
,它会尝试下一组语句块。如果最后一条语句成功地退出,则会执行statements-if-true-2.它会以这种方式继续,执行相对应的语句块,直到它碰到一个成功退出的命令为止。
如果if
或else
语句里没有一个为真,并且else
子句存在,它会执行statements-if-all-else-fails。否则,它什么事也不会做。整个if..fi
语句的退出状态,就是在then或else
后面的最后一个被执行命令的退出状态。如果无任何命令执行,则退出状态为0,。举例如下:
if grep pattern myfile > /dev/null
then
... #模式在这里
else
... #模式不在这里
fi
如果myfile含有模式pattern,则grep的退出状态为0。如果无任何的行匹配模式,则退出状态的值为1,且如果发生一个错误,则会具有一个大于1的值。Shell会根据grep的退出状态,选择要执行那一组语句块。
逻辑的NOT、AND与OR
有时,以否定状态表达测试操作会比较容易些:“如果John不在家,则…“,在Shell下,这种情况的做法是:将惊叹号放在管道(pipeline)前:
if ! grep pattern myfile > /dev/null
then
... #模式不在这里
fi
POSIX在1992标准中国引进这种标记方式。你可能会看到较旧的Shell脚本使用冒号(:
)命令,其实并没有做任何事,它只是为了处理下面的情况:
if grep pattern myfile > /dev/null
then
: #不做任何事
else
.... #模式不在这里
fi
除了!
来测试事情的相反面之外,你也常会需要以AND与OR
结构来测试多重子条件(如果John在家,且他不忙,则…)。当你以&&
将两个命令分隔时,Shell会先执行第一个。如果它成功地退出,则Shell执行第二个。如果第二个命令也成功地退出,则整个语句块视为已经成功:
if grep pattern1 myfile && grep pattern2 myfile
then
.... #myfile包含两种模式
fi
相对的,||
运算符则是用来测试两种条件是否有一个结果为真:
if grep pattern1 myfile || grep pattern2 myfile
then
.... #一个或另一个模式出现
fi
这两种都是快捷运算符,即当判断出整个语句块的真伪时,Shell会立即停止执行命令。举例来说,在command1 && command2
下,如果command1
失败,则整个结果不可能为真,所以command2
也不会被执行;以此类推,command1 || command2
指的就是:如果caommand1成功
那么也没必要执行command2
。
不要尝试过渡“简练”而使用&& 和 ||
取代if
语句。我们不反对简短且简单的事情,如下:
$ who | grep tolstoy > /dev/null && echo tolstoy is logged on
tolstoy is logged on
上面的实际做法是:执行who | grep ...
,且如果成功,就显示信息。而我们曾见过有厂商提供Shell脚本,所使用的是这样的结构:
some_command && {
one command
a second command
and a third command
}
花括号将所有命令语句在一起,只有在some_command
成功时它们才会被执行。使用if
可以让它更为简洁:
if some_command
then
one command
a second command
and a third command
fi
test命令
test
命令可以处理Shell脚本里的各类工作。它产生的不是一般输出,而是可使用的退出状态。test
接受各种不同的参数,可控制它执行哪一种测试。
test
命令有另一种形式:[....]
,这种用法的作用完全与test
命令一样。因此,下面是测试两个字符串是否相等的两个语句:
if test "$str1" = "$str2"
then
....
fi
if [ "$str1" == "$str2"]
then
...
fi
POSIX将test
的参数描述为“表达式”,有一元表达式和二元的表达式。通常,一元表达式由看似一个选项的部分(例如,-d
用来测试文件是否为目录)与一个相对应的运算数组成,后者基本上(但不一定)是一个文件名。二元的表达式则有两个运算数与一个内嵌的运算符,以执行某种比较操作。再者,当只有一个参数时,test
会检查它是否为null
字符串。完整的参考下表
运算符 | 如果…则为真 |
---|---|
string | string不是null |
-b file | file是块设备文件 |
-c file | file是字符设备文件 |
-d file | file是目录 |
-e file | file 存在 |
-f file | file为一般文件 |
-g file | file有设置它的setgid位 |
-h file | file是一符号连接 |
-L file | file是一符号连接(等同于-h) |
-n string | string为非null |
-p file | file是一命名的管道(FIFO文件) |
-r file | file是可读的 |
-S file | file是socket |
-s file | file不是空的 |
-t n | 文件描述符n指向一终端 |
-u file | file有设置它的setuid位 |
-w file | file是可写入的 |
-x file | file是可执行的,或file是可被查找的 |
-z string | string为null |
s1 = s2 | 字符串s1与s2相同 |
s1 != s2 | 字符串s1与s2不同 |
n1 -eq n2 | 整数n1等于n2 |
n1 -ne n2 | 整数n1不等于n2 |
n1 -lt n2 | n1小于n2 |
n1 -gt n2 | n1大于n2 |
n1 -le n2 | n1小于或等于n2 |
n1 -ge n2 | n1大于或等于n2 |
也可以测试否定的结果,只需前置!
字符即可。下面是测试运行的范例
if [ -f "$file" ]
then
echo $file is a regular file
elif [ -d "$file" ]
then
echo $file is a directory
fi
if [ ! -x "$file" ]
then
echo $file is NOT executable
fi
在XSI兼容的系统里,test
版本是较为复杂的。它的表达式可以与-a
(作逻辑的AND)与-o
(做逻辑的OR)结合使用。-a
的优先级高于-o
,而=
与!=
优先级则高于其他二元运算符。在这里,也可以使用圆括号将其语句括起来以改变计算顺序。
POSIX的test
算法介绍如下
为了可移植性,POSIX标准里建议对多重条件使用Shell层级测试,而非使用-a
与-o
运算符(我们建议也这么用),举例如下:
if [ -f "$file" ] && ![ -w "$file" ]
then
# $file存在且为一般文件,但不可写入
echo $0: $file is not writable, giving up. > &2
exit 1
fi
>&2
就是把结果输出到和标准错误一样;之前如果有定义标准错误重定向到某个file文件,那么标准输出也重定向到这个file文件。其中&
的意思,可以看成“The same as”、的意思。
下面是几个使用test
的诀窍:
-
需要参数
- 由于这个原因,所有的Shell变量展开都应该以引号括起来,这样
test
才能接手一个参数——即使它已变为null字符串。例如
- 由于这个原因,所有的Shell变量展开都应该以引号括起来,这样
if [ -f "$file "] .... #正确
if [ -f $file ] ... #不正确
-
字符串比较是很微妙的
-
特别是字符串值为空,或是开头带有一个减号时,
test
命令就会被混淆。因此有了一种比较难看不过广为使用的方式:在字符串前面前置字母X
(X
的使用是随意的,这是传统用法). -
if ["X$answer" = "Xyes"] ...
-
你会看到这种方式出现在许多Shell脚本中吗,事实上POSIX标准库里的所有范例都是这么用的。
-
将所有参数以引号括起来的算法仅适用于
test
,而这种算法在test
现代版本里是足够的,即使第一个参数的开头字符为减号也不会有问题。因此我们已经很少需要在新的程序里使用前置X
的方式了。不过,如果可移植性最大化远比可读重要,或许使用前置X
的方式比较好。
-
-
test是可以被愚弄的
- 当我们要检查通过网络加载的文件系统访问时,就有可能将加载选项与文件权限相结合,以欺骗
test
、使其认为文件是可读取的,但事实是:操作系统根本不让你访问这个文件。所以尽管:test -r a_file && cat a_file
。理论上应该一定可行,但实际上会失败。针对这一点你可以做的就是加上一些起亚层面的防御程序:
- 当我们要检查通过网络加载的文件系统访问时,就有可能将加载选项与文件权限相结合,以欺骗
if test -r a_file && cat a_file
then
#cat worked, proceed on
else
# attempt to recover, issue an error message, etc.
fi
-
只能做整数数字测试
- 你不能使用
test
做任何浮点数算术运算。所有的数字测试值可处理整数。
- 你不能使用
下列会测试$#
,即命令行参数编号,如果未提供,则显示错误。
#! /bin/bash
# finduser --寻找是否有第一个参数所执行的用户登录
if [ $# -ne 1 ]
then
echo Usage: finduser username >&2
exit 1
fi
who | grep $1
case语句
如果你需要通过多个数值来测试变量,可以将一系列if
与elif
测试搭配test
一起使用:
if [ "X$1" = "X-f" ]
then
... #针对 -f 选项的程序代码
elif [ "X$1" = "X-d" ] || [ "X$1" = "X--directory" ] #允许长选项
then
... #针对-d选项的程序代码
else
echo $1: unkown option >&2
exit 1
fi
不过这么做的时候写起来很不顺手,也很难阅读。相对地,Shell的case
结构应该用来进行模式匹配:
case $1 in
-f)
... #针对 -f选项的程序代码
;;
-d | --directory) #允许长选项
... #针对 -d选项的程序代码
;;
*)
echo $1: unknown option >&2
exit 1
# 在 “esac”之前的 ;;形式是一个好习惯,不过并非必要
esac
这里我们看到,要测试的值出现在case与in
之间。将值以双引号括起来虽然并非必要,但也无妨。要测试的值,根据Shell模式的列表依次测试,发现匹配的时候,便执行相应的的程序代码,直至;;
为止。可以使用多个模式,只要|
字符加以分隔即可,这种情况下称为“or(或)”。模式里会包含任何Shell通配符,且变量、命令与算术替换会在它用作模式匹配之前在此值上被执行。
你可能会觉得在每个模式列表之后的不对称的右圆括号有点奇怪;不过这也是Shell语言里不对称定界符的唯一实例。
最后的*
模式是传统用法,但非必须的,它是作为一个默认的情况。这通常是在你要显示诊断信息并退出时使用。正如我们前面提及的,最后一个情况不再需要结尾的;;
,不过加上它会是比较好的形式。
循环
除了if与else
语句之外,还有Shell的循环结构也是非常好用的工具。
for循环
for
循环用于重复整个对象列表,依次执行每一个独立对象循环内容。对象可能是命令行参数、文件名或是任何可以以列表格式建立的东西。
现在我们假定,比较可能出现的情况应该拥有一些XML文件,再由这些XML文件集结成小册子。在此情况下,我们要做的应该是改变所有这些XML文件。所以for
循环足以适合这一情况:
for i in atlbrochure*.xml
do
echo $i
mv $i $i.old
sed 's/Atlanta/&, the capital of the South' < $i.old > $i
done
该循环将每个原始文件备份为副文件名为.old
的文件,之后再使用sed
处理文件以建立新文件。这个程序也显示文件名,作为执行进度的一种指示,这在有许多文件要处理时会有很大的帮助。
for
循环里的in
列表(list
)是可选的,如果省略,Shell循环会遍历整个命令行参数。这就好像你已经输入了for i in "$@"
:
for i #循环通过命令行参数
do
case $i in
-f) ...
;;
...
esac
done
while与until循环
Shell的while
与until
循环,与传统程序语言循环类似。语法为:
while condition until condition
do do
statements statements
done done
至于if
语句,condition
可以是简单的命令列表,或者是包含&&
与||
的命令。
while
与until
唯一的不同之处在于,如何对待condition
的退出状态。只要condition
是成功退出,while
会继续循环。只要condition
未成功结束,until
则执行循环。例如:
pattern=... #模式会控制字符串的缩简
while [ -n "$string" ] #当字符串不为空时
do
#处理$string的当前值
string=$(string%pattern) #截去部分字符串
done
实际上,until
循环比while
用的少,不过如果你在等待某个事件发生,它就很有用了。见下列:
#使用until 等待某个用户登录
#等待特定用户登录,每30秒确认一次
printf "Enter username: "
read user
until who | grep "$user" > /dev/null
do
sleep 30
done
你可以将管道放入到while
循环中,用来重复处理每一行的输入,如下所示:
产生数据 |
while read name rank serial_no
do
...
done
以上述例子来说,while
循环的条件所使用的命令一直是read
。后面会进行举例同时会告诉你还可以使用管道将循环输出传递给另一个命令。
break与continue
并非所有Shell里的东西都是直接来自Algol68.Shell也从C借用了break与continue
命令。这两个命令分别用来退出循环,或跳到循环体的其他地方。
#等待特定的用户登录,每30秒确认一次
printf "Enter username: "
read user
while true
do
if who | grep "$user" > /dev/null
then
break;
fi
sleep 30
done
true
命令什么事也不必做,只是成功地退出。这用于编写无限循环,即会永久执行的循环。在编写无限循环时,必须放置一个退出条件在循环体内,正如同这里所做的。另有一个false
命令和它有点相似,只是较少用到,它也不做任何事,仅表示不成功的状态。false
命令常见于无限until fasle....
循环中。
continue
命令则用于提早开始下一段重复的循环操作,也就是在到达循环体的底部之前。
break与continue
命令都接受可选的数值参数,可分别用来指出要中断(break
)或继续多个被包含的循环(如果循环计数需要的是一个在运行时可被计算的表达式,可以使用$((...))
)。举例如下:
while condition1 #外部循环
do ...
while condition2 #内部循环
do ...
break 2 #外部循环的中断
done
done #在中断之后,继续执行这里的程序
...
break与continue
特别具备中断或继续多个循环层级的能力,从而以简洁的形式弥补了Shell语言里缺乏goto
关键字的不足。
shift与选项处理
我们在前面曾简短的提及shift
命令,它用来处理命令行参数的时候,一次向左位移一位(或更多位)。在执行shift
之后,原来的$1
就会消失,以$2
的旧值取代,$2
的新值即为$3
的旧值,以此类推,而$#
的值也会逐次减少。shift
还接受一个可选的参数,也就是可以执行一次要移动几位:默认为1.
通过结合while
、case
、break
以及shift
,可以做些简单的选项处理,如下所示:
#将标志变量设置为空值
file= verbose= quiet= long=
while[ $# -gt 0 ]
do
case $1 in #检查第一个参数
-f) file=$2
shift #移位退出“-f”,使得结尾的shift得到在$2里的值
;;
-v) verbose=true
quiet=
;;
-q) quiet=true
verbose=
;;
-l) long=true
;;
--) shift #传统上,以--结束选项
break
;;
-*) echo $0: $1: unrcongnized option >&2
;;
*) break; #无选项参数,在循环中跳出
;;
esac
shift #设置下一个重复
done
在此循环结束后,不同的标志变量都会设置,且可以使用test
或case
测试。任何剩下的无选项参数都仍然可利用,以便在$@
中做进一步的处理。
getopts
函数简化了选项处理。它能理解POSIX选项中将多个选项字母组织到一起的用法,也可以用来遍历整个命令行参数,一次一个参数。
getopts
的第一个参数是列出合法选项字母的一个字符串。如果选项字母后面跟着冒号,则表示该选项需要一个参数,此参数是必须提供的。一旦遇到这样的选项,getopts
会放置参数值到变量OPTARG
中。另一个变量OPTIND
包含下一个要处理的参数的索引值。Shell会把该变量初始化为1。
getopts
的第二个参数为变量名称,在每次getopts
调用时,该变量会被更新;它的值是找到的选项字母。当getopts
找到不合法的选项时,它会将此变量设置为一个问号字符。我们以getopts
重写前面的例子:
# 设置标志变量为口
file= verboase= quiet= long=
while getopts f:vql opt
do
case $opt in #检查选项字母
f) file=$OPTARG
;;
v) verbose=true
quiet=
;;
q) quiet=true
verbose=
;;
l) long=true
;;
esac
done
shift $((OPTIND-1)) #删除选项,留下参数
你会发现三个明显差异。首先,在case
里的测试只是用在选项字母上,开头的减号被删除了。再者,针对--
的情况(case
)也不见了:因为getopts
已自动处理。最后也消失的就是针对不合法选项的默认情况:getopts
会自动显示错误信息。
不过一般来说,在脚本里处理错误会比使用getopts
的默认处理要容易。将冒号(:
)置于选项字符中作为第一个字符,可以使得getopts
以两种方式改变它的行为:首先,它不会显示任何错误信息;第二,除了将变量设置为问号之外,OPTARG
还包含了给定的不合法选项字母。以下便是选项处理循环的最后版本:
# 设置标志变量为空
file= verbose= quiet= long=
#开头的冒号,是我们处理错误的方式
while getopts :f:vql opt
do
case $opt in #检查选项字母
f) file=$OPTARG
;;
v) verbose=true
quiet=
;;
q) quiet=true
verbose=
;;
l) long=true
;;
'?') echo "$0: invalid option -$OPTARGE" >&2
echo "Usage: $0 [ -f file ] [ -vql ] [ files ...] " >&2
exit 1
;;
esac
done
shift $((OPTIND-1)) #删除选项,留下参数
函数
就像其他的程序语言一样,函数(function
)是指一段单独的程序代码。用以执行一些定义完整的单项工作。在大型程序里,函数可以在程序多个地方使用(调用)。
函数在使用之前必须先定义。这可通过在脚本的起始处,或是将它们放在另一个独立文件里且以点号(.
)命令来取用(source
)它们。定义方式如下所示:
# 等待用户登录 ——函数版
# wait_for_user ---等待用户登录
#
# 语法: wait_for_user user [sleeptime]
wait_for_user(){
until who | grep "$1 " > /dev/null
do
sleep $(2:-30);
done
}
函数被引用(执行)的方式与命令相同:提供函数名称与任何相对应的参数。wait_for_user
函数可以以两种方式被引用:
wait_for_user tolstoy #等待用户tolstoy,每30秒检查一次
wait_for_user tolstoy 60 #等待用户tolstoy,每60秒检查一次
在函数体中,位置参数($1、$2、...、$#、$*,以及$@
)都是函数的参数。
父脚本的参数则临时地被函数参数所掩盖(shadowed
)或隐藏。$0
依旧是父脚本的名称。当函数完成时,原来的命令行参数会恢复。
在Shell函数里,return
命令的功能与工作方式都与exit
相同:
answer_the_question(){
...
return 42
}
需注意的是:在Shell函数体里使用exit
,会终止整个Shell脚本!
因为return
语句会返回一个退出值给调用者,所以你可以在if
与while
语句里使用函数。举例来说,可使用Shell的函数架构以取代test
所执行的两个字符串的比较:
# equal ---比较两个字符串
equal(){
case "$1" in
"$2") return 0 ;; #两字符串匹配
esac
return 1 #不匹配
}
if equal "$a" "$b" ...
if ! equal "$c" "$d" ...
有一个项目在这里需要注意:在case
模式列表里使用双引号。这么做会强制该值视为字面上的字符串,而非Shell模式。不过在$1
上使用引号则无伤大雅,但在这里没有必要。函数也有像命令那样会返回整数的退出状态:零值表示成功,非零则为失败。如果要返回其他的值,函数应该设置一个全局性Shell变量,或是利用父脚本捕捉它(使用命令替换),显示其值
myfunc(){
...
}
...
x=$(myfunc "$@") #调用myfunc,并存储输出
案例:从输入文件中参数一个SGML/XML标签的排序列表。它仅在命令行所指定的一个你文件上运作。我们现在可以使用for
循环处理参数,并利用Shell函数封装管道,以利于处理多个文件。修改后的脚本如下
# 从多个文件中,产生SGML标签列表
#! /bin/bash
# 读取一个或多个命令行上所提供的含有像<tag>word</tag>这样的标记的
# HTML/SGML/XML文件,并将其以tab分隔列表内容为:
# 计数值 单词 标签 文件名
# 由小至大排序单词与标签
# 将输出产生至标准输出上。
#
# 语法:
# taglist xml-files
process() {
cat "$1" |
sed -e 's#systemitem *role="url"#URL#g' -e 's#/systemitem#/URL#' |
tr '(){}[]' '\n\n\n\n\n\n\n' |
egrep '>[^<>]+</' |
awk -F '[<>]' -v FILE="$1" \
'{ printf("%s-31s\t%-15s\t%s\n", $3, $2, FILE) }' |
sort |
uniq -c |
sort -k2 -k3 |
awk '{
print ($2 == Lst) ? ($0 "<---") :$0
Last=$2
}'
}
for f in "$@"
do
process "$f"
done
函数(至少在POSIX Shell里)没有提供局部变量。因此所有的函数都与父脚本共享变量;即,你必须小心留意不要修改父脚本里不期望被修改的东西,例如PATH
。不过这也表示其他状态是共享的,例如当前目录与捕捉信息。