1、循环控制语句

1.1 概述

语句含义应用场景
exit终止执行脚本,退出返回值。exit n
n的取值范围:0-255
基础用法,判断后加上exit
脚本结束加上exit
用于脚本中检查部分(参数个数,检查格式,adv:rc=0,可以放在脚本最后exit $rc)
return放在函数中,终止执行函数,函数返回值写在函数中,检查函数命令运行是否成功,方便测试
建议判断最后1个命令或函数中关键命令
break终止循环(退出),不会继续运行剩余循环要在循环中表示退出循环
continue终止本次运行,进入下一次循环要在循环中跳过某一次循环

break n:结束多少层循环,比如:break 3

continue n:结束当前循环,并从第几层开始,比如:continue 2

相关案例:

continue:

[root@m01 ~]#for i in {1..3}
do
   for j in {a..c}
   do
     [ $j = b ] && continue
     echo $i  $j
   done
done
1 a
1 c
2 a
2 c
3 a
3 c
[root@m01 ~]#

continue n:

[root@m01 ~]#for i in {1..3}
do
  for j in {a..c}
  do
     [ $j = b ] && continue 2
    echo $i  $j
   done
 done
1 a
2 a
3 a
[root@m01 ~]#

continue:

[root@m01 ~]#for n in {1..5};do [ $n -eq 3 ] && continue; echo $n;done
1
2
4
5
[root@m01 ~]#

break:

[root@m01 ~]#for n in {1..5};do [ $n -eq 3 ] && break; echo $n;done
1
2
[root@m01 ~]#

break n:

[root@m01 ~]#for i in {1..3}
do   
   for j in {a..c};  
   do     
       [ $j = b ] && break 2     
       echo $i  $j  
   done      
done
1 a

[root@m01 ~]#for i in {1..3}
do   
   for j in {a..c}
      do     
        [ $j = b ] && break
        echo $i  $j
      done
done
1 a
2 a
3 a
[root@m01 ~]#

1.2 实战

案例:修改之前的猜数字脚本

如果输入不是数字,进行提示并继续提示用户输入

如果猜了10次还没成功,则结束

#1、random
num=$((RANDOM%100+1)) #RANDOM范围:1-32767,对100取余,再加1,得到1-100的数
i=0
percent() {
  if [ $i -le 5 ];then
    echo "超过了99.99%的人,猜了$i次"
  elif [ $i -le 10 ];then
    echo "超过了90%的人,猜了$i次"
  else
    echo "超过了85%的人,猜了$i次"
  fi
}
input() {
  #2、vars
  read -p "输入数字:" guess
}
check() {
  #3、check
  expr $guess + 1 &> /dev/null
  [ $? -gt 1 ] && {
    echo "请输入数字,重来"
   # exit 1
    continue
  }
}
compare() {
#4、compare
  if [ $guess -eq $num ];then
    echo "猜对了"
    percent
    exit
  elif [ $guess -gt $num ];then
    echo "数字大了"
  else
    echo "数字小了"
  fi
}
main() {
#5、while
  while true
  do
     input
     let i++
     check
     compare
     if [ $i -eq 5 ];then
        echo "猜了5次都没正确,正确数字是:$num"
        break
     fi
  done
}
main

测试:

[root@m01 /server/script/devops-shell]#sh 37.guess_num.sh
输入数字:80
数字大了
输入数字:60
数字大了
输入数字:50
数字大了
输入数字:30
数字小了
输入数字:40
数字大了
猜了5次都没正确,正确数字是:32
[root@m01 /server/script/devops-shell]#

案例02:return使用

[root@m01 /server/script/devops-shell]#vim +23 38.return.sh
function show() {
name=$1
cat <<EOF
  show函数的参数个数:$#
  show函数的所有参数:$*
  ${name}.com
  ${name}.cn
  ${name}.org
EOF
   return $?
   echo "这个会运行吗?"
}

