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

news2024/12/29 10:01:58

文章目录

  • 前言
  • 变量与算术
    • 变量赋值与环境
    • 参数展开
      • 展开运算符
      • 位置参数
      • 特殊变量
    • 算术展开
  • 退出状态
    • 退出状态值
    • 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$4set -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执行第一组介于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 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字符串。完整的参考下表

运算符如果…则为真
stringstring不是null
-b filefile是块设备文件
-c filefile是字符设备文件
-d filefile是目录
-e filefile 存在
-f filefile为一般文件
-g filefile有设置它的setgid位
-h filefile是一符号连接
-L filefile是一符号连接(等同于-h)
-n stringstring为非null
-p filefile是一命名的管道(FIFO文件)
-r filefile是可读的
-S filefile是socket
-s filefile不是空的
-t n文件描述符n指向一终端
-u filefile有设置它的setuid位
-w filefile是可写入的
-x filefile是可执行的,或file是可被查找的
-z stringstring为null
s1 = s2字符串s1与s2相同
s1 != s2字符串s1与s2不同
n1 -eq n2整数n1等于n2
n1 -ne n2整数n1不等于n2
n1 -lt n2n1小于n2
n1 -gt n2n1大于n2
n1 -le n2n1小于或等于n2
n1 -ge n2n1大于或等于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字符串。例如
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 ]
then
    echo Usage: finduser username >&2
    exit 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选项的程序代码
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的whileuntil循环,与传统程序语言循环类似。语法为:

while condition          until condition
do                       do
    statements                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
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.

通过结合whilecasebreak以及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

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

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语句会返回一个退出值给调用者,所以你可以在ifwhile语句里使用函数。举例来说,可使用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。不过这也表示其他状态是共享的,例如当前目录与捕捉信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/50953.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android: SimpleAdapter+GridView 简单图片展示

1&#xff1a;原理解析&#xff1a; 一个xml放总布局&#xff0c;一个xml放适配器要加载的模板&#xff08;我喜欢这样理解&#xff09;&#xff1b; java中写适配事件&#xff1b; 2&#xff1a;目录&#xff1a; 3&#xff1a;主布局&#xff1a;最重要的是要放一个GridView …

[附源码]Python计算机毕业设计Django港口集团仓库管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Google单元测试框架gtest之官方sample笔记3--值参数化测试

1.7 sample7--接口测试 值参数不限定类型&#xff0c;也可以是类的引用&#xff0c;这就可以实现对类接口的测试&#xff0c;一个基类可以有多个继承类&#xff0c;那么可以测试不同的子类功能&#xff0c;但是只需要写一个测试用例&#xff0c;然后使用参数列表实现对每个子类…

m基于可见光通信系统的RFID接口过程以及ALOHA防碰撞算法的matlab仿真

