我的上一篇博客写到我已经详细的介绍了SHELL脚本编程的循环与分支的相关内容,这些循环与分支在bash编程中有着至关重要的地位。今天我将要介绍bash编程进阶的第二部分,也是实际使用中很重要的一部分,函数和数组,但for的特殊用法没有介绍。因此,从for的特殊用法开始。
一、for的特殊用法
for的特殊用法其实是多了双小括号的用法,即"((......))"格式,也可以用于算术运算。双小括号实际上与C语言的变量操作相同,即是使bash Shell实现C语言风格的变量操作,使for循环操作变得更简单。
for循环的特殊格式:
for ((控制变量初始化;条件判断表达式;控制变量的修正表达式)) do 循环体 done
更清晰的可以从下图中看出:
控制变量初始化:仅在运行到循环代码段时执行一次
控制变量的修正表达式:每轮循环结束会先进行控制变量修正运算,而后再做条件判断
示例:
编写脚本test.sh,使用for循环的特殊用法实现1~100的和。
#!/bin/bash for ((i=1;i<=100;i++));do let sum+=i done echo sum=$sum
结果如下:
注:正确答案应该是5050。
二、select循环
select循环主要用于创建菜单,按数字顺序排列的菜单项将显示在标准错误上,并显示 PS3 提示符,等待用户输入。用户输入菜单列表中的某个数字,执行相应的命令,用户输入被保存在内置变量 REPLY 中。
select 是个无限循环,因此要记住用 break 命令退出循环,或用 exit 命令终止脚本,也可以按ctrl+c 退出循环。select 经常和 case 联合使用,与 for 循环类似,可以省略 in list,此时使用位置参量。
select循环的格式:
select variable in list do 循环体命令 done
示
例:
编写脚本,使用select循环实现一个简单的菜单。
#!/bin/bash PS3="请问你要吃什么(输入6退出):" select menu in 黄焖鸡 鱼香肉丝盖饭 烤肉饭 老碗面 招牌五谷渔粉 退出;do case $REPLY in 1) echo "黄焖鸡 15元";; 2) echo "鱼香肉丝盖饭 15元";; 3) echo "烤肉饭 12元";; 4) echo "老碗面 9元";; 5) echo "招牌五谷渔粉 12元";; 6) break esac done
结果如下:
三、信号捕捉trap
trap是一个shell内建命令,它用来在脚本中指定信号如何处理。比如,按Ctrl+C会使脚本终止执行,实际上系统发送了SIGINT信号给脚本进程,SIGINT信号的默认处理方式就是退出程序。如果要在Ctrl+C不退出程序,那么就得使用trap命令来指定一下SIGINT的处理方式了。
trap命令不仅仅处理Linux信号,还能对脚本退出(EXIT)、调试(DEBUG)、错误(ERR)、返回(RETURN)等情况指定处理方式。
trap的格式如下:
trap '触发指令' 信号:自定义进程收到系统发出的指定信号后,将执行触发指令,而不会执行原操作 trap '' 信号:忽略信号的操作 trap '-' 信号:恢复原信号的操作 trap -p:列出自定义信号操作,即提示当前使用的trap操作是什么。
示例:
打印0-9,Ctrl+C不退出程序
#!/bin/bash trap 'echo "signal:SIGINT"' SIGINT trap -p for((i=0;i<=10;i++)) do sleep 1 echo $i done
结果如下:
四、函数
函数function是由若干条shell命令组成的语句块,实现代码重用和模块化编程它与shell程序形式上是相似的,不同的是它不是一个单独的进程,不能独立运行,而是shell程序的一部分,函数和shell程序比较相似,区别在于:Shell程序在子Shell中运行,而Shell函数在当前Shell中运行。因此在当前Shell中,函数可以对shell中变量进行修改。函数变量默认的是全局变量,所以要定义本地变量只影响函数内部,避免修改函数外的变量值。函数不能用exit退出,因为函数本身没有开启子进程会退出整个脚本。
【函数带参数的说明】
1:函数体中位置参数($1、$2、$3、$4、$5、$#、$*、$?以及$@)都可以是函数的参数
2:父脚本的参数则临时地被函数参数所掩盖或隐藏
3:$0比较特殊,它仍然是父脚本的名称
4:当函数完成时,原来的命令行参数会恢复
5:在shell函数里面,return命令的功能的工作方式与exit相同,用于跳出函数
6:在shell函数体里使用exit会终止整个shell脚本
7:return语句会返回一个退出值给调用的程序
4.1 定义函数
函数由两部分组成:函数名和函数体
help function
语法一: f_name (){ ...函数体... } 语法二: function f_name { ...函数体... } 语法三: function f_name () { ...函数体... }
4.2 函数使用
函数的定义和使用:
可在交互式环境下定义函数
可将函数放在脚本文件中作为它的一部分
可放在只包含函数的单独文件中
调用:函数只有被调用才会执行
函数的调用若是在同一个脚本中,调用操作需要在定义的函数后面。调用:给定函数名
函数名出现的地方,会被自动替换为函数代码
函数的生命周期:被调用时创建,返回时终止
4.3 函数返回值
函数有两种返回值:
函数的执行结果返回值:
(1) 使用echo等命令进行输出
(2) 函数体中调用命令的输出结果
函数的退出状态码:
(1) 默认取决于函数中执行的最后一条命令的退出状态码
(2) 自定义退出状态码,其格式为:
return 从函数中返回,用最后状态命令决定返回值
return 0 无错误返回。
return 1-255 有错误返回
4.4 交互式环境下定义和使用函数
定义该函数后,若在$后面键入dir,其显示结果同ls -l的作用相同
dir
该dir函数将一直保留到用户从系统退出,或执行了如下所示的unset命令
unset dir
4.5 在脚本中定义及使用函数
函数在使用前必须定义,因此应将函数定义放在脚本开始部分,直至shell首次发现它后才能使用。
调用函数仅使用其函数名即可
示例:
编写脚本funcl.sh,实现
#!/bin/bash # func1 hello() { echo "Hello there today's date is `date +%F`" } echo "now going to the function hello" hello echo "back from the function"
结果如下:
4.6 使用函数文件
可以将经常使用的函数存入函数文件,然后将函数文件载入shell,文件名可任意选取,但最好与相关任务有某种联系。一旦函数文件载入shell,就可以在命令行或脚本中调用函数。可以使用set命令查看所有定义的函数,其输出列表包括已经载入shell的所有函数;若要改动函数,首先用unset命令从shell中删除函数。改动完毕后,再重新载入此文件。
4.7 载入函数
函数文件已创建好后,要将它载入shell
定位函数文件并载入shell的格式:
. filename 或 source filename
注意:此即<点> <空格> <文件名>,这里的文件名要带正确路径
4.8 执行or删除shell函数
要执行函数,简单地键入函数名即可,现在对函数做一些改动后,需要先删除函数,使其对shell不可用。使用unset命令完成删除函数
命令格式为:
unset function_name
4.9 环境函数
使子进程也可使用
声明:export -f function_name 查看:export -f 或 declare -xf
4.10 函数参数
函数可以接受参数:
传递参数给函数:调用函数时,在函数名后面以空白分隔给定参数列表即可;例如“testfunc arg1 arg2 ...”
在函数体中当中,可使用$1, $2, ...调用这些参数;还可以使用$@, $*, $#等特殊变量
4.11 函数变量
变量作用域:
环境变量:当前shell和子shell有效
本地变量:只在当前shell进程有效,为执行脚本会启动专用子shell进程;因此,本地变量的作用范围是当前shell脚本程序文件,包括脚本中的函数
局部变量:函数的生命周期;函数结束时变量被自动销毁
注意:如果函数中有局部变量,如果其名称同本地变量,使用局部变量
在函数中定义局部变量的方法:
local NAME=VALUE
Shell变量一般是无类型的,但是bash Shell提供了declare和typeset两个命令用于指定变量的类型,两个命令是等价的。
declare [选项] 变量名 -r 声明或显示只读变量 -i 将变量定义为整型数 -a 将变量定义为数组 -A 将变量定义为关联数组 -f 显示此脚本前定义过的所有函数名及其内容 -F 仅显示此脚本前定义过的所有函数名 -x 声明或显示环境变量和函数 -l 声明变量为小写字母 declare –l var=UPPER -u 声明变量为大写字母 declare –u var=lower
4.12 函数递归示例
函数递归:
函数直接或间接调用自身,注意递归层数
递归示例:
阶乘是基斯顿·卡曼于 1808 年发明的运算符号,是数学术语。一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且有0的阶乘为1,自然数n的阶乘写作n!,n!=1×2×3×...×n,阶乘亦可以递归方式定义:
0!=1,n!=(n-1)!×n
n!=n(n-1)(n-2)...1
n(n-1)! = n(n-1)(n-2)!
练习
1、编写函数,实现OS的版本判断
#!/bin/bash os(){ sed -r 's/.*[[:space:]]([0-9])\..*/\1/' /etc/redhat-release } os
结果如下:
2、编写函数,实现取出当前系统eth0的IP地址
#!/bin/bash eth0ip(){ ifconfig eth0 |grep Mask |tr -s ' '|cut -d' ' -f3 |cut -d: -f2 } eth0ip
结果如下:
3、使用函数功能打印国际棋盘
#!/bin/bash red(){ echo -e "\033[41m \033[0m\c" } yel(){ echo -e "\033[43m \033[0m\c" } redyel() { for ((i=1;i<=2;i++));do for ((j=1;j<=4;j++));do [ "$1" = "-r" ] && { yel;red; } || { red;yel; } done echo done } for ((line=1;line<=8;line++));do [ $[$line%2] -eq 0 ] && redyel || redyel -r done
结果如下:
4、斐波那契数列又称黄金分割数列,因数学家列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……,斐波纳契数列以如下被以递归的方法定义:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n≥2),利用函数,求n阶斐波那契数列。
read -p "请输入斐波那契数列的阶数:" n fact(){ if [ $1 -eq 0 ];then echo 0 elif [ $1 -eq 1 ];then echo 1 else echo $[`fact $[$1-1]`+`fact $[$1-1]`] fi } for i in `seq 0 $n`;do fact $i done
结果如下:
五、fork×××
fork×××是一种恶意程序,它的内部是一个不断在fork进程的无限循环,实质是一个简单的递归程序。由于程序是递归的,如果没有任何限制,这会导致这个简单的程序迅速耗尽系统里面的所有资源
函数实现
:(){ :|:& };: bomb() { bomb | bomb & }; bomb
脚本实现
cat Bomb.sh #!/bin/bash ./$0|./$0&
六、数组
变量:存储单个元素的内存空间
数组:存储多个元素的连续的内存空间,相当于多个变量的集合
数组名和索引
索引:编号从0开始,属于数值索引
注意:索引可支持使用自定义的格式,而不仅是数值格式,即为关联索引,bash4.0版本之后开始支持bash的数组支持稀疏格式(索引不连续)
声明数组:
declare -a ARRAY_NAME declare -A ARRAY_NAME: 关联数组
注意:两者不可相互转换
6.1 数组赋值
数组元素的赋值:
(1) 一次只赋值一个元素;
ARRAY_NAME[INDEX]=VALUE weekdays[0]="Sunday weekdays[4]="Thursday
(2) 一次赋值全部元素:
ARRAY_NAME=("VAL1" "VAL2" "VAL3" ...)
(3) 只赋值特定元素:
ARRAY_NAME=([0]="VAL1" [3]="VAL2" ...)
(4) 交互式数组值对赋值
read -a ARRAY 显示所有数组:declare -a
6.2 引用数组
引用数组元素:
${ARRAY_NAME[INDEX]}
注意:省略[INDEX]表示引用下标为0的元素
引用数组所有元素:
${ARRAY_NAME[*]} ${ARRAY_NAME[@]}
数组的长度
(数组中元素的个数):
${#ARRAY_NAME[*]} ${#ARRAY_NAME[@]}
删除数组中的某元素:导致稀疏格式
unset ARRAY[INDEX]
删除整个数组:
unset ARRAY
6.3 数组数据处理
引用数组中的元素:
数组切片:
${ARRAY[@]:offset:number}
offset: 要跳过的元素个数
number: 要取出的元素个数
取偏移量之后的所有元素
${ARRAY[@]:offset}
向数组中追加元素:
ARRAY[${#ARRAY[*]}]=value
关联数组:
declare -A ARRAY_NAME ARRAY_NAME=([idx_name1]='val1' [idx_name2]='val2‘...)
注意:关联数组必须先声明再调用
练习
1、生成10个随机数保存于数组中,并找出其最大值和最小值
#!/bin/bash declare -a rand declare -i max=0 declare -i min=32767 for i in {0..9}; do rand[$i]=$RANDOM echo ${rand[$i]} [ ${rand[$i]} -gt $max ] && max=${rand[$i]} [ ${rand[$i]} -lt $min ] && min=${rand[$i]} done echo "Max: $max Min:$min"
结果如下:
2、编写脚本,定义一个数组,数组中的元素是/var/log目录下所有以.log结尾的
文件;要统计其下标为偶数的文件中的行数之和
#!/bin/bash declare -a files files=(/var/log/*.log) declare -i lines=0 for i in $(seq 0 $[${#files[*]}-1]); do if [ $[$i%2] -eq 0 ];then let lines+=$(wc -l ${files[$i]} | cut -d' ' -f1) fi done echo "Lines: $lines."
统计并排序/var/log目录下所有以.log结尾的文件结果如下:
结果如下:
3、输入若干个数值存入数组中,采用冒泡算法进行升序或降序排序
#!/bin/bash quit() { if [[ $1 =~ [Qq][Uu][Ii][Tt]|[Qq] ]];then exit 0 fi } read -p "请输入数字: " -a num quit $num for x in ${num[*]};do if [[ $x =~ ^-?[0-9]+$ ]];then true else echo "请输入正确数字" 1>&2 exit 1 fi done while true;do for ((i=0;i<${#num[*]};i++));do for ((j=0;j<${#num[*]}-1;j++));do if [ ${num[j]} -gt ${num[j+1]} ];then n=${num[j]} num[j]=${num[j+1]} num[j+1]=$n fi done done echo ${num[*]} read -p "是否要继续输入数字进行冒泡,若输入quit则退出: " num1 quit $num1 if [[ $x =~ ^-?[0-9]+$ ]];then true else echo "请输入正确数字" 1>&2 exit 2 fi num[${#num[*]}]=$num1 done
结果如下: