文章目录

  • 前言
  • 变量与算术
    • 变量赋值与环境
    • 参数展开
      • 展开运算符
      • 位置参数
      • 特殊变量
    • 算术展开
  • 退出状态
    • 退出状态值
    • 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逐渐减少$#,循环将会终止
docase $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 ....]
thenstatements-if-true-1
[elif pipeleine[pipeline ...]
thenstatements-if-true-2
...]
[elsestatement-if-all-else-fails]
fi

(方括号表示的是可选部分,并非逐字输入)

以我们手边的例子来看,你应该大致猜得到它的工作方式:Shell执行第一组介于ifthen之间的语句块。如果最后一条执行的语句成功地退出,它便执行statements-if-true-1,否则,如果有elif,它会尝试下一组语句块。如果最后一条语句成功地退出,则会执行statements-if-true-2.它会以这种方式继续,执行相对应的语句块,直到它碰到一个成功退出的命令为止。

如果ifelse语句里没有一个为真,并且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 commanda second commandand a third command
}

花括号将所有命令语句在一起,只有在some_command成功时它们才会被执行。使用if可以让它更为简洁:

if some_command
thenone commanda second commandand 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" ]
thenecho $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. > &2exit 1
fi

>&2就是把结果输出到和标准错误一样;之前如果有定义标准错误重定向到某个file文件,那么标准输出也重定向到这个file文件。其中&的意思,可以看成“The same as”、的意思。

下面是几个使用test的诀窍:

  • 需要参数

    • 由于这个原因,所有的Shell变量展开都应该以引号括起来,这样test才能接手一个参数——即使它已变为null字符串。例如
if [ -f "$file "] .... #正确
if [ -f $file  ]  ...  #不正确
  • 字符串比较是很微妙的

    • 特别是字符串值为空,或是开头带有一个减号时,test命令就会被混淆。因此有了一种比较难看不过广为使用的方式:在字符串前面前置字母XX的使用是随意的,这是传统用法).

    • 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 ]
thenecho Usage: finduser username >&2exit 1
fi
who | grep $1

case语句

如果你需要通过多个数值来测试变量,可以将一系列ifelif测试搭配test一起使用:

if [ "X$1" = "X-f" ]
then ... #针对 -f 选项的程序代码
elif [ "X$1" = "X-d" ] || [ "X$1" = "X--directory" ] #允许长选项
then... #针对-d选项的程序代码
elseecho $1: unkown option >&2exit 1
fi

不过这么做的时候写起来很不顺手,也很难阅读。相对地,Shell的case结构应该用来进行模式匹配:

case $1 in
-f)...  #针对 -f选项的程序代码;;
-d | --directory)  #允许长选项...    #针对 -d选项的程序代码;;
*)echo $1: unknown option >&2exit 1# 在 “esac”之前的 ;;形式是一个好习惯,不过并非必要
esac

这里我们看到,要测试的值出现在case与in之间。将值以双引号括起来虽然并非必要,但也无妨。要测试的值,根据Shell模式的列表依次测试,发现匹配的时候,便执行相应的的程序代码,直至;;为止。可以使用多个模式,只要|字符加以分隔即可,这种情况下称为“or(或)”。模式里会包含任何Shell通配符,且变量、命令与算术替换会在它用作模式匹配之前在此值上被执行。

你可能会觉得在每个模式列表之后的不对称的右圆括号有点奇怪;不过这也是Shell语言里不对称定界符的唯一实例。

最后的*模式是传统用法,但非必须的,它是作为一个默认的情况。这通常是在你要显示诊断信息并退出时使用。正如我们前面提及的,最后一个情况不再需要结尾的;;,不过加上它会是比较好的形式。

循环

除了if与else语句之外,还有Shell的循环结构也是非常好用的工具。

for循环

for循环用于重复整个对象列表,依次执行每一个独立对象循环内容。对象可能是命令行参数、文件名或是任何可以以列表格式建立的东西。

现在我们假定,比较可能出现的情况应该拥有一些XML文件,再由这些XML文件集结成小册子。在此情况下,我们要做的应该是改变所有这些XML文件。所以for循环足以适合这一情况:

for i in atlbrochure*.xml
doecho $imv $i $i.oldsed 's/Atlanta/&, the capital of the South' < $i.old > $i
done

该循环将每个原始文件备份为副文件名为.old的文件,之后再使用sed处理文件以建立新文件。这个程序也显示文件名,作为执行进度的一种指示,这在有许多文件要处理时会有很大的帮助。

for循环里的in列表(list)是可选的,如果省略,Shell循环会遍历整个命令行参数。这就好像你已经输入了for i in "$@":

for i  #循环通过命令行参数
docase $i in-f) ...;;...esac
done

while与until循环

Shell的whileuntil循环,与传统程序语言循环类似。语法为:

while condition          until condition
do                       dostatements                statements
done                     done

至于if语句,condition可以是简单的命令列表,或者是包含&&||的命令。

whileuntil唯一的不同之处在于,如何对待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
dosleep 30
done

你可以将管道放入到while循环中,用来重复处理每一行的输入,如下所示:

产生数据 |while read name rank serial_nodo...done

以上述例子来说,while循环的条件所使用的命令一直是read。后面会进行举例同时会告诉你还可以使用管道将循环输出传递给另一个命令。

break与continue

并非所有Shell里的东西都是直接来自Algol68.Shell也从C借用了break与continue命令。这两个命令分别用来退出循环,或跳到循环体的其他地方。

#等待特定的用户登录,每30秒确认一次
printf "Enter username: "
read user
while true
doif who | grep "$user" > /dev/nullthen break;fisleep 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.

通过结合whilecasebreak以及shift,可以做些简单的选项处理,如下所示:

#将标志变量设置为空值
file=  verbose=  quiet=  long=
while[ $# -gt 0 ]
docase $1 in    #检查第一个参数-f)     file=$2shift    #移位退出“-f”,使得结尾的shift得到在$2里的值;;-v)     verbose=truequiet=;;-q)     quiet=trueverbose=;;-l)     long=true;;--)     shift      #传统上,以--结束选项break;;-*)    echo $0: $1: unrcongnized option >&2;;*)     break;     #无选项参数,在循环中跳出;;esacshift              #设置下一个重复
done

在此循环结束后,不同的标志变量都会设置,且可以使用testcase测试。任何剩下的无选项参数都仍然可利用,以便在$@中做进一步的处理。

getopts函数简化了选项处理。它能理解POSIX选项中将多个选项字母组织到一起的用法,也可以用来遍历整个命令行参数,一次一个参数。

getopts的第一个参数是列出合法选项字母的一个字符串。如果选项字母后面跟着冒号,则表示该选项需要一个参数,此参数是必须提供的。一旦遇到这样的选项,getopts会放置参数值到变量OPTARG中。另一个变量OPTIND包含下一个要处理的参数的索引值。Shell会把该变量初始化为1。

getopts的第二个参数为变量名称,在每次getopts调用时,该变量会被更新;它的值是找到的选项字母。当getopts找到不合法的选项时,它会将此变量设置为一个问号字符。我们以getopts重写前面的例子:

# 设置标志变量为口
file=  verboase=  quiet=  long=
while getopts f:vql opt
docase $opt in         #检查选项字母f)    file=$OPTARG;;v)    verbose=truequiet=;;q)    quiet=trueverbose=;;l)    long=true;;esac
done
shift $((OPTIND-1))  #删除选项,留下参数

你会发现三个明显差异。首先,在case里的测试只是用在选项字母上,开头的减号被删除了。再者,针对--的情况(case)也不见了:因为getopts已自动处理。最后也消失的就是针对不合法选项的默认情况:getopts会自动显示错误信息。

不过一般来说,在脚本里处理错误会比使用getopts的默认处理要容易。将冒号(:)置于选项字符中作为第一个字符,可以使得getopts以两种方式改变它的行为:首先,它不会显示任何错误信息;第二,除了将变量设置为问号之外,OPTARG还包含了给定的不合法选项字母。以下便是选项处理循环的最后版本:

# 设置标志变量为空
file=  verbose=  quiet=  long=
#开头的冒号,是我们处理错误的方式
while getopts :f:vql opt
docase $opt in        #检查选项字母f)    file=$OPTARG;;v)    verbose=truequiet=;;q)    quiet=trueverbose=;;l)    long=true;;'?')    echo "$0: invalid option -$OPTARGE" >&2echo "Usage: $0 [ -f file ] [ -vql ] [ files ...] " >&2exit 1;;esac
done
shift $((OPTIND-1)) #删除选项,留下参数

函数

就像其他的程序语言一样,函数(function)是指一段单独的程序代码。用以执行一些定义完整的单项工作。在大型程序里,函数可以在程序多个地方使用(调用)。

函数在使用之前必须先定义。这可通过在脚本的起始处,或是将它们放在另一个独立文件里且以点号(.)命令来取用(source)它们。定义方式如下所示:

# 等待用户登录 ——函数版
# wait_for_user ---等待用户登录
#
# 语法: wait_for_user user [sleeptime]
wait_for_user(){until who | grep "$1 " > /dev/nulldosleep $(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语句会返回一个退出值给调用者,所以你可以在ifwhile语句里使用函数。举例来说,可使用Shell的函数架构以取代test所执行的两个字符串的比较:

# equal ---比较两个字符串
equal(){case "$1" in"$2")    return 0 ;; #两字符串匹配esacreturn 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-filesprocess() {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 "<---") :$0Last=$2}'
}
for f in "$@"
doprocess "$f"
done

函数(至少在POSIX Shell里)没有提供局部变量。因此所有的函数都与父脚本共享变量;即,你必须小心留意不要修改父脚本里不期望被修改的东西,例如PATH。不过这也表示其他状态是共享的,例如当前目录与捕捉信息。

Shell脚本学习指南(五)——变量、判断、重复动作相关推荐

  1. Shell脚本学习指南(三)——文本处理工具

    文章目录 排序文本 行的排序 以字段的排序 文本块排序 sort的效率 sort的稳定性 sort小结 删除重复 重新格式化段落 计算行数.字数以及字符数 打印 打印技术的演化 其他打印软件 提取开头 ...

  2. linux shell脚本学习指南,shell脚本学习指南[二](Arnold Robbins Nelson H.F. Beebe著)

    该进入第四章了,刚才看到一个帖子标题:我空有一身泡妞的好本领,但可惜自己是个妞.汗-这个...音乐无国界嘛,这个不应该也没性别界么? 第四章文本处理工具 书中先说明了以下排序的规则,数值的就不用说了, ...

  3. Shell脚本学习指南(七)——产生脚本

    文章目录 前言 路径查找 软件构建自动化 前言 本篇,我们将进一步处理更复杂的工作.我们认为这里举出的例子都是一般用得到的工具,它们每一个都截然不同,且在大多数UNIX工具集里也没有. 在篇中的程序, ...

  4. Shell脚本学习指南 - 第二章入门篇

    shell脚本的第一行#! #! /bin/awk -f 内核会扫描文件开头的#!后面内容,跳过所有空白符号,寻求可以用来执行程序的解释器的full path和option(option后面的空格会识 ...

  5. Shell脚本学习指南-查找与替换篇

    一.查找与替换 编写 Shell脚本时经常用到的两个基本操作: 1.文本查找 (searching) - 寻找含有特定文本的行 2.文本替换(substitution)- 更换找到的文本 可以使用固定 ...

  6. Shell脚本学习指南(六)——输入/输出、文件与命令执行

    文章目录 前言 标准输入.标准输出与标准错误输出 使用read读取行 关于重定向 额外的重定向运算符 文件描述符处理 printf的完整介绍 波浪号展开与通配符 波浪号展开 使用通配符 命令替换 为h ...

  7. Shell脚本学习指南(二)——查找与替换

    文章目录 前言 查找文本 简单的grep 正则表达式 什么是正则表达式 POSIX方括号表达式 基本正则表达式 匹配单个字符 后向引用 单个表达式匹配多字符 文本匹配锚点 BRE运算符优先级 匹配单个 ...

  8. 《Shell脚本学习指南》读书笔记

    P30-31 软件工具的原则 1.一次做好一件事 2.处理文本行,不要处理二进制数据 3.使用正则表达式 4.默认使用标准输入/输出 5.避免喋喋不休 6.输出格式必须与可接受的输入格式一致 7.让工 ...

  9. Shell脚本学习-阶段五-MYSQL101条调优

    文章目录-Shell阶段五-MYSQL调优技巧 前言 101个MySQL调试和优化技巧 MySQL 服务器硬件和操作系统调节: MySQL 配置: MySQL模式优化: 查询优化: MySQL 备份过 ...

最新文章

  1. Python入门100题 | 第062题
  2. SQLite 日期 时间
  3. VS2010 关于.wav音频文件播放
  4. JS高级——JSON、数据存储学习笔记
  5. 每个前端工程师都应该懂的前端性能优化总结:
  6. 操作系统锁的实现方法有哪几种_一文带你彻底了解同步和锁的本质
  7. 玩转 SpringBoot 2.x 之 RedisTemplate 操作
  8. python 解除excel的密码_我帮公司财务写了个“群发工资条”的Python脚本!
  9. Codeforces Round #499 (Div. 2) C. Fly(数学+思维模拟)
  10. vivo手机计算机错误怎么弄,VIVO手机无法连接电脑怎么办?
  11. intel wifi 5100agn linux驱动,intel5100agn网卡驱动下载
  12. 大数据核心技术是什么 怎么掌握Hadoop知识
  13. processson的文件数量已达到上限如何删除老文件新建新的文件创建流程图
  14. 谷歌浏览器 Chrome 安装 Tampermonkey 油猴插件的方法
  15. 电话号码被标记了怎么取消?标记取消最强攻略来了
  16. UE4使用OpenCV插件调用电脑摄像头
  17. React Native三端同构
  18. 郭依婷——大学生的创业故事
  19. C语言实现不同进制之间的转换
  20. 这是我见过最完美的“docker学习宝典”,阿里云高工熬夜手写,服!

热门文章

  1. USB。标准键盘的报告描述符解析。
  2. linux 线程与进程的简单区别
  3. JAVA - JDK、JVM、JRE 是什么?
  4. python3通过CookieJar与urllib模拟登陆人人网
  5. 数仓、数湖、仓湖一体
  6. java写雷霆战机小游戏
  7. LeetCode 297 Serialize and Deserialize Binary Tree
  8. 开发微信小程序火车订票选座系统毕业设计
  9. [Iphone开发小记] UIColor 的RGBA定义颜色 (colorWithRed)
  10. 二手房中介费收费标准 二手房中介费能优惠吗