「生活可以更简单, 欢迎来到我的开源世界」
  1. 1. 语法
    1. 1.1 变量
      1. 变量的类型
        1. 无类型变量
        2. 有类型变量
      2. 变量赋值
      3. 变量使用
      4. 间接变量引用
      5. 引用
    2. 1.2 打印
    3. 1.3 运算
      1. 使用expr执行算术运算
      2. 使用[]执行算术
      3. 双圆括号执行算术运输
      4. bc运算器
    4. 1.4 控制
      1. 数值比较
      2. 整数比较运算符
      3. 字符串运算符
      4. 文件操作符
      5. 逻辑运算符
      6. 算术运算符
      7. 位运算符
      8. 测试命令test
      9. 退出状态
      10. if条件判断
    5. 1.5 循环
      1. for循环
      2. while 循环
      3. until 循环
      4. 循环嵌套
      5. 循环控制符break和continue
      6. select结构
    6. 1.6 容器
      1. 数组定义
    7. 4.3.移动变量
    8. 4.4.处理选项
      1. (1)查找选项
      2. (2)getopt命令
      3. (3)更高级的getopts命令
    9. 4.5.将选项标准化
    10. 4.6.获得用户输入
      1. (1)基本的读取
      2. (2)超时
      3. (3)隐藏方式读取
      4. (4)从文件中读取
shell编程基础
2020-11-19

原文:

1. 语法

1.1 变量

变量是脚本语言的核心,shell脚本又是无类型的。变量本质上存储数据的一个或多个计算机内存地址,分为

变量的类型

shell是弱类型语言所以不需要使用类型限定,并且变量可以修改类型。

下面的例子定义了一个字符串类型的str变量,之后修改为数值类型

注意点:变量等号两边不能有空格出现

modao@modao-hp:~$ str="abc"
modao@modao-hp:~$ echo $str
abc
modao@modao-hp:~$ echo ${str}
abc
modao@modao-hp:~$ str=123
modao@modao-hp:~$ echo $str
123
modao@modao-hp:~$ echo ${str}
123
无类型变量
  1. C中定义变量需要声明整型、浮点型、字符型等,而shell脚本变量却是无类型的。shell不支持浮点型只支持整型和字符型,同时字符型还具有一个整型值(判断标准:变量中只包含数字是数值型其他是字符串)。

  2. 位置参数:从命令行向shell脚本传递参数,$0表示脚本的名字,$1代表第一个参数,以此类推。从​${10}开始参数号需要用花括号括起来。

    特殊的位置参数:

    变量名 作用
    $0 当前脚本的名字
    $n 传递给脚本或者函数的参数,n表示第几个参数
    $# 传递给脚本或函数的参数个数
    $* 传递给脚本或函数的所有参数
    $@ 传递给脚本或者函数的所有参数
    $$ 当前shell脚本进程的PID
    $? 函数返回值,或者上个命令的退出状态

    $* 和 $@ 的区别

    相同:$* 和 $@ 都表示传递给函数或脚本的所有参数不被双引号(“ “)包含时,都以”$1” “$2” … “$n” 的形式输出所有参数。
    区别:当它们被双引号(“ “)包含时,”$*” 会将所有的参数作为一个整体,以”$1 $2 … $n”的形式输出所有参数;”$@” 会将各个参数分开,以”$1” “$2” … “$n” 的形式输出所有参数。

  3. 内部变量:指能够对bash shell脚本行为产生影响的变量,属于环境变量的范畴。

    变量名 作用
    $BASH BASH记录了shell的路径,通常是/bin/bash。内部变量SHELL是通过BASH的值确定当前Shell的类型
    $BASH_ENV BASH的启动文件
    $BASH_VERSINFO 是一个包含6个元素的数组,这些元素用于表示bash的版本信息。
    BASH_VERSINFO[0]表示bash shell的主版本号,BASH_VERSINFO[1]表示shell的次版本号,BASH_VERSINFO[2]表示shell的补丁级别,BASH_VERSINFO[3]表示shell的编译版本,BASH_VERSINFO[4]表示shell的发行状态,BASH_VERSINFO[5]表示shell的硬件架构。
    $BASH_VERSION linux系统的bash shell版本包含主次版本、补丁级别、编译版本和发行状态,即BASH_VERSINFO数组取值为0~4。
    $EDITOR 脚本所调用的默认编辑器
    $EUID 当前有效的用户ID
    $FUNCNAME 当前函数名
    $GROUPS 当前用户所属组,linux的一个用户可同时包含在多个组内,GROUPS是一个数组记录了当前用户所属的所有群组号。管理用户组的文件是/etc/group,格式:群组名:加密后的组口令:群组号:组成员,组成员(组成员列表)。
    $HOME 当前用户家目录
    $HOSTTYPE
    $MACHTYPE
    都用于记录系统的硬件架构,它们与BASH_VERSINFO[5]等值
    $LINENO 当前行号
    $OSTYPE 记录操作系统类型,linux系统中,$OSTYPE=linux。
    $PATH PATH路径
    $PPID 当前shell进程的父进程ID
    $PWD 当前工作目录
    $SECONDS 当前脚本运行秒数
    $TMOUT 不为0时,超过指定的秒将退出shell
    $UID 当前用户ID
    $DIRSTACK 它显示目录栈的栈顶值。linux目录栈用于存放工作目录,便于程序员手动控制目录的切换,bash shell定义了两个系统命令pushd(将某目录压入目录栈并将当前工作目录切换到入栈的目录)和popd(将栈顶目录弹出并将当前工作目录切换到栈顶目录)来维护目录栈。
    DIRSTACK记录栈顶目录值,初值为空。linux还有一个命令dirs用于显示目录栈的所有内容。
    $GLOBIGNORE 它是由冒号分隔的模式列表,表示通配时忽略的文件名集合。一旦GLOBIGNORE非空,shell会将通配得到的结果中符合GLOBIGNORE模式中的目录去掉。例如ls a*列出当前目录以a开头的文件,设置GLOBIGNORE=“ar*”,再次执行ls a*将剔除以ar开头的文件。
    $HOSTNAME HOSTNAME记录了主机名,linux主机名是网络配置时必须要设置的参数,可在/etc/sysconfig/network文件中设置主机名。/etc/hosts文件用于设定IP地址和主机名之间的对应关系,可快速从主机名查找IP地址。
    $REPLY REPLY变量与read和select命令有关。read用于读取标准输入(stdin)的变量值,read variable将标准输入存储到variable变量中,而select将读到的标准输入存储到REPLY变量中。
    $SECONDS SECONDS记录脚本从开始执行到结束所耗费的时间(单位为秒)。调试性能时比较有用。
    $SHELLOPTS 它记录了处于开状态的shell选项列表,它是一个只读变量。Shell选项用于改变Shell的行为,Shell选项有开和关两种状态,set命令用于打开或关闭选项。set -o optionname(打开名为optionname选项),set +o optionname(关闭名为optionname选项)。比如打开interactive(交互模式运行)可以使用set -o interactive或set -i等价。Shell选项有很多。
    $SHLVL 记录Shell嵌套的层次,启动第一个shell时,$SHLVL=1,若在这个Shell中执行脚本,脚本中的SHLVL为2,脚本中再执行子脚本,SHLVL就会递增。
    $TMOUT 用于设置Shell的过期时间,TMOUT不为0时,shell会在TMOUT秒后将自动注销,TMOUT放在脚本中可以规定脚本的执行时间。

    select脚本:

    #!/bin/bash
    # "#?"提示符由shell提示符变量PS3进行设置(#?是其默认值)。
    # 修改export PS3="your choice:"
    # REPLY变量值为用户选择的序号,var变量为REPLY序号所对应的字符串。
    echo "Pls. choose your profession?"
    select var in "Worker" "Doctor" "Teacher" "Student" "Other"
    do
    echo "The \$REPLY is $REPLY."
    echo "Your preofession is $var."
    break
    done
有类型变量

Shell变量一般是无类型的,bash shell提供了declare和typeset两个命令用于指定变量的类型(它们完全等价)。

declare [选项] 变量名,有6个选项。

选项名 意义
-r 将变量设置为只读属性
-i 将变量定义为整形数
-a 将变量定义为数组
-f 显示此脚本前定义过的所有函数名及其内容
-F 仅显示此脚本前定义过的所有函数名
-x 将变量声明为环境变量

declare命令-r选项将变量设置成只读属性,与readonly命令一样,变量值不允许修改。

declare命令-x选项将变量声明为环境变量,相当于export,但declare -x允许声明环境变量同时给变量赋值,而export不支持。

declare -i将变量定义为整型数,不能再按字符串形式处理改变量(和let命令进行算术运算一样,expr命令可以替换let命令)。

#!/bin/bash
variable1=2009
variable2=$variable1+1
echo "variable2=$variable2"
let variable3=$variable1+1
echo "variable3=$variable3"
declare -i variable4
variable4=$variable1+1
echo "variable4=$variable4"

变量赋值

变量使用

modao@modao-hp:~$ dir1=$(pwd)
modao@modao-hp:~$ echo $dir1
/home/modao
modao@modao-hp:~$ dir2=`pwd`
modao@modao-hp:~$ echo $dir2
/home/modao

间接变量引用

该引用不是将变量引起来,而是理解为:如果第一个变量的值是第二个变量的名字,从第一个变量的名字引用第二个变量的值就称为间接变量引用。

bash shell提供了两种格式实现间接变量引用:

使用间接变量引用实现数据库表格的查找:

#!/bin/bash

S01_name="Li Hao"
S02_name="Zhang Ju"
S03_name="Zhu Lin"

PS3='Pls. select the number of student:'
select stunum in "S01" "S02" "S03"
do
name=${stunum}_name #name变量的值是S01_name,S01_name是另一个变量的名字
#从变量name得到变量S01_name的值,即为间接变量引用
echo "NAME:${!name}"
break
done

引用

引用指将字符串用引用符号引起来,以防止特殊字符被shell脚本重解释为其他意义。

