便捷工具知识

8: Shell脚本进阶

2022-09-14  本文已影响0人  随便写写咯

3. bash的配置文件

查看当前Shell类型

echo $SHELL
/bin/bash

bash shell的配置文件很多, 可以分成下面类别

3.1 按照生效范围

全局配置

/etc/profile
/etc/profile.d/*.sh
/etc/bashrc

个人配置

~/.bashrc
~/.bash_profile

3.2 Shell登录两种方式分类

3.2.1 交互式登录

配置文件执行顺序

/etc/profile.d/*.sh
/etc/bashrc
/etc/profile
/etc/bashrc  #此文件执行两次
~/.bashrc
~/.bash_profile

范例: 验证配置文件执行顺序

# 测试命令echo, 都写到文件的最后一行

[root@demo-c8 ~]# vim /etc/profile.d/test.sh
echo "/etc/profile.d/test.sh"

[root@demo-c8 ~]# vim /etc/bashrc
echo "/etc/bashrc" 

[root@demo-c8 ~]# vim /etc/profile
echo "/etc/profile"

[root@demo-c8 ~]# vim ~/.bashrc 
echo "~/.bashrc "

[root@demo-c8 ~]# vim ~/.bash_profile
echo "~/.bash_profile"
# 重新登录终端

[root@demo-c8 ~]# exit
logout

Connection closed by foreign host.

Disconnected from remote host(demo-c8) at 15:13:53.

Type `help' to learn how to use Xshell prompt.
[c:\~]$ 
Reconnecting in 1 seconds. Press any key to exit local shell.
.

Connecting to 10.0.0.108:22...
Connection established.
To escape to local shell, press 'Ctrl+Alt+]'.

Activate the web console with: systemctl enable --now cockpit.socket

Last login: Fri Sep  9 15:02:48 2022 from 10.0.0.1
/etc/profile.d/test.sh
/etc/bashrc
/etc/profile
/etc/bashrc
~/.bashrc   # 这两行是root用户自己的配置, 所以其他用户登录时, 是不会显示的, 因为没有在其他用户加目录下的.bashrc和.bash_profile进行配置
~/.bash_profile
[root@demo-c8 ~]# 
# 非交互式配置文件执行顺序

[root@demo-c8 ~]# su root
/etc/profile.d/test.sh
/etc/bashrc
~/.bashrc
# wang用户家目录的.bashrc和.bash_profile没有设置, 所以不会执行echo命令
[root@demo-c8 ~]# su - wang
/etc/profile.d/test.sh
/etc/bashrc
/etc/profile
/etc/bashrc
[root@demo-c8 ~]# su wang
/etc/profile.d/test.sh
/etc/bashrc

注意: 如果一个配置被写到了多个配置文件里, 那么后执行的配置文件中的配置会生效, 比如别名.

注意: 不同的配置文件之间, 有可能存在调用关系, 比如/etc/bashrc中会调用/etc/profile.d/*.sh, 所以命令在配置文件中的书写顺序, 会影响配置文件的执行效果

[root@demo-c8 ~]# vim /etc/bashrc
...
    for i in /etc/profile.d/*.sh; do  # /etc/bashrc中, 调用/etc/profile.d/*/sh
        if [ -r "$i" ]; then
            if [ "$PS1" ]; then
                . "$i"
            else
                . "$i" >/dev/null
            fi
        fi
    done
...

3.2.2 非交互式登录

配置文件执行顺序:

/etc/profile.d/*.sh
/etc/bashrc
.bashrc

无论是交互式还是非交互式登录, 这些配置文件是在用户登录时才会执行, 如果只是开机, 用户不登录. 那并不会执行

这些配置文件的作用就是用来初始化用户Shell环境

不同操作系统的配置文件执行顺序不同, 以上为CentOS测试结果, Ubuntu还需按照同样的方法自己测试

3.3 按功能划分分类

profile类和bashrc类

3.3.1 Profile类

profile类为交互式登录的Shell提供配置

全局: /etc/profile, /etc/profile.d/*.sh
个人: ~/.bash_profile

功用:

3.3.2 Bashrc类

bashrc类: 为非交互式和交互式登录的Shell提供配置

全局: /etc/bashrc
个人: ~/.bashrc

功能:

3.4 编辑配置文件生效

修改profile和bashrc文件后需要手动使其生效:

1. 重新登录Shell
2. source | . 配置文件

source | . *.sh vs bash *.sh

正常执行Shell脚本会开启子进程, 在脚本中定义的变量, 不会影响当前环境

用source或者.去执行一个脚本, 是在当前Shell环境执行, 因此会更改当前Shell环境

执行脚本不要用source和., 但是在脚本里可以用source或者.去执行其他脚本, 使得另一个脚本和本脚本在同一个Shell中执行

source和.用来执行配置文件, 因为配置文件本身就是用来修改当前Shell环境的

export用来将一个自定义的变量, 变成普通变量, 一旦一个普通变量变成了环境变量, 之后对齐修改赋值, 就不需要在加export了.

而PATH和PS1等, 属于系统自带的环境变量, 本身已经存在了, 所以对齐修改时不用加export

范例: 给同一个脚本, 创建不同的软链接, 使得执行软链接文件时功能不同

[root@demo-c8 data]# vim test.sh

#!/bin/bash
echo $0 # 该脚本作用就是返回执行的脚本文件的全路径, 和脚本是不是软链接无关
# 创建两个软链接, 都指向test.sh

[root@demo-c8 data]# ln -s test.sh a.sh
[root@demo-c8 data]# ln -s test.sh b.sh
[root@demo-c8 data]# ll
total 4
lrwxrwxrwx. 1 root root  7 Sep  9 15:52 a.sh -> test.sh
lrwxrwxrwx. 1 root root  7 Sep  9 15:52 b.sh -> test.sh
-rw-r--r--. 1 root root 21 Sep  9 15:52 test.sh
# 虽然两个软链接指向同一个脚本, 但是执行结果却是不同的
[root@demo-c8 data]# bash a.sh 
a.sh
[root@demo-c8 data]# bash b.sh 
b.sh
[root@demo-c8 data]# chmod +x *
[root@demo-c8 data]# ./a.sh 
./a.sh
[root@demo-c8 data]# ./b.sh 
./b.sh
[root@demo-c8 data]# ll
total 4
lrwxrwxrwx. 1 root root  7 Sep  9 15:52 a.sh -> test.sh
lrwxrwxrwx. 1 root root  7 Sep  9 15:52 b.sh -> test.sh
-rwxr-xr-x. 1 root root 21 Sep  9 15:52 test.sh

3.5 Bash退出任务

保存在~/.bash_logout(用户)文件中, 用户在退出当前Shell终端时自动运行

功能:

练习

1、让所有用户的PATH环境变量的值多出一个路径,例如:/usr/local/apache/bin
2、用户 root 登录时,将命令指示符变成红色,并自动启用如下别名:

rm=‘rm -i’
cdnet=‘cd /etc/sysconfig/network-scripts/’
editnet=‘vim /etc/sysconfig/network-scripts/ifcfg-eth0’
editnet=‘vim /etc/sysconfig/network-scripts/ifcfg-eno16777736 或 ifcfg-ns33 ’ (如果系统是CentOS7)

3、任意用户登录系统时,显示红色字体的警示提醒信息“Hi,dangerous!”
4、编写生成脚本基本格式的脚本,包括作者,联系方式,版本,时间,描述等

4. 流程控制

4.1 条件选择

4.1.1 条件判断介绍

单分支语法:

if 条件判断; then
  CMD 条件为真的分支代码
fi

双分支语法:

if 条件判断; then
  CMD 条件为真的分支代码
else
  CMD 条件为假的分支代码
fi

多分支语法:

if 条件判断1; then
  CMD 条件1为真的代码, 也就是if后的命令执行结果$?返回0
elif 条件判断2; then
  CMD 条件2为真的代码
elif 条件判断3; then
  CMD 条件3为真的代码
...
else
  以上条件都为假的分支代码
fi

说明:

4.1.2 if-else案例

范例: 根据ping命令的结果判断主机存活情况

#!/bin/bash
STATION1=10.0.0.1

if ping -c1 -W1 ${STATION1} &> /dev/null; then  #-W定义超时时间, 1秒中就返回结果
    echo "${STATION1} is up"
elif grep -q "~/maintenance.txt"; 
    echo "${STATION1} is under maintenance"
else
    echo "${STATION1} is down!!!"
    exit 1 
    # 如果服务器不在维护列表, 并且无法ping通, 那么返回${STATION1} is down!!!, 同时执行exit退出, 返回状态码1.
    # 不同的执行错误会返回不同的状态码, 通过自定义状态码, 把失败定义为1, 就可以更明确的判断命令执行成功与否

fi

4.1.3 case语句判断离散值

语法:

case 变量引用 in  #变量引用需要加$, $变量名
PATTERN1)
  分支1
  ;;
PATTERN2)
  分支2
  ;;
PATTERN3)
  分支3
  ;;
...
*)  # * 通配符, 表示任意字符串, 也就是默认分支, 默认分支一定是写到最后, 当所有的情况都不满足时, 执行默认分支
  默认分支
  ;;
esac

case支持glob风格的通配符:

* 任意长度任意字符
? 任意单个字符
[] 指定范围内的任意单个字符
| 或者, 例如 a|b

case案例:

  1. Yes or No方法1
#!/bin/bash
read -p "Do you agree? Yes or No: " INPUT

INPUT=`echo $INPUT | tr 'A-Z' 'a-z'`

case $INPUT in
y|yes)
    echo "You have agreed!"
    ;;
n|no)
    echo "You have disagreed!"
    ;;
*)  
    echo "Wrong answer!!!"
esac 
  1. Yes or No方法2
#!/bin/bash
read -p "Do you agree?: " INPUT

case $INPUT in

[Yy] | [Yn][Ee][Ss])
    echo "You have agreed!!!"
    ;;
[Nn] | [Nn][Oo])
    echo "You have disagreed!!"
    ;;
*)
    echo "Wrong answer!!!"
esac   
  1. 实现运维菜单

日常工作写入运维菜单脚本

#!/bin/bash
echo -e "\033[1;32m"

cat <<EOF
请选择:
1) 备份数据库
2) 清理日志 
3) 软件升级
4) 软件回滚
5) 删库跑路
6) 重启所有生产服务器
EOF

echo -e "\033[0m"

read -p "请输出您想执行的操作!: " NUM

case $NUM in

1)
   ./backup.sh # 在一个脚本中, 调用另一个脚本
    ;;
2)
    echo "清理日志ing"
    ;;
3)  
    echo "软件升级ing"
    ;;
4)
    echo "软件回滚ing"
    ;;
5)  
    echo "删库ing"
    ;;
6)
    echo "重启所有生成服务器ing"
    ;;
*)  
    echo "输错啦!!!"
esac

练习

1、编写脚本 createuser.sh,实现如下功能:使用一个用户名做为参数,如果指定参数的用户存在,就显示其存在,否则添加之。并设置初始密码为123456,显示添加的用户的id号等信息,在此新用户第一次登录时,会提示用户立即改密码,如果没有参数,就提示:请输入用户名

2、编写脚本 yesorno.sh,提示用户输入yes或no,并判断用户输入的是yes还是no,或是其它信息

3、编写脚本 filetype.sh,判断用户输入文件路径,显示其文件类型(普通,目录,链接,其它文件类型)

4、编写脚本 checkint.sh,判断用户输入的参数是否为正整数

5、编写脚本 reset.sh,实现系统安装后的初始化环境,包括:1、别名 2、环境变量,如PS1等 3、安装常用软件包,如:tree 5、实现固定的IP的设置,6、vim的设置等

4.2 循环

4.2.1 循环执行介绍

将某代码段重复运行多次,通常有进入循环的条件和退出循环的条件

重复运行次数

常见的循环命令

for: 次数循环, 遍历循环
while: 条件循环
until: 条件循环

4.2.2 for循环

格式1:

for 变量名 [ in 列表 ]; do  
  循环体
done

for 变量名 [ in 列表 ]
do
  循环体
done

变量名: 不加$

执行机制:

列表: 空格隔开或者换行隔开的字符串列表, 列表中的字符串个数, 决定了循环的次数. 这里说的列表是没有中括号的

每次循环时, 变量名的值, 会按照顺序取列表中的元素, 直到取光列表中的所有元素

列表生成: 可以手动输入, 也可通过命令生成列表; 只要生成的是列表即可

for循环列表生成方式:

#!/bin/bash
for i in 1 2 3 4; do
    echo $i                                                                    
done

[root@demo-c8 data]# bash for1.sh 
1
2
3
4
[root@demo-c8 data]# vim for2.sh
#!/bin/bash
for i in  {1..10};do
    echo $i                                                                    
done

[root@demo-c8 data]# bash for2.sh 
1
2
3
4
5
6
7
8
9
10
[root@demo-c8 data]# vim for3.sh
#!/bin/bash
for i in `seq 10`; do
    echo $i
done   

[root@demo-c8 data]# bash for3.sh 
1
2
3
4
5
6
7
8
9
10
[root@demo-c8 data]# vim for4.sh
#!/bin/bash  
for i in *.sh; do                                                              
    echo $i
done

[root@demo-c8 data]# bash for4.sh 
action1.sh
a.sh
b.sh
for1.sh
for2.sh
for3.sh
for4.sh
menu.sh
ping.sh
test.sh
[root@demo-c8 data]# vim for5.sh
#!/bin/sh
for i in "$*"; do                                                              
    echo $i
done

[root@demo-c8 data]# bash for5.sh 1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10

案例: 计算 1..100相加之和

[14:07:17 root@c8prac /data/scripts]#sum=0; for i in `seq 100`;do let sum+=i; done ;echo sum=$sum
sum=5050
[root@demo-c8 data]# sum=0;for i in 2 4 6 8 10;do let sum+=i;done;echo sum=$sum
sum=30
[root@demo-c8 data]# sum=0;for i in 1 3 5 7 9;do let sum+=i;done;echo sum=$sum
sum=25
[14:12:21 root@c8prac /data/scripts]#seq -s+ 100 | bc
5050

生成奇数

seq 1 2 100
[root@demo-c8 data]# echo {1..10..2}
1 3 5 7 9

生成偶数

seq 2 2 100
[root@demo-c8 data]# echo {2..10..2}
2 4 6 8 10
seq -s+ 100 #-s+, 把seq默认的换行符\n, 替换成+

案例: 99乘法表

# 方法1:
#!/bin/bash

for i in {1..9}; do
    for j in `seq $i`;do
        let var=$i*$j
        echo -e "$j*$i=$var\t\c" #\t 把每个结果用tab隔开, \c每行循环完才换行.
    done
    echo
done  
[14:42:49 root@c8prac /data/scripts]#bash 99.sh 
1*1=1   
1*2=2   2*2=4   
1*3=3   2*3=6   3*3=9   
1*4=4   2*4=8   3*4=12  4*4=16  
1*5=5   2*5=10  3*5=15  4*5=20  5*5=25  
1*6=6   2*6=12  3*6=18  4*6=24  5*6=30  6*6=36  
1*7=7   2*7=14  3*7=21  4*7=28  5*7=35  6*7=42  7*7=49  
1*8=8   2*8=16  3*8=24  4*8=32  5*8=40  6*8=48  7*8=56  8*8=64  
1*9=9   2*9=18  3*9=27  4*9=36  5*9=45  6*9=54  7*9=63  8*9=72  9*9=81  
# 方法2:
#!/bin/bash

for i in {1..9};do
    for j in `seq $i`;do
        echo -e "$j*$i=$[i*j]\t\c"                                                                                                                                                          
    done
    echo
done 
[root@demo-c8 data]# bash 99.sh 
1*1=1   
1*2=2   2*2=4   
1*3=3   2*3=6   3*3=9   
1*4=4   2*4=8   3*4=12  4*4=16  
1*5=5   2*5=10  3*5=15  4*5=20  5*5=25  
1*6=6   2*6=12  3*6=18  4*6=24  5*6=30  6*6=36  
1*7=7   2*7=14  3*7=21  4*7=28  5*7=35  6*7=42  7*7=49  
1*8=8   2*8=16  3*8=24  4*8=32  5*8=40  6*8=48  7*8=56  8*8=64  
1*9=9   2*9=18  3*9=27  4*9=36  5*9=45  6*9=54  7*9=63  8*9=72  9*9=81  

案例: 将指定目录下所有文件, 都加上.bak结尾, 也就是保持原有后缀

[15:01:52 root@c8prac /data/prac]#touch f{1..5}.txt
[15:01:58 root@c8prac /data/prac]#touch f{1..5}.log
[15:02:00 root@c8prac /data/prac]#touch f{1..5}.sh

#!/bin/bash

DIR=/data/prac
for i in `ls $DIR`; do 
  mv $i $i.bak
done

案例: 将指定目录下指定后缀的文件, 都加上.bak结尾

[root@demo-c8 opt]# vim mv.sh

#!/bin/bash
 
DIR="/opt" # 针对哪个目录内的文件进行修改, 不加/目录后缀
SUFFIX="txt" # 指定修改哪些后缀类型的文件
                                                                                                                                                                          
for i in `ls ${DIR}/*.${SUFFIX}`; do
    mv $i ${i}.bak
done

案例: 给指定目录下, 所有文件的后缀都改为.bak, 也就是修改原有后缀为.bak

这就要求被修改的数据不能有同名的, 因为一旦同名, 后改名的文件有可能覆盖先改名的文件, 在移动文件时就会提示是否覆盖, 还需要手动输入是或否. 或者就是在脚本中进行判断, 如果有同名文件, 则修改文件名称

[15:09:18 root@c8prac /data/prac]#touch {a..j}.log
[15:09:55 root@c8prac /data/prac]#touch {k..p}.txt
[15:10:08 root@c8prac /data/prac]#touch {q..z}.sh
[15:10:13 root@c8prac /data/prac]#ls
a.log  c.log  e.log  g.log  i.log  k.txt  m.txt  o.txt  q.sh  s.sh  u.sh  w.sh  y.sh
b.log  d.log  f.log  h.log  j.log  l.txt  n.txt  p.txt  r.sh  t.sh  v.sh  x.sh  z.sh


[15:12:09 root@c8prac /data/prac]#ls
a.bak  c.bak  e.bak  g.bak  i.bak  k.bak  m.bak  o.bak  q.bak  s.bak  u.bak  w.bak  y.bak
b.bak  d.bak  f.bak  h.bak  j.bak  l.bak  n.bak  p.bak  r.bak  t.bak  v.bak  x.bak  z.bak

#!/bin/bash
DIR=/data/prac

for i in `ls $DIR`; do
    file=`echo $i | sed -nr 's/(^.*)\.(.*$)/\1/p'`
mv $i $file.bak

done

范例: 只显示一个目录下的文件夹

# ls的-F选项会显示出Linux目录的后缀/
[15:18:00 root@c8prac /data/prac]#ls -F
2020-07-25/  2020-10-23/  c.bak  h.bak  m.bak  r.bak  w.bak
2020-08-25/  2020-10-24/  d.bak  i.bak  n.bak  s.bak  x.bak
2020-09-25/  2020-10-25/  e.bak  j.bak  o.bak  t.bak  y.bak
2020-10-21/  a.bak        f.bak  k.bak  p.bak  u.bak  z.bak
2020-10-22/  b.bak        g.bak  l.bak  q.bak  v.bak
[15:18:01 root@c8prac /data/prac]#ls -F | grep .*/$
2020-07-25/
2020-08-25/
2020-09-25/
2020-10-21/
2020-10-22/
2020-10-23/
2020-10-24/
2020-10-25/

范例: 要求将目录YYYY-MM-DD/中所有的文件, 移动到YYYY-MM/DD下

情况1: 单目录移动到单目录, 将2022-09-09目录中的所有文件, 移动到2022-09/09目录

[root@demo-c8 ~]# vim /data/mv2.sh 

#!/bin/bash
  
SDATE="2022-09-09"                                                      # DATE格式为YYYY-MM-DD
SFDIR="/opt"                                                            # 源父目录
SDIR="${SFDIR}/${SDATE}"                                                # 源日志目录


YM=`echo ${SDATE} | sed -nr 's#(^[0-9]{4}-[0-9]{2})(.*)#\1#p'`          # 目标父目录
DD=`echo ${SDATE} | sed -nr 's#(^[0-9]{4}-[0-9]{2})-([0-9]{2})#\2#p'`

DFDIR="/opt"
DDATE="${YM}/${DD}"
DDIR="${DFDIR}/${DDATE}"                                                # 目标日志目录


mkdir -pv ${DDIR}
mv ${SDIR}/* ${DDIR} && echo "mv finish" || echo "mv failed"                                                                                                                                

#echo SDATE=${SDATE}
#echo SFDIR=${SFDIR}
#echo SDIR=${SDIR}
#echo YM=${YM}
#echo DD=${DD}
#echo DDATE=${DDATE}
#echo DFDIR=${DFDIR}
#echo DDIR=${DDIR}
[root@demo-c8 ~]# ls /opt/2022-09-09/
f10.txt  f2.txt  f4.txt  f6.txt  f8.txt  l10.bak  l2.bak  l4.bak  l6.bak  l8.bak  t10.log  t2.log  t4.log  t6.log  t8.log
f1.txt   f3.txt  f5.txt  f7.txt  f9.txt  l1.bak   l3.bak  l5.bak  l7.bak  l9.bak  t1.log   t3.log  t5.log  t7.log  t9.log
[root@demo-c8 ~]# ls /opt/2022-09-09/
f10.txt  f2.txt  f4.txt  f6.txt  f8.txt  l10.bak  l2.bak  l4.bak  l6.bak  l8.bak  t10.log  t2.log  t4.log  t6.log  t8.log
f1.txt   f3.txt  f5.txt  f7.txt  f9.txt  l1.bak   l3.bak  l5.bak  l7.bak  l9.bak  t1.log   t3.log  t5.log  t7.log  t9.log
[root@demo-c8 ~]# bash /data/mv2.sh 
mkdir: created directory '/opt/2022-09'
mkdir: created directory '/opt/2022-09/09'
[root@demo-c8 ~]# ls /opt/2022-09-09/
[root@demo-c8 ~]# ls /opt/2022-09/09/
f10.txt  f2.txt  f4.txt  f6.txt  f8.txt  l10.bak  l2.bak  l4.bak  l6.bak  l8.bak  t10.log  t2.log  t4.log  t6.log  t8.log
f1.txt   f3.txt  f5.txt  f7.txt  f9.txt  l1.bak   l3.bak  l5.bak  l7.bak  l9.bak  t1.log   t3.log  t5.log  t7.log  t9.log

情况2: 多目录移动到多目录, 把YYYY-MM-DD目录中的内容, 移动到对应的YYYY-MM/DD目录中

单目录移动到单目录, 只需要按照目录名称需求, 利用sed命令通过源目录去构建目标目录, 之后利用mv命令即可, 无需for循环

多目录移动到多目录, 需要利用for循环, 每次先把涉及到的目录名称遍历出来, 之后, 针对每个目录名称去构建目标目录, 然后执行mv命令

[15:43:30 root@c8prac /data/prac]#ls
2010-2-25   2020-10-25  2020-12-25  mv.sh
2020-09-25  2020-11-25  2020-2-25
[15:43:32 root@c8prac /data/prac]#tree
.
├── 2010-2-25
│   ├── f1.txt
│   └── f2.txt
├── 2020-09-25
│   ├── f1.txt
│   └── f2.txt
├── 2020-10-25
│   ├── f1.txt
│   └── f2.txt
├── 2020-11-25
│   ├── f1.txt
│   └── f2.txt
├── 2020-12-25
│   ├── f1.txt
│   └── f2.txt
├── 2020-2-25
│   ├── f1.txt
│   └── f2.txt
└── mv.sh

6 directories, 13 files
#!/bin/bash
for i in `ls -F | grep .*/$`; do
    YM=`echo $i | sed -nr 's#(^[0-9]{4}-[0-9]{2})(.*)#\1#p'`
    DD=`echo $i | sed -nr 's#(^[0-9]{4}-[0-9]{2})-([0-9]{2})\/$#\2#p'`
    mkdir $YM/$DD -p                                                    
    mv $i/* $YM/$DD
    rm -rf $i
done   

执行结果:

[15:43:35 root@c8prac /data/prac]#bash mv.sh 
[15:44:11 root@c8prac /data/prac]#tree
.
├── 2020-09
│   └── 25
│       ├── f1.txt
│       └── f2.txt
├── 2020-10
│   └── 25
│       ├── f1.txt
│       └── f2.txt
├── 2020-11
│   └── 25
│       ├── f1.txt
│       └── f2.txt
├── 2020-12
│   └── 25
│       ├── f1.txt
│       └── f2.txt
└── mv.sh

8 directories, 9 files

范例: 删除YYYY-MM-DD格式的目录

rm -rf *-*-*

范例: 按照YYYY-MM-DD日期格式, 生成365天的目录, 每个目录里创建.log文件

for i in {1..365}; do
    DIR=`date -d  "-$i day" +%F`
    mkdir $DIR
    cd $DIR
    for n in {1..10};do
        touch $RANDOM.log
    done
    cd ..                                                               
done

范例: 扫描一个网段:10.0.0.0/24,判断此网段中主机在线状态,将在线的主机的IP打印出来

[root@demo-c8 opt]# vim ping.sh 

#!/bin/bash
NETWORK="10.0.0"
IP=`seq 255`
for i in $IP ;do
    if ping -c1 -W1 $NETWORK.$i &> /dev/null; then
        echo "$NETWORK.$i"
    fi                                                                                                                                                                     
done
[root@demo-c8 opt]# bash ping.sh 
10.0.0.1
10.0.0.2
10.0.0.108

格式2:

双小括号方法, 即((...))格式, 也可以用于算数运算, 双小括号方法也可以使bash shell实现C语音风格的变量操作

for ((控制变量初始化;条件表达式;控制表达的修正表达式)); do
  循环体
done

说明:

1. 初始化变量 
2. 判断变量是否满足条件, 如果满足则进入循环, 不满足则直接退出循环 
3. 满足条件时, 进入循环, 执行循环体, 然后执行变量修正表达式
4. 执行条件判断, 判断变量是否满足条件, 满足则进入循环, 不满足则直接退出
...

循环体可以没有, 用true或者:代替

范例: 计算1+2+3+...+100之和

# sum=0: 初始化变量
# i<=100: 条件判断
# i++: 修改变量
[root@demo-c8 ~]# for ((sum=0,i=1; i<=100;i++));do let sum+=i;done;echo sum=$sum
sum=5050

范例: 99乘法表

[root@demo-c8 opt]# vim 99.sh
#!/bin/bash
for ((i=1;i<=9;i++));do
    for ((j=1;j<=i;j++));do
        echo -e "$j*$i=$[$i*$j]\t\c"                                                                                                                                                        
    done
    echo
done
[root@demo-c8 opt]# bash 99.sh 
1*1=1   
1*2=2   2*2=4   
1*3=3   2*3=6   3*3=9   
1*4=4   2*4=8   3*4=12  4*4=16  
1*5=5   2*5=10  3*5=15  4*5=20  5*5=25  
1*6=6   2*6=12  3*6=18  4*6=24  5*6=30  6*6=36  
1*7=7   2*7=14  3*7=21  4*7=28  5*7=35  6*7=42  7*7=49  
1*8=8   2*8=16  3*8=24  4*8=32  5*8=40  6*8=48  7*8=56  8*8=64  
1*9=9   2*9=18  3*9=27  4*9=36  5*9=45  6*9=54  7*9=63  8*9=72  9*9=81

范例: 死循环

# ((;;))为死循环
[root@demo-c8 opt]# for ((;;));do echo "for";sleep 1;done
for
for
for
for

范例: 生成进度条

[root@demo-c8 opt]# for ((i = 0; i <= 100; ++i)); do printf "\e[4D%3d%%" $i;sleep 0.1s; done

for循环是根据定义的列表进行多次循环

如果想要实现达到某一条件时才循环, 可以用while

练习:用 for 实现
1、判断/var/目录下所有文件的类型
2、添加10个用户user1-user10,密码为8位随机字符
3、/etc/rc.d/rc3.d目录下分别有多个以K开头和以S开头的文件;分别读取每个文件,以K开头的输出为文件加stop,以S开头的输出为文件名加start,如K34filename stop S66filename start
4、编写脚本,提示输入正整数n的值,计算1+2+…+n的总和
5、计算100以内所有能被3整除的整数之和
6、编写脚本,提示请输入网络地址,如192.168.0.0,判断输入的网段中主机在线状态
7、打印九九乘法表
8、在/testdir目录下创建10个html文件,文件名格式为数字N(从1到10)加随机8个字母,如:1AbCdeFgH.html
9、打印等腰三角形
10、猴子第一天摘下若干个桃子,当即吃了一半,还不瘾,又多吃了一个。第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,只剩下一个桃子了。求第一天共摘了多少?

4.2.3 while循环

格式:

while COMMANDS; do COMMANDS; done

while CONDITIONS; do
      循环体
done

说明:

CONDITION循环控制条件: 进入循环之前, 先做一次判断; 之后每次进入循环之前会再次做判断; 条件为true, 则执行一次循环; 直到条件测试状态为false终止循环, 因此, CONDITION一般应该有循环控制变量; 而此变量的值会在循环体代码中, 不断地被修正

进入循环条件: CONDITION为true
退出条件: CONDITION为false

无限循环:

while true; do
  循环体
done

while : ; do
  循环体
done

:true 一样, 永远返回真

案例: 检查磁盘利用率, 当某个分区利率率大于百分之80告警

[root@demo-c8 ~]# df
Filesystem     1K-blocks    Used Available Use% Mounted on
devtmpfs         3957252       0   3957252   0% /dev
tmpfs            3985416       0   3985416   0% /dev/shm
tmpfs            3985416    9832   3975584   1% /run
tmpfs            3985416       0   3985416   0% /sys/fs/cgroup
/dev/sda2       41922560 4561196  37361364  11% /
/dev/sda5       41922560  325332  41597228   1% /data
/dev/sda1         999320  192552    737956  21% /boot
tmpfs             797080    1168    795912   1% /run/user/42
tmpfs             797080       4    797076   1% /run/user/0
# ^/dev.*([0-9]{1,3})%.*
# ([0-9]{1,3}表示数字0-9内的任意数字, 出现1到3次, 因此, 出现1次也是满足的
# 而grep和sed是贪婪匹配, 所以第一组.*会持续匹配任意字符, 任意次数, 直到磁盘利用率的十位, 然后留下个位的数字给到([0-9]{1,3}做匹配
# 如果利用率是21%, 那么贪婪匹配会把2也匹配进去, 只留下1给 ([0-9]{1,3}

[root@demo-c8 ~]# df | sed -nr '/^\/dev/s#^/dev.*([0-9]{1,3})%.*#\1#p'
1
1
1
# 为了避免贪婪匹配, 可以在要匹配的字符串前, 加一个空格, 或者其他字符串, 把贪婪匹配的内容, 和要匹配的目标内容隔开
# 本案例就是用的空格, 因为磁盘利用率前面的字符串是空格

[root@demo-c8 ~]# df | sed -nr '/^\/dev/s#^/dev.* ([0-9]{1,3})%.*#\1#p'
11
1
21
[root@demo-c8 ~]# cat .mailrc 
set from=abc@123.com  #邮件以哪个地址发送
set smtp=smtp.qq.com #腾讯qq邮箱服务器
set smtp-auth-user=abc@123.com #qq邮箱
set smtp-auth-password=avibeavdgeztbcfd #授权码
set smtp-auth=login
set smtp-verity=ignore
#!/bin/bash
WARNING=80

while true; do # 死循环, 持续监测

    USE=`df | sed -nr '/^\/dev/s#.* ([0-9]+)%.*#\1#p' | sort -nr | head -n1`                                                                        

    if [ $USE -gt $WARNING ]; then

        echo Disk will be full at `hostname -I` | mail -s 'disk warning' abc@123.com

    fi

    sleep 10 # 每十秒执行一次循环
done
# 执行检查脚本, 因为是死循环, 所以脚本默认会占据终端
[root@demo-c8 opt]# bash check_disk.sh 

image.png

练习题: 用while实现

  1. 编写脚本, 求100以内所有正奇数之和
  1. 编写脚本, 提示请输入网络地址, 如, 192.168.0.0, 判断输入的网络中主机在线状态, 并统计在线和离线主机各多少
  1. 编写脚本打印99乘法表
  1. 编写脚本, 利用变量RANDOM生成10个随机数字, 输出这10个数字, 并显示其中的最大值和最小值
  1. 编写脚本, 实现打印国际象棋棋盘
  1. 后续6个字符串: efbaf275cd, 4be9c40b8b, 44b2395c46, f8c8873ce0, b902c16c8b, ad865d2f63 是通过对随机数变量RANDOM随机执行命令: echo $RANDOM|md5sum|cut -c1-10后的结果, 请破解这些字符串对应的RANDOM的值

4.2.4 until循环

CONDITION为假, 才执行循环, 为真,则退出循环; 刚好和while相反

格式:

until COMMANDS; do COMMANDS; done

until CONDTION;do
  循环体
done

说明:

进入条件: CONDITION为false
退出条件: CONDITION为true

无限循环:

until false; do
  循环体
done

适用场景: 当到达某一条件后, 要退出循环, 可以用until

案例: 利用until计算1..100之和

当变量i大于100时就退出循环

#初始化变量时, 和是0, i是从1开始
#每次执行循环, 如果条件成立, 那么先做sum=sum+i, 然后i再自增
#i自增后再次判断i是否大于100, 如果不大于,也就是不满足条件, 那么继续做sum+=i, 然后i自增
#直到i满足大于100了, 立即退出循环, 不再执行sum+=i和let i++
[18:58:26 root@CentOS-8 ~]#i=1;sum=0; until [ $i -gt 100 ]; do let sum+=i ; let i++; done ; echo $sum
5050
[19:03:23 root@CentOS-8 ~]#i=1;sum=0; while [ $i -le 100 ];do let sum+=i; let i++; done ; echo $sum
5050

4.2.4 循环控制语句 continue

continue [N]: 提前结束第N层的本轮循环, 而直接进入下一轮判断, 也就是continue后的所有本次循环的命令都不再执行; 最内层为第1层, continue可以结束任意一层的本次循环

格式:

while CONDITION1; do
  CMD1
  ...
  if CONDITION2; then
        continue
  fi 
  CMDn
  ...
done

范例:

for ((i=0;i<10;i++));do

    for ((j=0;j<10;j++));do
        [ $j -eq 5 ] && continue  # continue, 只结束本层的本次循环, 也就是只结束内层for循环的当j=5时的这次循环
        echo $j
    done
    echo -----------         # 当内层for循环进行到j=10时, 不满足条件, 则退出内层for循环, 打印echo --------, 然后进入下一次外层for循环                                                                                                                
done
[root@demo-c8 data]# bash while1.sh 
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
0
1
2
3
4
6
7
8
9
-----------
for ((i=0;i<10;i++));do

    for ((j=0;j<10;j++));do
        [ $j -eq 5 ] && continue 2 # 当j=5时, 直接结束本次内层for循环, 也结束本次外层循环, 直接进入下一次外层for循环
        echo $j
    done
    echo -----------              # 当j=5时, 内层的本次循环直接结束, 外层的本次for循环也直接结束, 所以echo -----是不会执行的                                                                                                                  
done
[root@demo-c8 data]# bash while2.sh 
0
1
2
3
4
0
1
2
3
4
0
1
2
3
4
0
1
2
3
4
0
1
2
3
4
0
1
2
3
4
0
1
2
3
4
0
1
2
3
4
0
1
2
3
4
0
1
2
3
4

4.2.5 循环控制语句 break

break [N]: 提前结束第N层整个循环, 也就是本层的循环就不做了,直接退出, 如果有外层循环那么继续执行外层循环; 最内层为第1层

格式:

while CONDITION1; do
  CMD1
  ...
  if CONDITION2; then
    break
  fi
  CMDn
  ...
done

案例: break

for ((i=1;i<10;i++)); do
    for ((j=1;j<10;j++));do
        [ $j -eq 5 ] && break                                                                                                                       
        echo $j
    done
    echo --------
done
[root@demo-c8 opt]# for ((i=1;i<10;i++)); do
>     for ((j=1;j<10;j++));do
>         [ $j -eq 5 ] && break                                                                                                                       
>         echo $j
>     done
>     echo --------
> done
1
2
3
4
--------
1
2
3
4
--------
1
2
3
4
--------
1
2
3
4
--------
1
2
3
4
--------
1
2
3
4
--------
1
2
3
4
--------
1
2
3
4
--------
1
2
3
4
--------

案例: break 2

for ((i=1;i<10;i++)); do
    for ((j=1;j<10;j++));do
        [ $j -eq 5 ] && break 2                                                                                                                   
        echo $j
    done
    echo --------
done
[root@demo-c8 opt]# for ((i=1;i<10;i++)); do
>     for ((j=1;j<10;j++));do
>         [ $j -eq 5 ] && break 2  # break 2直接把外层for循环给干掉了, 所以外层for循环只执行了i=1这一轮就结束了. 而内层for循环只执行了1,2,3,4                                                                                                        
>         echo $j
>     done
>     echo --------
> done
1
2
3
4
[root@demo-c8 opt]# cat drink.sh 
#!/bin/bash
YELLOW='echo -e \033[1;33m'
GREEN='echo -e \033[1;32m'
RED='echo -e \033[1;31m'
END2='echo -e \033[0m'
END='\033[0m'


while true; do
$YELLOW
    cat << EOF
1) 可乐
2) 雪碧
3) 芬达
4) 蛋白奶
5) 矿泉水
6) 一样一瓶
7) 退出
EOF
${END2}

    read -p "喝啥? : " DRINK
    echo
    case $DRINK in 

    1) 
        $GREEN可乐3块钱一瓶!$END
        let SUM+=3 
        ;;
    2)
        $GREEN雪碧3块钱一瓶!$END
        let SUM+=3
        ;;
    3)
        $GREEN芬达4块钱一瓶!$END
        let SUM+=4
        ;;
    4)
        $GREEN蛋白奶2块钱一瓶!$END
        let SUM+=2
        ;;
    5)
        $GREEN矿泉水1块钱一瓶!$END
        let SUM+=1
        ;;
    6)
        $GREEN一样一瓶总共13元!$END
    let SUM+=13
        ;;
    7)  
        $GREEN你花了$SUM!$END
        break
        
        ;;
    *)
        $RED到底喝啥?重选!$END
        ;;
    esac    
done

4.2.6 循环控制shift命令

shift [n] 用于将参数列表 list 左移指定次数,默认为左移一次。
参数列表 list 一旦被移动,最左端的那个参数就从列表中删除。while 循环遍历位置参数列表时,常用到 shift

当脚本有个多个参数需要处理时, 用shift命令可以每次只处理第一个变量, 相当于每次处理完一个变量, 就把这个变量从参数列表删除, 后面的变量变成$1. 这样就不用去考虑脚本后面跟了几个参数, 每次执行shift, 都处理一次位置参数, 默认一次处理一个参数

案例: shift基本使用

[root@demo-c8 opt]# vim shift1.sh

#!/bin/bash
  
until [ -z $1 ]; do # -z判断字符串是否为空, 为空返回真, 不为空返回假
                    # 如果$1为空了, 也就是没有参数了, 那么until循环结束
        echo $1
        shift #每次循环结束, 都把参数向左移动一位, 也就是删除刚刚处理完的参数
done
[root@demo-c8 opt]# bash shift1.sh a b c d e f
a
b
c
d
e
f

案例: 根据用户输入的用户名, 批量创建账号

# #S先判断参数个数, 如果为0, 说明脚本没有跟参数, 那么就无法创建用户

if  [ $# -eq 0 ];then
    echo "Usage: `basename $0`  user1 user2 user3 ..."
    exit
fi

while [ "$1" ];do # 直接判断$1是否存在, 存在则创建爱你, 不存在则退出

    if id $1 &> /dev/null; then
        echo $1 exists
    else
        useradd $1
        echo $1 has been created!
    fi
    shift # 每次处理完一个参数, 就左移一位
done                                                                                                                                                
echo "All uses have been created!"

练习

1、每隔3秒钟到系统上获取已经登录的用户的信息;如果发现用户hacker登录,则将登录时间和主机记录于日志/var/log/login.log中,并退出脚本
2、随机生成10以内的数字,实现猜字游戏,提示比较大或小,相等则退出
3、用文件名做为参数,统计所有参数文件的总行数
4、用二个以上的数字为参数,显示其中的最大值和最小值

4.2.7 while read 特殊用法

read 后面如果不接变量名, 则会把标准输入赋值给REPLY

注意: 管道前后的命令是在单独的进程里, 都是当前Shell进程的子进程, 且是独立的进程
注意: 通过管道传递或定义的变量是在管道内的子进程有效的, 在当前Shell无效, 因为管道里的进程是当前Shell的子进程, 普通变量无法传递给父进程

[20:27:57 root@CentOS-8 /data/scripts]#echo wang | read NAME
[20:28:42 root@CentOS-8 /data/scripts]#echo $NAME

[20:28:44 root@CentOS-8 /data/scripts]#echo wang | (read NAME;echo $NAME) 
wang
[20:38:31 root@CentOS-8 /data/scripts]#{ echo $BASHPID; sleep 100; } | { read  NAME;echo $NAME; echo $BASHPID;sleep 200; }
6662  #6662是管道前面的echo $BASHPID的结果, 传递给管道后面的read命令, 然后echo $NAME出来的结果
6663 #6663是管道后面执行echo $BASHPID的结果
#通过下面pstree -p命名可以看到, 6662和6663都是当前Shell的子进程, 并且相互独立

───bash(1568)─┬─bash(6662)───sleep(6664)
              └─bash(6663)───sleep(6665)

整个while循环内的代码都是处在同一个进程

[root@demo-c8 opt]# echo wang | while read x; do echo $x; done
wang
[root@demo-c8 opt]# echo wang | ( read x; echo $x)
wang
[root@demo-c8 opt]# echo wang | { read x; echo $x; }
wang

read可以同时对多个变量赋值

[20:39:29 root@CentOS-8 ~]#read x y
1 2
[20:42:53 root@CentOS-8 ~]#echo $x;echo $y
1

如果变量数和数据数不匹配会出问题, 因此除非能控制输入, 否则不要用多个变量

[root@demo-c8 opt]# read x y
1 2 3
[root@demo-c8 opt]# echo $x
1
[root@demo-c8 opt]# echo $y
2 3

read只是读入一行, 然后对变量赋值; 配合while 可以实现逐行处理

案例:

[20:47:38 root@CentOS-8 ~]#while read NAME; do echo $NAME; done < /etc/fstab

#
# /etc/fstab
# Created by anaconda on Thu Oct 29 17:21:15 2020
#
# Accessible filesystems, by reference, are maintained under '/dev/disk/'.
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info.
#
# After editing this file, run 'systemctl daemon-reload' to update systemd
# units generated from this file.
#
UUID=a62dc0e0-420e-4d19-baf4-b6e71884dec9 / xfs defaults 0 0
UUID=22d71e36-c425-4174-ab35-9181c1d80104 /boot ext4 defaults 1 2
UUID=fd6c51d7-3ac8-4e0d-b7b5-fbff825972ce /data xfs defaults 0 0
UUID=836c1290-855e-4c1c-a3e9-14fb6efd7a20 swap swap defaults 0 0

利用这个特性, 可以把想要处理的字符放在文件里, 按行分开, 通过while read 把每一行作为NAME变量读进循环, 然后处理

while read line; do
    循环体
done < /PATH/FROM/SOMEFILE

案例: 批量创建账号

[20:49:32 root@CentOS-8 ~]#for i in {1..3}; do useradd user$i; done
[20:49:43 root@CentOS-8 ~]#vim name.txt
haha
lala
heihei
ooxx 
[20:51:09 root@CentOS-8 ~]#while read NAME;do  useradd $NAME; done < name.txt 
[20:51:24 root@CentOS-8 ~]#cat name.txt | while read NAME; do echo $NAME; done
haha
lala
heihei
ooxx

范例: while read 设置磁盘告警

DISK: 接收磁盘名, /dev/sda1...
USE: 接收磁盘利用率

WARNING=80

df | sed -nr '/^\/dev/s#(^/dev/sd.[0-9]).* ([0-9]+)%.*#\1 \2#p' | while read DISK USE; do
[ $USE -gt $WARNING ] && echo "$DISK is over ${WARNING}% at ${USE}%" ;done

案例: 三个小于号赋值

[21:30:30 root@CentOS-8 /data/scripts]#read x y z <<< "i love linux"
[21:31:40 root@CentOS-8 /data/scripts]#echo $x; echo $y; echo $z
i
love
linux

范例: while true; 配合read 可以用 while read代替

[root@demo-c8 opt]# vim read1.sh
#!/bin/bash
while true; do
        read -p "请输入名字: " NAME # read -P可以用来提醒用户输入什么内容
        echo $NAME
done
[root@demo-c8 opt]# bash read1.sh 
请输入名字: haha
haha
请输入名字: lala
lala
请输入名字: heihei
heihei
[root@demo-c8 opt]# vim read2.sh
#!/bin/bash
while read -p"请输入命令: " NAME; do
        echo $NAME
done
[root@demo-c8 opt]# bash read2.sh 
请输入命令: cd
cd
请输入命令: rm
rm
请输入命令: ls
ls

范例: 根据lastb返回的登录失败信息, 批量将ip地址加入黑名单

[root@demo-c8 opt]# lastb
root     ssh:notty    10.0.0.1         Thu Sep 15 14:15 - 14:15  (00:00)
root     ssh:notty    10.0.0.1         Thu Sep 15 14:15 - 14:15  (00:00)
root     ssh:notty    10.0.0.1         Thu Sep 15 14:15 - 14:15  (00:00)
[root@demo-c8 opt]#  lastb | sed -nr '/ssh:/s#.* ((([0-9]{1,3}\.){3})[0-9]{1,3}).*#\1#p' | sort | uniq -c 
      3 10.0.0.1
      4 10.0.0.109
      1 10.0.0.88
[root@demo-c8 opt]# vim block_ip.sh 

#!/bin/bash
  
lastb | sed -nr '/ssh:/s#.* ((([0-9]{1,3}\.){3})[0-9]{1,3}).*#\1#p' | sort | uniq -c | \
        
while read COUNT IP; do
        if [ $COUNT -gt 3 ]; then
                iptables -A INPUT -s $IP -j REJECT
        fi      
done

执行脚本, 测试0.0.0.109访问10.0.0.108失败, 而10.0.0.88和10.0.0.1可以成功

root@u18:~# hostname -I
10.0.0.109 
root@u18:~# ssh root@10.0.0.108
ssh: connect to host 10.0.0.108 port 22: Connection refused
[14:55:06 root@CentOS-8-8 ~]#hostname -I
10.0.0.88 
[14:55:10 root@CentOS-8-8 ~]#ssh root@10.0.0.108
root@10.0.0.108's password: 
Activate the web console with: systemctl enable --now cockpit.socket

Last login: Thu Sep 15 14:51:49 2022 from 10.0.0.88
[root@demo-c8 ~]# 
root@u18:~# hostname -I
10.0.0.109 
root@u18:~# ssh root@10.0.0.108
ssh: connect to host 10.0.0.108 port 22: Connection refused

范例: 查看Shell类型为/sbin/nologin的用户名和UID

[root@demo-c8 opt]# vim nologin.sh

#!/bin/bash
while read LINE; do

        if [[ $LINE =~ /sbin/nologin$ ]];then
                echo $LINE | cut -d ":" -f1,3
        fi
done < /etc/passwd
[root@demo-c8 opt]# bash nologin.sh 
bin:1
daemon:2
adm:3
lp:4
mail:8
operator:11
games:12
ftp:14
nobody:65534
dbus:81
systemd-coredump:999
systemd-resolve:193
tss:59
polkitd:998
geoclue:997
rtkit:172
pulse:171
libstoragemgmt:996
qemu:107
usbmuxd:113
unbound:995
rpc:32
gluster:994
chrony:993
setroubleshoot:992
pipewire:991
saslauth:990
dnsmasq:984
radvd:75
clevis:983
cockpit-ws:982
cockpit-wsinstance:981
sssd:980
flatpak:979
colord:978
gdm:42
rpcuser:29
gnome-initial-setup:977
sshd:74
avahi:70
rngd:976
tcpdump:72

范例: while read实现猜数字

[root@demo-c8 opt]# vim guess.sh

#!/bin/bash
  
NUM=$[RANDOM%10]  # 先计算一个随机值
# echo $NUM

# 根据这个随机值进行猜数, 猜对了直接退出, 猜错了继续猜

while read -p "请输入[0-9]的数字 :" INPUT; do
        if [ $INPUT -eq $NUM ]; then
                echo "猜对了"
                break
        elif [ $INPUT -gt $NUM ];then
                echo "猜大了"
        else
                echo "猜小了"
        fi
done
[root@demo-c8 opt]# bash guess.sh 
请输入[0-9]的数字 :0
猜小了
请输入[0-9]的数字 :1
猜小了
请输入[0-9]的数字 :2
猜小了
请输入[0-9]的数字 :3
猜小了
请输入[0-9]的数字 :4
猜对了

4.2.8 select 循环与菜单

格式:

select NAME [ in WORD ... ; ] do COMMANDS; done

select VARIABLE in LIST; do

  循环体命令
done

说明:

范例:

PS3="请输入选项" #PS3变量就相当于read -p 接的提示信息
select MENU in "disable selinux" "create user"; do #这条命令就是生成对应的菜单
case $REPLY in #select默认把用户输入的信息存到REPLY变量, 利用case实现离散值判断
后续的操作就和while一样

5. 函数function

函数function是由若干条Shell命令组成的语句块,实现代码重用和模块化编程

它与Shell程序形式上是相似的,不同的是它不是一个单独的进程,不能独立运行,而是Shell程序的一部分

函数和Shell程序区别:

5.1 管理函数

函数由两部分组成:函数名和函数体
帮助参看:help function

5.1.1 定义函数

#语法一:
func_name (){
 ...函数体...
}

#语法二:
function func_name {
 ...函数体...
} 

#语法三:
function func_name () {
 ...函数体...
}

5.1.2 查看函数

#查看当前已定义的函数名
declare -F
#查看当前已定义的函数定义
declare -f
#查看指定当前已定义的函数名
declare -f func_name 
#查看当前已定义的函数名定义
declare -F func_name

5.1.3 删除函数

格式:

unset func_name

5.2 函数调用

函数的调用方式

调用:函数只有被调用才会执行,通过给定函数名调用函数,函数名出现的地方,会被自动替换为函数代码

函数的生命周期:被调用时创建,返回时终止

5.2.1 交互式环境调用函数

交互式环境下定义和使用函数

范例:

[root@demo-c8 ~]# dir(){
> ls -l
> }
[root@demo-c8 ~]# dir
total 8
-rw-------. 1 root root 1385 Aug 15 17:06 anaconda-ks.cfg
drwxr-xr-x. 2 root root    6 Aug 15 17:19 Desktop
drwxr-xr-x. 2 root root    6 Aug 15 17:19 Documents
drwxr-xr-x. 2 root root    6 Aug 15 17:19 Downloads
-rw-r--r--. 1 root root 1495 Aug 15 17:17 initial-setup-ks.cfg
drwxr-xr-x. 2 root root    6 Aug 15 17:19 Music
drwxr-xr-x. 2 root root    6 Aug 15 17:19 Pictures
drwxr-xr-x. 2 root root    6 Aug 15 17:19 Public
drwxr-xr-x. 2 root root    6 Aug 15 17:19 Templates
drwxr-xr-x. 2 root root    6 Aug 15 17:19 Videos

范例: 判断CentOS版本号

# 注意sed和grep的贪婪匹配, 需要把要取的内容和.*通过空格或者特定字符隔开
[root@demo-c8 opt]# sed -nr 's#CentOS.*([0-9]+).*#\1#p' /etc/centos-release 
4
[root@demo-c8 opt]# sed -nr 's#CentOS.* ([0-9]+).*#\1#p' /etc/centos-release 
8
# 函数名本身也是变量名, 所以只支持字母, 数字和下划线
[root@demo-c8 ~]# centos_release(){
> sed -nr 's#CentOS.* ([0-9]+).*#\1#p' /etc/centos-release 
> }
[root@demo-c8 ~]# centos_release 
8

范例: 函数定义后也可以对其结果进行判断, 之后执行操作

[ `centos_release` -eq 8 ] && CMD1 || CMD2

范例: 可以将函数执行结果存到变量里, 然后利用echo打印

local version=`centos_release`
echo $version

5.2.2 在脚本中定义及使用函数

函数在使用前必须定义,因此应将函数定义放在脚本开始部分,直至Shell首次发现它后才能使用,调用函数仅使用其函数名即可

范例: 在Shell脚本中, 定义和调用函数

[root@demo-c8 opt]# vim func1.sh

#!/bin/bash
  
#name: func1
hello(){

        echo "Hello, it is `date +"%F"` today"
}

echo "执行函数"
hello
echo "调用结束"
[root@demo-c8 opt]# bash func1.sh 
执行函数
Hello, it is 2022-09-14 today
调用结束

范例: 系统初始化脚本


5.2.3 使用函数文件

可以将经常使用的函数存入一个单独的函数文件,然后将函数文件载入Shell,再进行调用函数

文件名可任意选取,但最好与相关任务有某种联系,例如:functions

一旦函数文件载入Shell,就可以在命令行或脚本中调用函数. 可以使用delcare -fset 命令查看所有定义的函数,其输出列表包括已经载入Shell的所有函数

若要改动函数,首先用unset命令从Shell中删除函数. 改动完毕后,再重新载入此文件

实现函数文件的过程:

  1. 创建函数文件, 只存放函数的定义
  2. 在Shell脚本或交互式Shell中调用函数文件, 格式如下:
. filename 或 source  filename  

范例: 使用函数文件

  1. 创建函数文件, 并定义函数
[root@demo-c8 opt]# vim functions 

#!/bin/bash
#functions
hello(){
        echo run hello FUNCTION
}
hello2(){
        echo run hello2 FUNCTION        
}
  1. 载入函数文件到Shell
[root@demo-c8 opt]# . functions
  1. 调用函数文件内的函数
[root@demo-c8 opt]# hello
run hello FUNCTION
[root@demo-c8 opt]# hello2
run hello2 FUNCTION
  1. 在脚本中调用函数文件内的函数
[root@demo-c8 opt]# vim test.sh

#!/bin/bash
  
. functions # 调用函数文件, .或者source调用文件, 会改变当前Shell环境, 所以在脚本内才可以调用函数文件内的函数

hello
hello2
root@demo-c8 opt]# bash test.sh 
run hello FUNCTION
run hello2 FUNCTION
  1. 验证函数文件内定义的函数
[root@demo-c8 opt]# declare -f hello hello2
hello () 
{ 
    echo run hello FUNCTION
}
hello2 () 
{ 
    echo run hello2 FUNCTION
}

总结: 可以将常用的功能写在函数里, 然后把函数写到一个文件里, 一般命名为functions. 之后如果需要在脚本里调用这些函数, 只需要在脚本里先source或者. functions文本, 然后直接调用函数即可. 在哪个进程执行souce调用函数文件, 这些函数才能在哪个进程中使用.

范例: 在脚本中, 调用定义好的函数

  1. 先在自定义的functions文件中定义函数
[root@demo-c8 opt]# vim functions

color(){
case $1 in
        1) echo -e "\033[1;31m$2\033[0m"
        ;;
        2) echo -e "\033[1;32m$2\033[0m"
        ;;
        3) echo -e "\033[1;33m$2\033[0m"
        ;;
        *)
         echo -e "\033[1;34mWRONG\033[0m"
        ;;
        esac
}

centos_release(){
        sed -nr 's#CentOS.* ([0-9]+).*#\1#p' /etc/centos-release
}
  1. 在脚本中调用functions文件, 并调用函数
[root@demo-c8 opt]# vim test7.sh

#!/bin/bash

. functions

centos_release
color 2 helloworld
image.png

范例: CentOS系统自带的functions文件/etc/init.d/functions

[root@demo-c8 opt]# cat /etc/init.d/functions
# -*-Shell-script-*-
#
# functions This file contains functions to be used by most or all
#       shell scripts in the /etc/init.d directory.
#

TEXTDOMAIN=initscripts

# Make sure umask is sane
umask 022

# Set up a default search path.
PATH="/sbin:/usr/sbin:/bin:/usr/bin"
export PATH

if [ $PPID -ne 1 -a -z "$SYSTEMCTL_SKIP_REDIRECT" ] && \
        [ -d /run/systemd/system ] ; then
    case "$0" in
    /etc/init.d/*|/etc/rc.d/init.d/*)
        _use_systemctl=1
        ;;
    esac
fi
...

范例: 系统functions文件自带action函数, 可以显示命令执行ok还是false. action只是显示, 并不真正执行命令, 只是起到提示效果

. /etc/init.d/functions  # 这里不能用bash, 因为bash会开启子进程, 那么functions里定义的变量就不会在脚本里生效了. 所有要用source因为是在当前进程生效
[11:37:08 root@CentOS-8 ~]#action hello
hello                                                      [  OK  ]
[11:37:12 root@CentOS-8 ~]#action hello false
hello                                                      [FAILED]
[11:37:20 root@CentOS-8 ~]#action "rm -rf /*" # 只是显示ok, 并不会真正执行命令
rm -rf /*                                                  [  OK  ]

范例: 初始化脚本中调用/etc/init.d/functions


5.3 函数返回值

函数的执行结果返回值:

函数的退出状态码:

return    根据函数体最后一行代码的执行结果进行返回
return    0 自定义无错误返回
return    1-255 自定义有错误返回

如果不写return或者只写了return, 那么就是执行成功返回0, 执行失败返回非0数字

范例: 自定义return返回值

和Python等开发语言不同, Shell中的return只是用来定义函数退出的状态, 必须是数字, 不能返回其他信息

centso_ver(){
CMD
return 100  #函数执行到return就会自动退出, 不会执行函数内后续的命令, 并不会影响其他的函数
}
[root@demo-c8 opt]# vim test8.sh

#!/bin/bash
  
testfunc(){
        return 100
        echo "return后的命令不会执行"
}

testfunc # 调用函数

echo $? # 返回函数退出状态码

[root@demo-c8 opt]# bash test8.sh 
100

exit vs return

5.4 环境函数

类似于环境变量, 也可以定义环境函数, 使子进程也可以使用父进程定义的函数

范例: 默认情况, 子进程无法继承父进程定义的函数

[root@demo-c8 opt]# f1(){
> echo "f1"
> }
[root@demo-c8 opt]# bash # 开启子进程
[root@demo-c8 opt]# f1 # 尝试调用函数
bash: f1: command not found... # 调用失败
[root@demo-c8 opt]# exit # 退出子进程
exit
[root@demo-c8 opt]# f1
f1

定义环境函数:

export -f FUNCTION_NAME
declare -xf FUNCTION_NAME

查看环境函数:

export -f
declare -xf

5.5 函数参数

函数可以接收参数:

1. 在函数名后面以空白分隔指定参数列表即可,如:`testfunc arg1 arg2 ...
2. 在函数体中,可使用$1, $2, ...调用这些参数;还可以使用$@, $*, $#等特殊变量, 这样就可以把调用函数时给定的参数, 传给函数体