show baidu.com
echo "函数的返回值:$?" #检查show函数执行是否正确

执行:

[root@m01 /server/script/devops-shell]#sh 38.return.sh
show函数的参数个数:1
show函数的所有参数:baidu.com
baidu.com.com
baidu.com.cn
baidu.com.org
函数的返回值:0
[root@m01 /server/script/devops-shell]#

2、数组

2.1 概述

数组也是一种变量

数组可以存放多个相关内容,通过访问数组调用结果(值)

应用场景:用于存放相关的数据

数组创建(手动):

数组名=(值)

ip_addr=(192.168.10.61 192.168.10.2)  #以空格分隔

获取数组的值:

${数组命[元素名称或下标]}

例如:

echo ${ip_addr[0]}

注意:数组元素的下标从0开始。

如下:

[root@m01 ~]#ip_addr=(192.168.10.61 192.168.10.2)
[root@m01 ~]#echo ${ip_addr[0]}
192.168.10.61
[root@m01 ~]#echo ${ip_addr[1]}
192.168.10.2
[root@m01 ~]#echo ${ip_addr[2]}

[root@m01 ~]#

取出数组全部元素:”*、@

[root@m01 ~]#echo ${ip_addr[*]}
192.168.10.61 192.168.10.2
[root@m01 ~]#echo ${ip_addr[@]}
192.168.10.61 192.168.10.2
[root@m01 ~]#

数组的元素个数(数组长度):#数组名[*或@]

[root@m01 ~]#echo ${#ip_addr[@]}
2
[root@m01 ~]#echo ${#ip_addr[*]}
2
[root@m01 ~]#

数组元素遍历:

[root@m01 ~]#for((i=0;i<${#ip_addr[*]};i++))
 do
   echo ${ip_addr[i]}
 done
[root@m01 ~]#

或者:

[root@m01 ~]#for ip in ${ip_addr[*]}
do
  echo $ip
done
[root@m01 ~]#

2.2 数组赋值

shell数组创建(赋值)形式应用场景
批量直接赋值array=(ip1 ip2 ip3) array=(命令结果) ,如:array=(cat ip.txt)应用最多
逐个元素赋值array[0]=haha  array[1]=hello…几乎不用
read命令赋值read -p ”请输入:” -a array(使用空格分隔)用于连续读取数据

例如:

[root@m01 /server/script/devops-shell]#cat ip.txt 
192.168.10.61
192.168.10.1
192.168.10.2
192.168.10.7
www.baidu.com
baidu.com
[root@m01 /server/script/devops-shell]#ip_list=(`cat ip.txt`)
[root@m01 /server/script/devops-shell]#
[root@m01 /server/script/devops-shell]#users=(`awk -F: '{print $1}' /etc/passwd`)
[root@m01 /server/script/devops-shell]#echo ${#users[@]}
22
[root@m01 /server/script/devops-shell]#

read赋值:

[root@m01 ~]#read -p "输入数据:" -a num
输入数据:1 2 3   
[root@m01 ~]#echo ${num[*]}
1 2 3
[root@m01 ~]#

行转列:

[root@m01 ~]#echo 1 2 3 | xargs -n1
1
2
3
[root@m01 ~]#

案例:写一个shell程序,然后求出其总和、平均值

在脚本中获取用户输入的数据

· read或$1、$2....
· 保存到文件
· 数组

脚本:

[root@m01 /server/script/devops-shell]#vim 38.cals_num.sh
#1、read
read -p "请输入数字(空格分隔,回车结束):" -a num
[[ "${num[*]}" =~ ^[0-9\ ]+$ ]] || {
   echo "请输入数字"
   exit 1
}
#2、cals
sum=0
for n in ${num[*]}
do
  let sum=$sum+$n