shell中定义了四种引用符号。

  1. 双引号:美圆符号($)、反引号(`)和反斜线(\)仍被解释为特殊意义,其它字符均解释为字面意义,利用双引号引用变量能防止字符串分割,而保留变量中的空格。因此双引号的引用方式称为部分引用

  2. 单引号:除单引号本身之外都解释为字面意义,不再具备引用变量的功能,单引号的引用方式称为全引用

  3. 反引号:反引号进行命令替换(将命令的标准输出作为值赋给某个变量),等价于$(),同时$()形式的命令替换是可以嵌套的。

    # 当前工作目录
    `pwd`
    $(pwd)

    反引号与$()在处理双反斜线时存在区别

    • 反引号将反斜线符号处理为空格

    • $()符号将其处理为单斜线符。

  4. 反斜线:表示转义,将屏蔽下一个字符的特殊意义,而以字面意义解析它。转义符除了屏蔽特殊字符的特殊意义外,加上一些字母能够表达特殊的含义(转义字符)。

1.2 打印

因为shell没有单步调试和其他功能强大的IDE,所以打印功能就经常使用到,此外打印功能还可以当做函数的返回值,比return作为函数的返回值功能更强大

shell 使用echo打印,内容默认输出到控制台中,echo可以打印字符串、变量、以及字符串中嵌入变量的混合内容

[更多内容见:echo详解]

modao@modao-hp:~$ str="abcd"
modao@modao-hp:~$ echo $str
abcd
modao@modao-hp:~$ echo "the str = $str"
the str = abcd

1.3 运算

使用expr执行算术运算

expr一般用于整数值计算和字符串的操作。

若expr的操作符是元字符(不是普通字符),需要用转义符将操作符的特殊含义屏蔽,进行数学运算,如expr 2014 \* 2

expr中操作符的两端必须有空格,否则不会执行数学运算,如expr 2014 - 2008

注意:*乘法运算符号需要转义

img

modao@modao-hp:~$ ans=$(expr 3 + 8)
modao@modao-hp:~$ echo $ans
11
modao@modao-hp:~$ ans=$(expr 3 - 8)
modao@modao-hp:~$ echo $ans
-5
modao@modao-hp:~$ ans=$(expr 3 \* 8)
modao@modao-hp:~$ echo $ans
24
modao@modao-hp:~$ ans=$(expr 24 / 8)
modao@modao-hp:~$ echo $ans
3

使用[]执行算术

[]执行算术比expr简单多了,并且*乘法运算符号不需要转义

modao@modao-hp:~$ ans=$[3 + 8]
modao@modao-hp:~$ echo $ans
11
modao@modao-hp:~$ ans=$[3 - 8]
modao@modao-hp:~$ echo $ans
-5
modao@modao-hp:~$ ans=$[3 * 8]
modao@modao-hp:~$ echo $ans
24
modao@modao-hp:~$ ans=$[24 / 8]
modao@modao-hp:~$ echo $ans
3

双圆括号执行算术运输

双圆括号即((…))格式。result=$((var1*var2));var1和var2执行乘法运算。

双圆括号可以使shell实现C语言风格的变量操作。 双圆括号实现五种C语言风格的运算:

#!/bin/bash
((a = 2014))
echo "The initial value of a is:$a"
((a++))
echo "After a++,the value of a is:$a"
((++a))
echo "After ++a,the value of a is:$a"
((a--))
echo "After a--,the value of a is:$a"
((--a))
echo "After --a,the value of a is:$a"

bc运算器

bash 不支持浮点运算,如果需要进行浮点运算,需要借助bc,awk 处理

bc是一种内建的运算器,是bash shell中最常用的浮点数运算工具,包括整型数和浮点数、数组变量、表达式、复杂程序结构和函数。

bc运算器支持的数学运算符号如下表:

img

bc运算器定义了内建变量scale用于设定除法运算的精度(默认scale=0)。

image-20210320145626875

scale设为4后,除法结果小数点后保留4位。bc -q可以使bc运算器不输出版本信息。

在shell中用bc运算器进行浮点数运算需要使用命令替换的方式。脚本中调用bc运算器一般格式:

variable=`echo "options; expression" | bc`
#!/bin/bash

var1=20
var2=3.14159
var3=`echo "scale=5; $var1 ^ 2" | bc`
var4=`echo "scale=5; $var3 * $var2" | bc`
echo "The area of this circle is:$var4"

bc运算器的指数运算计算var1变量的平方,scale=5输出结果的小数点后精确到第5位。

1.4 控制

数值比较

控制使用if/else/fi语法,典型的数值比较如下

modao@modao-hp:~$ if [[ 3 > 7 ]];then echo "yes"; else echo "no"; fi
no
modao@modao-hp:~$ if [ 3 > 7 ];then echo "yes"; else echo "no"; fi
yes
modao@modao-hp:~$ if [[ 3 -gt 7 ]];then echo "yes"; else echo "no"; fi
no
modao@modao-hp:~$ if [ 3 -gt 7 ];then echo "yes"; else echo "no"; fi
no

shell if条件判断中:双中括号与单中括号的区别

单中括号的情况:当使用if [ $1 = 1 ]时,如果变量“参数”的值为空,那么就if语句就变成了if [ = 1 ],这不是一个合法的条件。为了避免出现这种情况,必须给变量加上引号if [ "$1" == "1" ],这样即使是空变量也提供了合法的测试条件if [ " " == "1" ]

关于双中括号:

  • 双方括号提供了字符串比较的高级特性

  • 括号中可以定义一些正则表达式来匹配字符串

  • 注意不是所有的shell都支持双方括号!

    单中括号是比较基本的变量计算及数值比较的方法,一般情况下已经足够使用;双中括号是扩展的数值比较方法,里面的数值计算也相对来说复杂些。

    引申:

  1. 单括号和双括号的区别
  2. 单引号和双引号的区别

整数比较运算符

比较符 描述
n1 -eq n2 检查n1是否与n2相等
n1 -ge n2 检查n1是否大于或等于n2
n1 -gt n2 检查n1是否大于n2
n1 -le n2 检查n1是否小于或等于n2
n1 -lt n2 检查n1是否小于n2
n1 -ne n2 检查n1是否不等于n2

字符串运算符

Shell编程是严格区分大小写的,并注意空格的问题,运算符左右的空格不能少。

比较符 描述
str1 = str2 检查str1是否和str2相同
str1 != str2 检查str1是否和str2不同
str1 < str2 检查str1是否比str2小
str1 > str2 检查str1是否比str2大
-n str1
str1
检查str1的长度是否非0
-z str1 检查str1的长度是否为0

文件操作符

比较符 描述
-d file 检查file是否存在并是一个目录
-e file 检查file是否存在
-f file 检查file是否存在并是一个文件
-r file 检查file是否存在并可读
-s file 检查file是否存在并非空
-w file 检查file是否存在并可写
-x file 检查file是否存在并可执行
-L file 测试file是否符合化链接
-O file 检查file是否存在并属当前用户所有
-G file 检查file是否存在并且默认组与当前用户相同
file1 -nt file2 检查file1是否比file2新
file1 -ot file2 检查file1是否比file2旧

逻辑运算符

逻辑操作符 描述
! expression 如果expression为假,测试结果为真
expression1 -a expression2 如果expression1和expression2同时为真,则测试结果为真
expression1 -o expression2 如果expression1和expression2中有一个为真,则测试结果为真

用于测试多个条件是否为真或为假,也可使用逻辑非测试单个表达式。

表达式:expression1 -a expression1 -aexpression1(并不是所有的运算符都会被执行,只有表达式expression1为真,才会测试expression2为真。只有expression1和expression2都为真才会接着测试expression3是否为真)

表达式:expression1 -aexpression1 -aexpression1(只要expression1为真,就不用去测试表达式expression2和expression3。只有expression1为假时才会去判断表达式expression2和expression3。同样,只有expression2和expression3同时为假时才会去测试expression3)

算术运算符

运算符 描述
+ 加运算
- 减运算
* 乘运算
/ 除运算
% 取余运算
** 幂运算

使用let命令来执行算术运算,除法和取余运算过程中要注意除数不能为0,使用算术运算符无法对字符串、文件、浮点型数进行计算(浮点型操作,需要用到专门的函数)。

算术运算符与赋值运算符”=”联用,称为算术复合赋值运算符+=-=*=/=%=

自增自减运算符

自增自减运算符包括前置自增、前置自减、后置自增和后置自减。自增自减操作符的操作元只能是变量,不能是常数或表达式,且该变量值必须为整数型。

#!/bin/sh

num1=5

let "a=5+(++num1) "
echo "a=$a"

num2=5

let "b=5+(num2++) "
echo "b=$b"

数字常量

脚本或命令默认将数字以十进制的方式进行处理,当使用0作为前缀时表示八进制,当使用0x进行标记时表示十六进制,同时还可使用num#这种形式标记进制数。

#!/bin/sh

let "num1=40"
echo "num1=$num1"

let "num2=040"
echo "num2=$num2"

let "num3=0x40"
echo "num3=$num3"

位运算符

用于整数间的运算

img

位运算符同样可以同赋值运算符联用,组成复合赋值运算符

img

测试命令test

Linux中shell的测试命令,用于测试某种条件或某几种条件是否真实存在。

测试命令是判断语句和循环语句中条件测试的工具,对判断和运算符的比较测试有很大的帮助。

测试条件为真,返回一个0值;为假,返回一个非0整数值。

测试命令有两种方式

退出状态

Linux系统,每当命令执行完成后,系统返回一个退出状态。

状态值 含义
0 表示运行成功,程序执行没有遇到任何问题
1~125 表示运行失败,脚本命令、系统命令错误或参数传递错误
126 找到了改命令但无法执行
127 未找到要运行的命令
>128 命令被系统强行结束

exit命令格式:exit status(status在0~255之间),返回该状态值时伴随脚本的退出,参数被保存在shell变量$?

if条件判断

if、then、else语句用于判断给定的条件是否满足,并根据测试条件的真假来选择相应的操作。if/else仅仅用于两分支判断,多分支的选择时需要用到if/else语句嵌套、if/elif/else和case多分支选择判断结构。

if结构

#!/bin/sh
echo "Please input a integer:"
read integer1

if [ "$integer1" -lt 15 ]
then echo "The integer lower than 15."
fi

注意:测试条件后如果没有”;”则then语句要换行。

if/else结构

#!/bin/sh

echo "Please input the file which you want to delete:"
read file

if rm -f "$file"
then
echo "Delete the file $file sucessfully!"
else
echo "Delete the file $file failed!"
fi

if/else语句嵌套

可同时判断三个或三个以上条件,但要注意if与else配对关系,else语句总是与它上面最近的未配对的if配对。

#!/bin/bash
#提示用户输入分数(0-100)
echo "Please Input a integer(0-100): "
read score

#判断学生的分数类别
if [ "$score" -lt 0 -o "$score" -gt 100 ]
then
echo "The score what you input is not integer or the score is not in (0-100)."
else
if [ "$score" -ge 90 ]
then
echo "The grade is A!"
else
if [ "$score" -ge 80 ]
then
echo "The grade is B!"
else
if [ "$score" -ge 70 ]
then
echo "The grade is C!"
else
if [ "$score" -ge 60 ]
then
echo "The grade is D!"
else
echo "The grade is E!"
fi
fi
fi
fi
fi

if/elif/else结构

if/else嵌套在编程中很容易漏掉then或fi产生错误,而且可读性很差,因此引入if/elif/else结构针对某一事件的多种情况进行处理,fi只出现一次,可读性也提高了。

#!/bin/bash  

echo "Please Input a integer(0-100): "
read score

if [ "$score" -lt 0 -o "$score" -gt 100 ]
then
echo "The score what you input is not integer or the score is not in (0-100)."
elif [ "$score" -ge 90 ]
then
echo "The grade is A!"
elif [ "$score" -ge 80 ]
then
echo "The grade is B!"
elif [ "$score" -ge 70 ]
then
echo "The grade is C!"
elif [ "$score" -ge 60 ]
then
echo "The grade is D!"
else
echo "The grade is E!"
fi

case结构

case结构变量值依次比较,遇到双分号则跳到esac后的语句执行,没有匹配则脚本将执行默认值*)后的命令,直到;;为止。

case的匹配值必须是常量或正则表达式。

#!/bin/bash

echo "Please Input a score_type(A-E): "
read score_type

case "$score_type" in
A)
echo "The range of score is from 90 to 100 !";;
B)
echo "The range of score is from 80 to 89 !";;
C)
echo "The range of score is from 70 to 79 !";;
D)
echo "The range of score is from 60 to 69 !";;
E)
echo "The range of score is from 0 to 59 !";;
*)
echo "What you input is wrong !";;
esac

1.5 循环

三种常用的循环语句:for、while和until。while循环和for循环属于“当型循环”,而until属于“直到型循环”。

循环控制符:break和continue控制流程转向。

for循环

列表for循环

#!/bin/bash

for varible1 in {1..5}
#for varible1 in 1 2 3 4 5
do
echo "Hello, Welcome $varible1 times "
done
#!/bin/bash
sum=0

for i in {1..100..2}
do
let "sum+=i"
done

echo "sum=$sum"
#!/bin/bash

for file in $( ls )
# 也可一使用for file in *
# 通配符*产生文件名扩展,匹配当前目录下的所有文件。
#for file in *
do
echo "file: $file"
done
#!/bin/bash
# $#表示参数的个数,$@表示参数列表,$*则把所有的参数当作一个字符串显示。
echo "number of arguments is $#"
echo "What you input is: "
for argument in "$@"
do
echo "$argument"
done

不带列表for循环

由用户制定参数和参数的个数,与上述的for循环列表参数功能相同。

#!/bin/bash

echo "number of arguments is $#"
echo "What you input is: "
for argument
do
echo "$argument"
done

C语言格式的for循环

也被称为计次循环

num=0
for (( i = 0; i < 10; i++ )); do
num=$[$num + $i]
done
echo "result = ${num}"
## output:result = 45

for in 循环处理文件

data文件内容如下:

echo "=======循环for in======="

file="data"
IFS_OLD=$IFS
IFS=$'\n'
for line in $(cat $file)
do
echo "${line}"
done
IFS=${IFS_OLD}

# 输出:=======循环for in=======
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.
The quick brown fox jumps over the lazy dog.

while 循环

while表示条件满足执行循环

#!/bin/bash
sum=0
i=1
while(( i <= 100 ))
do
let "sum+=i"
let "i += 2"
done

echo "sum=$sum"
#!/bin/bash

echo "Please input the num(1-10) "
read num

while [[ "$num" != 4 ]]
do
if [ "$num" -lt 4 ]
then
echo "Too small. Try again!"
read num
elif [ "$num" -gt 4 ]
then
echo "To high. Try again"
read num
else
exit 0
fi
done

echo "Congratulation, you are right! "
# 使用用户输入的标志值来控制循环的结束(避免不知道循环结束标志的条件)。
#!/bin/bash

echo "Please input the num "
read num

sum=0
i=1

signal=0

while [[ "$signal" -ne 1 ]]
do
if [ "$i" -eq "$num" ]
then
let "signal=1"
let "sum+=i"
echo "1+2+...+$num=$sum"
else
let "sum=sum+i"
let "i++"
fi
done

控制的while循环求1~n的累加和,循环变量值小于100执行else累加同时循环变量加1,直到循环变量值等于100将标志值设置为1,并输出。

控制的while循环与结束标记控制的while循环的区别是用户无法确定无法确定结束标志,只能程序运行后确定结束标志。两者也可以相互转化。

#!/bin/bash

echo "number of arguments is $#"
echo "What you input is: "
while [[ "$*" != "" ]]
do
echo "$1"
shift
done

until 循环

until和while相反,表示条件不满足执行循环

j=1
line=""
until [[ j -eq 10 ]]; do
if [[ j -le i ]]; then
result=$(expr $i \* $j)
resultStr="$j X $i = $result"
line=${line}${resultStr}"\t"
fi
j=$(expr $j + 1)
done
echo -e ${line}

循环嵌套

一个循环体内又包含另一个完整的循环结构,在外部循环的每次执行过程中都会触发内部循环,for、while、until可以相互嵌套。

#!/bin/bash

for (( i = 1; i <=9; i++ ))
do

for (( j=1; j <= i; j++ ))
do
let "temp = i * j"
echo -n "$i*$j=$temp "
done

echo "" #output newline
done
#!/bin/bash

for ((i=1; i <= 9; i++))
do
j=9;
while ((j > i))
do
echo -n " "
let "j--"
done
k=1
while ((k <= i))
do
echo -n "*"
let "k++"
done
echo ""
done

循环控制符break和continue

在for、while和until循环中break可强行退出循环,break语句仅能退出当前的循环,如果是两层循环嵌套,则需要在外层循环中使用break。

#!/bin/bash

sum=0
for (( i=1; i <= 100; i++))
do
let "sum+=i"

if [ "$sum" -gt 1000 ]
then
echo "1+2+...+$i=$sum"
break
fi
done

在for、while和until中用于让脚本跳过其后面的语句,执行下一次循环。continue用于显示100内能被7整除的数。

#!/bin/bash

m=1
for (( i=1; i < 100; i++ ))
do
let "temp1=i%7" #被7整除

if [ "$temp1" -ne 0 ]
then
continue
fi

echo -n "$i "

let "temp2=m%7" #7个数字换一行

if [ "$temp2" -eq 0 ]
then
echo ""
fi

let "m++"
done

select结构

select结构从技术角度看不能算是循环结构,只是相似而已,它是bash的扩展结构用于交互式菜单显示,功能类似于case结构比case的交互性要好。

#!/bin/bash

echo "What is your favourite color? "
select color in "red" "blue" "green" "white" "black"
do
break
done
echo "You have selected $color"
#!/bin/bash

echo "What is your favourite color? "

select color
do
break
done

echo "You have selected $color"

1.6 容器

数组定义

数组(Array)是一个由若干同类型变量组成的集合,数组均由连续的存储单元组成,最低地址对应于数组的第一个元素,最高地址对应于最后一个元素。

数组的定义如下declare -a array_name

osx系统因为bash的版本太低,只能定义索引数组

在bash版本高于4.1的版本可以使用declare -A array_name定义关联数组

以下的代码片定义一个数组,用于保存配置文件中的内容,然后使用for循环遍历数组内容输出到控制台。

一键复制

#!/bin/bash
# Grabbing the last parameter
params=$#
echo The last parameter is $params
echo The last parameter is ${!#}

上述示例中的两种方式都没问题。但要注意,当命令行上没有任何参数时,$#的值为0params变量的值也一样,但${!#}变量会返回命令行用到的脚本名。

有时候需要抓取命令行上提供的所有参数,希望能够在单个变量中存储所有的命令行参数,而不是先用$#变量来判断命令行上有多少参数,然后再进行遍历。

可以使用一组其他的特殊变量$*$@来解决这个问题:

通过使用for命令遍历这两个特殊变量,可以看到它们是如何不同地处理命令行参数的。 $*变量会将所有参数当成单个参数,而$@变量会单独处理每个参数。这是遍历命令行参数的一个绝妙方法。二者之间的差异见下例:

一键复制

#!/bin/bash
count=1
for param in "$*"
do
echo "\$* Parameter #$count = $param"
count=$[ $count + 1 ]
done
echo

count=1
for param in "$@"
do
echo "\$@ Parameter #$count = $param"
count=$[ $count + 1 ]
done

运行结果如图:
抓取所有参数的两种方法

4.3.移动变量

bash shell的shift命令能够用来操作命令行参数。顾名思义,他会根据它们的相对位置来移动命令行参数。
默认情况下它会将每个参数变量向左移动一个位置。所以,变量$3的值会移到$2中,变量$2的值会移到$1中,而变量$1的值则会被删除(注意,变量$0的值即程序名不会改变)。也可以一次性移动多个位置,只需要给shift命令提供一个参数指明要移动的位置数就行了:shift n
注意:如果某个参数被移出,它的值就被丢弃了,无法再恢复。

这是遍历命令行参数的另一个好方法,尤其是在你不知道到底有多少参数时。你可以只操作第一个参数,移动参数,然后继续操作第一个参数,例如:

一键复制

#!/bin/bash
count=1
while [ -n "$1" ]; do
echo "Parameter #$count = $1"
count=$[ $count + 1 ]
shift
done

运行结果如图所示:
移动变量

4.4.处理选项

选项」是跟在单破折线后面的单个字母,它能改变命令的行为,此处介绍3种在脚本中处理选项的方法。

(1)查找选项

表面上看,命令行选项也没什么特殊的。在命令行上,它们紧跟在脚本名之后,就跟命令行参数一样。实际上,如果愿意,你可以像处理命令行参数一样处理命令行选项。

(2)getopt命令

上述shell脚本已经有了处理命令行选项的基本能力,但还有一些限制。比如,合并选项是Linux中一个很常见的用法,如果你想将多个选项放进一个参数中时,它就不能工作了。
getopt命令是一个在处理命令行选项和参数时非常方便的工具。它能够识别命令行参数,从而在脚本中解析它们时更方便。

命令格式:getopt optstring parameters
optstring是这个过程的关键所在,它定义了命令行有效的选项字母,还定义了哪些选项字母需要参数值。
首先,在optstring中列出你要在脚本中用到的每个命令行选项字母。然后,在每个需要参数值的选项字母后加一个冒号。getopt命令会基于你定义的optstring解析提供的参数。

举例如图:
getopt命令
命令行getopt ab:cd -a -b test1 -cd test2 test3中的optstring定义了四个有效选项字母:abcd。冒号(:)被放在了字母b后面,因为b选项需要一个参数值。当getopt命令运行时,它会检查提供的参数列表(-a -b test1 -cd test2 test3),并基于提供的optstring进行解析。注意,它会自动将-cd选项分成两个单独的选项,并插入双破折线来分隔行中的额外参数test2 test3
注意:如果指定了一个不在optstring中的选项,默认情况下getopt命令会产生一条错误消息,可以在命令后加-q选项来忽略这条错误消息。如下图所示:
多余的选项


可以在脚本中使用getopt来格式化脚本所携带的任何命令行选项或参数,但用起来略微复杂。
getopt命令生成的格式化后的版本来替换已有的命令行选项和参数,set命令的选项之一是双破折线--,它会将命令行参数替换成set命令的命令行值。

该方法会将原始脚本的命令行参数传给getopt命令,之后再将getopt命令的输出传给set命令,用getopt格式化后的命令行参数来替换原始的命令行参数,格式看起来如下所示:set -- $(getopt -q ab:cd "$@")
现在原始的命令行参数变量的值会被getopt命令的输出替换,而getopt已经为我们格式化
好了命令行参数。利用该方法就可以写出能帮我们处理命令行参数的脚本:

一键复制

#!/bin/bash
set -- $(getopt -q ab:cd "$@")
while [ -n "$1" ]; do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
done

count=1
for param in "$@"; do
echo "Parameter #$count: $param"
count=$[ $count + 1 ]
done

注意到该例和上文查找选项中第三种情况「处理带值的选项」一样,唯一不同的是加入了getopt命令来帮助格式化命令行参数。并且可以运行带有复杂选项的脚本如合并的选项:./mz.sh -ac,同时之前的功能照样没有问题。

(3)更高级的getopts命令

然而,getopt命令并不擅长处理带空格和引号的参数值,它会将空格当作参数分隔符,而不是根据双引号将二者当作一个参数。

getopts命令(注意是复数)内建于bash shell,它跟近亲getopt看起来很像,但多了一些扩展功能。getopt将命令行上选项和参数处理后只生成一个输出,而getopts命令能够和已有的shell参数变量配合默契。

每次调用getopts时,它一次只处理命令行上检测到的一个参数。处理完所有的参数后,它会退出并返回一个大于0的退出状态码。这让它非常适合用于解析命令行所有参数的循环中。

getopts命令的格式如下:getopts optstring variable
optstring值类似于getopt命令中的那个。有效的选项字母都会列在optstring中,如果选项字母要求有个参数值,就加一个冒号。要去掉错误消息的话,可以在optstring之前加一个冒号。getopts命令将当前参数保存在命令行中定义的variable中。
getopts命令会用到两个环境变量:OPTARG环境变量保存选项需要跟的一个参数值;OPTIND环境变量保存了参数列表中getopts正在处理的参数位置。这样你就能在处理完选项之后继续处理其他命令行参数了。

一键复制

#!/bin/bash
# simple demonstration of the getopts command
while getopts :ab:c opt
do
case "$opt" in
a) echo "Found the -a option" ;;
b) echo "Found the -b option, with value $OPTARG";;
c) echo "Found the -c option" ;;
*) echo "Unknown option: $opt";;
esac
done

while语句定义了getopts命令,指明了要查找哪些命令行选项,以及每次迭代中存储它们的变量名(opt)。注意到在本例中case语句的用法有些不同:getopts命令解析命令行选项时会移除开头的单破折线,所以在case定义中不用单破折线。
getopts命令有几个好用的功能:①可以在参数值中包含空格;②可以将选项字母和参数值放在一起使用,而不用加空格,getopts命令能够从选项中正确解析出参数值;③可以将命令行上所有未定义的选项统一输出成问号,以问号形式发送给代码。
上述代码运行结果如图:
getopts命令用法

getopts命令知道何时停止处理选项,并将参数留给你处理。在getopts处理每个选项时,它会将OPTIND环境变量值增一。在getopts完成处理时,你可以使用shift命令和OPTIND值来移动参数。如下例:

一键复制

#!/bin/bash
while getopts :ab:cd opt
do
case "$opt" in
a) echo "Found the -a option" ;;
b) echo "Found the -b option, with value $OPTARG" ;;
c) echo "Found the -c option" ;;
d) echo "Found the -d option" ;;
*) echo "Unknown option: $opt" ;;
esac
done

shift $[ $OPTIND - 1 ]
count=1
for param in "$@"
do
echo "Parameter $count: $param"
count=$[ $count + 1 ]
done

运行结果如图:
环境变量OPTIND

4.5.将选项标准化

所谓选项标准化,就是尽量遵循某些字母选项在Linux世界里已经拥有的某种程度的标准含义,而不是随意决定用哪些字母选项以及它们的用法,将选项标准化使得脚本看起来能更友好一些。常用的Linux命令选项如下:

选项 含义 描述
-a all 显示所有对象
-c count 生成一个计数
-d directory 指定一个目录
-e extend 扩展一个对象
-f file 指定读入数据的文件
-h help 显示命令的帮助信息
-i ignorecase 忽略文本大小写
-l long 产生输出的长格式版本
-n non-interactive 使用非交互模式(批处理)
-o output redirect 将所有输出重定向到指定的输出文件
-q -s quiet silent 以安静模式运行
-r recursive 递归地处理目录和文件
-v verbose 生成详细输出
-x exclude 排除某个对象
-y yes 对所有问题回答yes

4.6.获得用户输入

尽管命令行选项和参数是从脚本用户处获得输入的一种重要方式,但有时脚本的交互性还需要更强一些。比如你想要在脚本运行时问个问题,并等待运行脚本的人来回答。bash shell为此提供了read命令。

(1)基本的读取

read命令从标准输入(键盘)或另一个文件描述符中接受输入,在收到输入后会将数据放进一个指定的变量。例如:

一键复制

#!/bin/bash
echo -n "Enter your name: "
read name
echo "Hello $name, welcome to my program. "

注意,上例中生成提示的echo命令使用了-n选项。该选项不会在字符串末尾输出换行符,允许脚本用户紧跟其后输入数据,而不是下一行。这让脚本看起来更像表单。
实际上,**read命令包含了-p选项,允许你直接在read命令行指定提示符**。例如:

一键复制

#!/bin/bash
read -p "Please enter your age: " age
days=$[ $age * 365 ]
echo "That makes you over $days days old! "

read命令也允许指定多个变量,输入的每个数据值都会分配给变量列表中的下一个变量。如果变量数量不够,剩下的数据就全部分配给最后一个变量。是不是和Python中的*args**kwargs有点像呢?
也可以在read命令行中不指定变量,这样它收到的任何数据都会放进特殊环境变量REPLY中。REPLY环境变量会保存输入的所有数据,可以在shell脚本中像其他变量一样使用。

(2)超时

如果不管是否有数据输入,脚本都必须继续执行,你可以用-t选项来指定一个计时器,他指定了read命令等待输入的秒数。当计时器过期后,read命令会返回一个非零退出状态码,可以使用if-then语句或while循环这种标准的结构化语句来理清所发生的具体情况。

也可以不对输入过程计时,而是让read命令来统计输入的字符数。当输入的字符达到预设的字符数时,就自动退出,将输入的数据赋给变量。可以将-n选项和值1一起使用,告诉read命令在接受单个字符后退出。只要按下单个字符回答后,read命令就会接受输入并将它传给变量,无需按回车键。

一键复制

#!/bin/bash
read -n1 -p "Do you want to continue [Y/N]? " answer
case $answer in
Y | y) echo
echo "fine, continue on…";;
N | n) echo
echo OK, goodbye
exit;;
esac
echo "This is the end of the script"

运行结果如图:
read命令接受指定字符

(3)隐藏方式读取

当需要输入类似密码这种需要从脚本用户处得到输入,但又在屏幕上显示输入信息时,可以使用-s选项不回显终端的输入(实际上数据会被显示,只是read命令会将文本颜色设成跟背景色一样)。输入提示符输入的数据不会出现在屏幕上,但会赋给变量,以便在脚本中使用。例如:read -s -p "Enter your password: " password

(4)从文件中读取

可以用read命令来读取文件里的数据,每次调用read命令都会从文件中读取一行文本。当文件中再没有内容时,read命令会退出并返回非零退出状态码。
如何将文件中的数据传给read命令呢?最常见的方法是对文件使用cat命令,将结果通过管道直接传给含有read命令的while命令。见下例:

一键复制

#!/bin/bash
count=1
cat textfile | while read line
do
echo "Line $count: $line"
count=$[ $count + 1]
done
echo "Finished processing the file"

文件textfile内容及运行结果如下:
[从文件中读取数据

<⇧>