举例: 通过位置参数给函数体传参

和脚本一样, 函数也接收位置参数, 用法和脚本一样

[11:26:35 root@CentOS-8 ~]#color() {
> case $1 in
> 1)
> echo -e '\033[1;32mGreen\033[0m'
> ;;
> 2)
> echo -e '0\33[1;31mRed\033[0m'
> ;;
> esac
> }
[11:27:21 root@CentOS-8 ~]#color 1
Green
# $1定义颜色, $2定义打印的字符串
[root@demo-c8 opt]# color(){
> case $1 in
> 1)
> echo -e "\033[1;31m$2\033[0m"
> ;;
> 2)
> echo -e "\033[1;32m$2\033[0m"
> ;;
> 3)
> echo -e "\033[1;33m$2\033[0m"
> ;;
> *)
> echo -e "\033[1;34mWRONG\033[0m"
> ;;
> esac
> }
image.png

范例: 实现进度条打印

[root@demo-c8 opt]# vim progress_chart.sh

#!/bin/bash
  
function print_chars(){

        # 传入的第一个参数表示要打印的字符串
        local char="$1"
        # 传入的第二个参数表示要打印多少次指定的字符串
        local number="$2"
        local c
        for ((c=0;c<number;++c));do
                printf "$char"
        done
}

COLOR=32
declare -i end=50
for ((i=1;i<=end;++i));do
        printf "\e[1;$[COLOR]m\e[80D["
        print_chars "#" $i
        print_chars " " $((end -i))
        printf "] %3d%%\e[0m" $((i*2))
        sleep 0.1s
done
echo
image.png

5.6 函数变量

变量作用域:

[root@demo-c8 opt]# name=admin
[root@demo-c8 opt]# vim test1.sh
#!/bin/bash
echo name=$name
[root@demo-c8 opt]# bash test1.sh 
name=
[root@demo-c8 opt]# export name=admin
# 获取当前Shell进程的PID
[root@demo-c8 opt]# echo $BASHPID
2075
[root@demo-c8 opt]# vim test2.sh
#!/bin/bash
echo name=$name
echo $BASHPID # 或者子进程的PID
sleep 200
[root@demo-c8 opt]# bash test2.sh 
name=admin
6262 # 子进程PID

[root@demo-c8 opt]# pstree -p
├─sshd(1145)─┬─sshd(2045)───sshd(2069)───bash(2075)───bash(6262)───sleep(6263)

范例: 局部变量一般定义在函数内部, 使用local NAME

[root@demo-c8 opt]# vim test3.sh 

#!/bin/bash
  
f1(){
        local account=admin # local关键字定义局部变量, 只在函数声明周期内有效, 函数调用结束就会失效
                            # 局部变量不会影响全局变量
        echo $account
}
f1
echo local_account=$account
[root@demo-c8 opt]# bash test3.sh 
admin
local_account=

注意: Shell中定义的变量, 默认都是全局变量, 只影响当前Shell进程, 即使是函数内的默认变量, 也是全局变量, 会影响这个脚本的进程

范例: 函数内定义的全局变量, 会影响当前脚本Shell进程

#1. 在当前交互式Shell定义age全局变量
[root@demo-c8 opt]# age=20
#2. 在脚本中定义函数
[root@demo-c8 opt]# vim test4.sh

#!/bin/bash
  
echo $age  # 返回空, 因为父进程定义的全局变量不会继承给子进程
age=30
f2(){
        echo $age # 脚本内定义的变量是当前Shell进程的全局变量, 返回30
        age=40 # 在函数内部定义全局变量
        echo $age # 返回40
}
f2
echo $age # 函数内默认定义的也是全局变量, 返回40
#3. 执行脚本
[root@demo-c8 opt]# bash test4.sh

30
40
40
#4. 在交互式Shell调用变量
[root@demo-c8 opt]# echo $age
20

范例: 全局变量是和进程绑定的, 通过echo $BASHPID查看进程

注意: 每个进程中, 默认定义的都是该进程的全局变量, 终端命令行只是父进程, 而运行的脚本和脚本内定义的函数是终端命令行的子进程

注意: 在终端命令行定义的函数, 和终端命令行处于同一个进程