done
avg=`echo "$sum/${#num[*]}" | bc -l`
max=`echo ${num[*]} | xargs -n1 | sort -rn | head -1`
min=`echo ${num[*]} | xargs -n1 | sort -rn | tail -1`
#3、echo
cat <<EOF
  "所有数据:${num[*]}"
  "总和:$sum"
  "平均值:$avg"
  "最大值:$max"
  "最小值:$min"
EOF

执行脚本:

[root@m01 /server/script/devops-shell]#sh 38.cals_num.sh
请输入数字(空格分隔,回车结束):1 2 3
  "所有数据:1 2 3"
  "总和:6"
  "平均值:2.00000000000000000000"
  "最大值:3"
  "最小值:1"
[root@m01 /server/script/devops-shell]#

cat EOF格式问题:

cat <<EOF
  xxx
EOF(此处的EOF必须顶格写)

不顶格写:
cat <<-EOF(加上-)
  xxx
	EOF(此处不顶格写,必须用tab键,不能用空格)

取出数组元素最大、最小值的通用方法:

array=(2 3 1 4)
echo "原始数组:${array[*]}"
#取出元素最大、最小值
min=${array[0]} #把数组第一个元素作为初始值
max=${array[0]} #把数组第一个元素作为初始值
#min
for n in ${array[*]}
do
   if [ $min -gt $n ];then
       min=$n
   fi
done
#max
for n in ${array[*]}
do
   if [ $max -lt $n ];then
       max=$n
   fi
done

cat <<EOF
  "最小值:$min"
  "最大值:$max"
EOF

2.3 数组排序

#!/bin/bash
arr=(2 3 1 4)
echo "原始数组:${arr[*]}"
#sort
for((i=1;i<${#arr[*]};i++))
do
   for((j=0;j<${#arr[*]}-i;j++))
   do
     if [ ${arr[j]} -gt ${arr[j+1]} ];then
        temp=${arr[j]}
        arr[j]=${arr[j+1]}
        arr[j+1]=$temp
     fi 
   done
   echo "第$i次排序后:${arr[*]}"
done
echo "排序后:${arr[*]}"

3、堡垒机

shell脚本实现

功能:

1、登录堡垒机的机器,自动运行堡垒机脚本
2、显示菜单选择功能
  1、远程连接
  2、测试ping、telent等

不能给登录堡垒机的用户root权限

流程:

环境准备:

先配置好堡垒机到其他机器的秘钥认证。配置过程不再演示。

3.1、脚本

[root@m01 /server/script/devops-shell]#vim 39.baoleiji.sh
#0、trap
trap '' SIGINT
#1、main_menu主菜单
clear
main_menu() {
cat <<EOF
##########堡垒机#########
  a:远程连接
  b:测试工具
#########################
EOF
}
#远程连接菜单
ssh_menu() {
clear
cat <<EOF
 1:连接192.168.10.5
 2:连接192.168.10.7
EOF
}
#测试工具
test_menu() {
cat <<EOF
 1:ping
 2:telent
EOF
}
#2、input获取用户输入
input() {
  read choice
}
#test
check_ping() {
  read -p "请输入ping的ip:" ip
  ping -c2 -i0.5 $ip &> /dev/null
  [ $? -eq 0 ] && echo "$ip可以访问" || echo "无法访问"
}
check_telnet() {
  read -p "请输入telent的ip 端口" ip port
  echo q | telnet -eq $ip $port &> /dev/null
  [ $? -eq 0 ] && echo "$ip $port可以访问" || echo "无法访问"
}
ssh_all() {
 #远程连接
   ssh_menu
   input
   case "$choice" in
     1)  ssh root@192.168.10.5 ;;
     2)  ssh root@192.168.10.7 ;;
     *)
        echo "选择错误,请重新输入"
        continue
   esac

}
telnet_all() {
#测试
  test_menu
  input
  case "$choice" in
    1) check_ping ;;
    2) check_telnet ;;
    *)
       echo "选择错误"
       continue
  esac
}
#3、main
main() {
while true
do
  main_menu
  input
  case "$choice" in
  a|A)
       ssh_all
       ;;
  b|B)
       telnet_all
       ;;
   quit) break ;;
   *)
      echo error
      continue
  esac
done
}
main

执行脚本:

整体功能OK

存在的问题:

脚本需要手动执行
脚本一次性执行

3.2 脚本自动运行

把脚本放在/etc/profile.d/目录下即可。脚本名称要以.sh结尾

此处做个软连接作为调试用:

[root@m01 /server/script/devops-shell]#ln -sf /server/script/devops-shell/39.baoleiji.sh /etc/profile.d/

3.3 脚本禁止取消或结束

屏蔽ctrl+c

trap:获取指定的信号,用于获取指定操作产生的信号

ctrl+c对应的信号SIGINT

通过trap获取SIGINT信号(ctrl+c),获取后不执行任何任务。

trap '' SIGINT

把该命令放在脚本开头

4、脚本debug流程

书写习惯:

1、添加注释
2、变量:脚本中尽量使用变量,变量名要规范
3、函数:代码中尽量使用函数,增加说明
4、返回值:尽可能添加函数return功能,方便后期调试
5、参数与选项检查:尽可能增加exit返回值的功能,方便后期调试
6、书写的时候适当添加输出,通过临时的输出分析错误
7、缩进:代码注意缩进
8、成对的符号要提前输入好,以免忘记。如:[]、{}、''等成对的符号

调试方法

调试方法
-x:大部分的时候可以使用,sh -x 脚本、bash -x 脚本,显示详细的执行过程。
精确显示过程:脚本使用functions函数库,脚本过大,可以在脚本中使用
set -x开始显示详细信息
代码 set +x关闭显示详细信息
注释法:通过注释,缩小范围定位问题
输出关键变量:在变量后增加echo,输出变量内容,查看过程

5、三剑客

5,1 sed与变量

案例1:sed命令中如何调用变量

ip.txt文件内容如下:

192.168.10.61
192.168.10.2

sed命令中使用变量

[root@m01 ~]#src=192.168.10
[root@m01 ~]#dst=172.16.1
[root@m01 ~]#sed "s#$src#$dst#g" ip.txt 
172.16.1.61
172.16.1.2
[root@m01 ~]#

5.2 awk与变量

案例1:过滤出/etc/passwd的第2到第9行的第1和第3列

[root@m01 ~]#awk -F: 'NR>=2 && NR<=9{print $1,$3}'  /etc/passwd  |  column -t

column -t:格式对齐

案例2:第1列用户名等于root的最后一列(命令解释器)

[root@m01 ~]#awk -F: '$1=="root"{print $NF}'  /etc/passwd
/bin/bash
[root@m01 ~]#awk -F: '$1==root{print $NF}'  /etc/passwd
[root@m01 ~]#
#$1==root的root没有加双引号,awk任务root是个变量,因此输出为空

使用变量:

[root@m01 ~]#name=root
[root@m01 ~]#awk -vn=$name -F: '$1==n{print $NF}'  /etc/passwd
/bin/bash
[root@m01 ~]#awk -vn=$name -F: '$1~n{print $NF}'  /etc/passwd
/bin/bash
[root@m01 ~]#

案例3:过滤网卡文件的ip地址那行

[root@m01 ~]#awk -F= '/IPADDR/{print $2}' /etc/sysconfig/network-scripts/ifcfg-eth0
192.168.10.61
[root@m01 ~]#

5.3 awk判断与循环

1)判断

awk中的if语句格式:

单分支:
if () {
   语句
}
双分支:
if () {
   语句
}
else {
   语句
}

注意与shell脚本不一样。

例如:判断根分区磁盘使用率大于7%,提示磁盘空间不足

[root@m01 ~]#df -h | awk '$NF=="/"{ if($5>=7) print"磁盘空间不足"}'
磁盘空间不足
[root@m01 ~]#df -h / | awk -F "[ %]+" 'NR==2{if ($(NF-1)>=7) { print "磁盘空间不足";print $(NF-1)}}'
磁盘空间不足
7
[root@m01 ~]#
2)循环

例如:计算1到100的和

[root@m01 ~]#awk 'BEGIN{for(i=1;i<=100;i++){sum=sum+i} print sum}'
5050
[root@m01 ~]#awk 'BEGIN{
for(i=1;i<=100;i++)
     { sum=sum+i }
 print sum
}'
5050
[root@m01 ~]#

案例2:输入数字,计算总和

[root@m01 ~]#read -p "请输入数字:" num
请输入数字:1 2 3
[root@m01 ~]#echo $num | awk '{for(i=1;i<=NF;i++) sum=sum+i ;print sum}'
6
[root@m01 ~]#

5.4 awk与数组

awk数组专用于统计与分析:去重统计次数(sort+uniq)、去重求和

awk数组与shell数组区别:

awk数组:关联数组,下标啥都行
shell数组:普通数组,下标是数字,shell中也有关联数组

使用:

创建数组:

[root@m01 ~]#awk 'BEGIN{ arr[0]="haha";arr["num"]=1 }'

取值:

[root@m01 ~]#awk 'BEGIN{ arr[0]="haha";arr["num"]=1; print arr["num"]}'
1
[root@m01 ~]#

5.5 awk数组循环

awk专用于数组的循环

for (n in 数组名)
   print n(数组下标),数组名[n]
   
变量n:获取数组下标
数组名[下标] 才是对应的值

例如:

[root@m01 ~]#awk 'BEGIN{ arr[0]="haha";arr["num"]=1;
 for(n in arr)
    print n,arr[n]
 }'
num 1
0 haha
[root@m01 ~]#
[root@m01 ~]#awk 'BEGIN{ arr[0]="haha";arr["num"]=1;                   
for(n in arr)
   print n
}'
num
0
[root@m01 ~]#

案例1:批量赋值

awk-arr.txt文件内容如下:

blog.test.com  6
img.test.com  7
bbs.test.com  99

要求:创建以url为下标,元素值是次数数组,并输出

[root@m01 ~]#awk 'NR>=1{arr[$1]=$2}END{ for(url in arr) print url,arr[url] }' awk-arr.txt 
blog.test.com 6
bbs.test.com 99
img.test.com 7
[root@m01 ~]#

案例2:去重统计次数

url.txt文件内容如下:

http://www.etiantian.org/index.html
http://www.etiantian.org/1.html
http://post.etiantian.org/index.html
http://mp3.etiantian.org/index.html
http://www.etiantian.org/3.html
http://post.etiantian.org/2.html

awk:

[root@m01 ~]#awk -F"[/.]+" '{arr[$2]=arr[$2]+1} END{for(n in arr) print n,arr[n]}' url.txt 
www 3
mp3 1
post 2
[root@m01 ~]#

arr[$2]=arr[$2]+1可以写成:arr[$2]++

案例3:统计access.log中每个ip地址的流量总数

access.log文件:

第1列:ip
第10列:流量(单位:字节)
每列以空格分隔

awk:

[root@m01 ~]#awk '{liu[$1]=liu[$1]+$10} END{for(ip in liu) print ip,liu[ip]}' access.log | sort -rnk2 | head

liu[$1]=liu[$1]+$10可以写成:liu[$1]+=$10

5.6 awk与shell语法格式对比

具体语法awkshell编程
if单分支if(条件)
{
命令
}
if [ 条件 ];then
命令
fi
if双分支if(条件)
{
命令
}
else
{
命令
}
if [ 条件 ] ;then
命令
else
命令
fi
for循环,c语言格式for(i=1;i<=10;i++)
{
命令
}
for(i=1;i<=10;i++)
do
命令
done
for循环:通用格式awk专用于数组的循环格式:
for(n in 数组名)
print n,数组名[n]
for n in 清单
do
命令
done