目录 1.算法描述 2.matlab算法仿真效果 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 射频识别技术(Radio Frequency Identification&#xff0c;RFID)是一种非接触式自动识别技术&#xff0c;与传统的识别方式相比&#xff0c;它无需直接接触、无需光学可视、无需人工干预即…

产品经理的七大定律的总结

最近学习了产品经理的七大定律&#xff0c;这些设计准则都基于人类心理学&#xff1a;人们如何感知、学习、推理、记忆&#xff0c;以及把意图转换为行动。 1、菲茨&#xff08;Paul Fitt&#xff09;定律 菲茨定律的对于产品设计时的启示&#xff1a; 1&#xff09;按钮等可…

SpringBoot 3.0 新特性,内置声明式HTTP客户端

http interface 从 Spring 6 和 Spring Boot 3 开始&#xff0c;Spring 框架支持将远程 HTTP 服务代理成带有特定注解的 Java http interface。类似的库&#xff0c;如 OpenFeign 和 Retrofit 仍然可以使用&#xff0c;但 http interface 为 Spring 框架添加内置支持。 什么是…

steam deck科普、上手教程及模拟器配置指南

steam_deck前言 早在2021年得时候&#xff0c;坊间就开始流传steam deck这个东西要问世了。但是中途跳了几次票&#xff0c;直到2022年2月&#xff0c;第一批steam deck才正式面向大众玩家。在熟悉steam deck之前&#xff0c;我们有必要了解如下的知识: Steam 准确来说&…

G1D27-deberta右键创建md文档

回家啦&#xff01;&#xff01;&#xff01;中午的炒饭太好吃了&#xff01;&#xff01;吃的好撑&#xff01;&#xff01;回家后和mm去了超市&#xff0c;买了冰淇淋、薯片和水果&#xff0c;好开心&#xff01;&#xff01;&#xff01; 下午睡了一会觉&#xff0c;真的好舒…

Spring MVC处理用户请求的完整流程

Spring MVC 框架是高度可配置的&#xff0c;包含多种视图技术&#xff0c;例如 JSP 技术、Velocity、Tiles、iText 和 POI。 Spring MVC 框架并不关心使用的视图技术&#xff0c;也不会强迫开发者只使用 JSP 技术&#xff0c;但教程中使用的视图是 JSP&#xff0c;本节主要介绍…

猿如意 | 带你手把手安装 Visual Studio Code

目录 一、什么是猿如意 二、借助猿如意安装Visual Studio Code 1、安装猿如意 2、安装Visual Studio Code 三、总结 一、什么是猿如意 猿如意是CSDN推出来的一款面向开发者的工具&#xff0c;他能够帮助开发者&#xff0c;找到自己心仪的开发工具提高自己的开发效率。 目标…

华为云两台机器内网互联

文章目录1. 前言2. ping公网ip3. 不同账号需要在同一大区4. 创建虚拟私有云5. 更换服务器所属的VPC网段6. 创建对等连接7. 填写对端项目ID和对端VPC ID8. 配置对等连接9. 添加对等连接路由10. 测试是否联通11. 后记1. 前言 最近在华为云买了两台低配Linux机器&#xff0c;35一…

聚焦出海 长城汽车50多国家和地区经销商集团齐聚泰国车博会

11月30日&#xff0c;长城汽车携新能源豪华阵容登陆第39届泰国国际汽车博览会&#xff08;简称“泰国车博会”&#xff09;。以“GWM Light the Future”&#xff08;长城汽车点亮未来&#xff09;为参展主题&#xff0c;长城汽车旗下中大型商务豪华SUV坦克500 HEV量产版、欧拉…

[附源码]Python计算机毕业设计SSM留守儿童管理平台(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

英文Paper写作如何正确掌握质量情况?

在国外留学的学子们要想完成高质量高水平的作业&#xff08;assignment与Paper&#xff09;&#xff0c;那么首先就要在心里想想这些基本必要的问题&#xff0c;等有了这些问题的轮廓之后&#xff0c;相信留学的朋友都能够写出满意自足的Paper或assignment。 1&#xff0e;Why …

TypeError: can only concatenate list (not “int“) to list

参考 TypeError: can only concatenate list (not "int") to list - 云社区 - 腾讯云 观察是否将列表和非列表的类型相连。 入队enqueue_op 5会报错&#xff0c;改成乘5就不会了。

Springboot整合策略模式概念->使用场景->优缺点->企业级实战

一、前言 策略模式可能是在工作中使用最多的&#xff0c;也是在面试中最常提到的&#xff0c;代码重构和优化的必备&#xff01; 小编之前也是一直说&#xff0c;其实没有真正的实战&#xff1b;最近有了机会实战了一下&#xff0c;来分享一下使用心得和在企业级的使用&#x…

Mac装机清理工具CleanMyMac2022最新版功能介绍

从最初开始下载CleanMyMac开始&#xff0c;CMM一直在提供智能的清理&#xff0c;从颇受小白用户喜爱的自动清理特性开始&#xff0c;仅需一键即可快速而安全地清理系统各角落垃圾&#xff0c;释放宝贵硬盘空间&#xff0c;CMM或许可以说是是 Mac上最知名的系统清理工具了。 Cl…

java EE初阶 —— 线程的安全问题

文章目录1.线程安全1.1 代码体现线程的不安全1.2 线程安全问题分析1.3 产生线程安全问题的原因1.4 线程安全问题的解决办法1.5 synchronized 的使用方法1.线程安全 多线程的抢占式执行&#xff0c;带来的随机性是线程安全问题的罪魁祸首&#xff0c;万恶之源。 如果没有多线程…

国内表格软件-FineReport Count函数

1. 概述 1.1 函数作用 计算数组或数据区域中所含项的个数&#xff0c;例如统计「地区数」和「销售员个数」&#xff0c;如下图所示&#xff1a; 也可与其他函数嵌套使用&#xff0c;例如进行「条件计数」&#xff0c;计算除孙林以外的销售员个数&#xff0c;如下图所示&#x…

Lactoferrin-PEG-alginate 乳铁蛋白-聚乙二醇-海藻酸钠

产品名称&#xff1a;乳铁蛋白-聚乙二醇-海藻酸钠 英文名称&#xff1a;Lactoferrin-PEG-alginate 纯度&#xff1a;95% 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 外观:固体或粘性液体&#xff0c;取决于分子量 PEG分子量可选&#xff1a;350、550、750、1k、…