[root@demo-c8 opt]# echo $BASHPID
2075
[root@demo-c8 opt]# vim test5.sh

#!/bin/bash
echo $BASHPID
f3(){
        echo $BASHPID
}
f3
[root@demo-c8 opt]# bash test5.sh 
6466
6466
[root@demo-c8 opt]# vim test5.sh

#!/bin/bash
echo $BASHPID
f3(){
        echo $BASHPID
        sleep 200
}
f3
[root@demo-c8 opt]# pstree -p
           ├─sshd(1145)─┬─sshd(2045)───sshd(2069)───bash(2075)───bash(6486)───sleep(6487)

范例: 函数内, 如果局部变量和全局变量冲突, 那么局部变量优先级高

[root@demo-c8 opt]# vim test6.sh
#!/bin/bash
  
fun1(){
        n1=10
        local n1=20
        echo $n1 # 返回20, 局部变量和全局变量冲突时, 局部变量优先级高
}
fun1
[root@demo-c8 opt]# bash test6.sh 
20

注意: 因为局部变量和全局变量冲突, 所以, 函数内, 建议只用本地(局部)变量

范例: 函数体代码只有在被调用时, 才会执行

  1. 编辑自定义的functions文件, 添加函数代码
test(){

        student=admin
}
  1. 在命令行终端也定义student变量
[root@demo-c8 opt]# student=root
  1. 在命令行导入functions文件
[root@demo-c8 opt]# . functions 
  1. 此时, student变量还是root, 不会被修改, 因为还没有调用test函数
[root@demo-c8 opt]# echo $student
root
  1. 调用test函数, 执行函数体代码, student变量被改为admin. 这是因为函数体内默认定义的是全局变量, 会影响当前Shell进程, 而在终端执行. functions后, functions内的函数就和当前终端处在同一个进程了, 所有对于student会有同名冲突
[root@demo-c8 opt]# test
[root@demo-c8 opt]# echo $student
admin
  1. 解决方法: 在函数体内定义变量时, 使用局部变量, local NAME
[root@demo-c8 opt]# student=root
[root@demo-c8 opt]# vim functions
test(){
        local student=admin
}
[root@demo-c8 opt]# echo $student 
root
[root@demo-c8 opt]# . functions 
[root@demo-c8 opt]# echo $student 
root
[root@demo-c8 opt]# test
[root@demo-c8 opt]# echo $student 
root

5.7 函数递归

函数递归:函数直接或间接调用自身,注意递归层数,可能会陷入死循环

范例:

[root@centos8 ~]#func () { let i++;echo $i;echo "run func"; func; }
[root@centos8 ~]#func

范例: 阶乘计算

阶乘是基斯顿·卡曼于 1808 年发明的运算符号,是数学术语,一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0和1的阶乘为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)!
[root@demo-c8 opt]# vim fact.sh

#!/bin/bash
  

fact(){
        if [ $1 -eq 0 -o $1 -eq 1 ]; then  # 条件判断的中括号要有空格, 数学计算时不加空格
                echo 1
        else
                echo $[$1*$(fact $[$1-1])]
        fi
}
fact $1
[root@demo-c8 opt]# bash fact.sh 10
3628800

练习:

  1. 编写函数,实现OS的版本判断
  2. 编写函数,实现取出当前系统eth0的IP地址
  3. 编写函数,实现打印绿色OK和红色FAILED
  4. 编写函数,实现判断是否无位置参数,如无参数,提示错误
  5. 编写函数,实现两个数字做为参数,返回最大值
  6. 编写服务脚本/root/bin/testsrv.sh,完成如下要求
(1) 脚本可接受参数:start, stop, restart, status 
(2) 如果参数非此四者之一,提示使用格式后报错退出
(3) 如是start:则创建/var/lock/subsys/SCRIPT_NAME, 并显示“启动成功”
考虑:如果事先已经启动过一次,该如何处理?
(4) 如是stop:则删除/var/lock/subsys/SCRIPT_NAME, 并显示“停止完成”
考虑:如果事先已然停止过了,该如何处理?
(5) 如是restart,则先stop, 再start
考虑:如果本来没有start,如何处理?
(6) 如是status, 则如果/var/lock/subsys/SCRIPT_NAME文件存在,则显示“SCRIPT_NAME is 
running...”,如果/var/lock/subsys/SCRIPT_NAME文件不存在,则显示“SCRIPT_NAME is 
stopped...”
(7)在所有模式下禁止启动该服务,可用chkconfig 和 service命令管理
说明:SCRIPT_NAME为当前脚本名
  1. 编写脚本/root/bin/copycmd.sh
(1) 提示用户输入一个可执行命令名称
(2) 获取此命令所依赖到的所有库文件列表
(3) 复制命令至某目标目录(例如/mnt/sysroot)下的对应路径下
如:/bin/bash ==> /mnt/sysroot/bin/bash
    /usr/bin/passwd ==> /mnt/sysroot/usr/bin/passwd
(4) 复制此命令依赖到的所有库文件至目标目录下的对应路径下: 
如:/lib64/ld-linux-x86-64.so.2 ==> /mnt/sysroot/lib64/ld-linux-x86-64.so.2
(5)每次复制完成一个命令后,不要退出,而是提示用户键入新的要复制的命令,并重复完成上述功能;直到用户输入quit退出
  1. 斐波那契数列又称黄金分割数列,因数学家列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列: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阶斐波那契数列

  2. 汉诺塔(又称河内塔)问题是源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘,利用函数,实现N片盘的汉诺塔的移动步骤

上一篇下一篇

猜你喜欢

热点阅读