文章目录
- 1. shell 概述
- 2. shell 脚本编程基础
- 1. shell 概述
- 2. shell 脚本编程基础
1. shell 概述
1.1 shell 和 shell 脚本
shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务,在用户和内核之间充当翻译官的角色,是一个命令解释器。Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 shell。
shell 脚本(shell script),是一种为 shell 编写的脚本程序。业界所说的 shell 通常都是指 shell 脚本,shell 和 shell script 是两个不同的概念。
1.2 Shell 脚本应用场景
- 重复性操作
- 交互性任务
- 批量事务处理
- 服务运行状态监控
- 定时任务执行
…
1.3 Linux 中的 shell 类型
以 CentOS 7 为例
[root@c7-1 ~]#cat /etc/shells
/bin/sh #/bin/sh 是 bash 命令的软链接(已经被 /bin/bash 所替换)
/bin/bash #基准于 GNU 的框架下发展出的 shell
/usr/bin/sh #己经被 bash 所替换
/usr/bin/bash #centos 和 redhat 系统默认使用 bash shell
/bin/tcsh #csh 的增强版,与 csh 完全兼容,提供更多的功能
/bin/csh #已经被 /bin/bash 所替换(整合 c shell,提供更多的功能)
/sbin/nologin #奇怪的 shel1,这个 shell 可以让用户无法登录主机
1.4 脚本实例
[root@c7-1 ~]#curl -s http://47.117.130.238/hello.sh
# ------------------------------------------
# Filename: hello.sh
# Version: 1.0
# Date: 2021/06/06
# Author: gongboyi
# Email: 1520509800@qq.com
# Website: https://www.cnblogs.com/shenyuanhaojie/
# Description: This is the first script
# Copyright: 2021 zhou
# License: GPL
# ------------------------------------------
echo "hello world!"
[root@c7-1 ~]#curl -s http://47.117.130.238/hello.sh|bash
hello world!
2. shell 脚本编程基础
2.1 脚本调试
只检测脚本中的语法错误,无法检查出命令错误,不真正执行脚本
bash -n /path/to/script.sh
调试并执行
bash -x /path/to/script.sh
脚本错误常见的有三种:
语法错误:会导致后续的命令不继续执行,可以用 bash -n 检查错误,提示的出错行数不一定是准确的。
命令错误:后续的命令还会继续执行,用 bash -n 无法检查出来 ,可以使用 bash -x 进行观察。
逻辑错误:只能使用 bash -x 进行观察。
2.2 脚本执行方法
相对路径执行( ./script.sh )在脚本当前目录,脚本需要执行权限
绝对路径执行( /PATH/to/script.sh )无需在脚本目录,脚本需要执行权限
bash 执行( bash /PATH/to/script.sh )bash 后可跟绝对路径和相对路径,脚本无需执行权限
source 执行( source /PATH/to/script.sh )source 后可跟绝对路径和相对路径,脚本无需执行权限
bash < script.sh 或 cat script.sh | bash
示例:
[root@aliyunhost01html]#./hello.sh
hello world!
[root@aliyunhost01html]#/var/www/html/1.sh
hello world
[root@aliyunhost01html]#bash /var/www/html/1.sh
hello world
[root@aliyunhost01html]#source /var/www/html/1.sh
hello world
[root@aliyunhost01html]#bash < /var/www/html/1.sh
hello world
[root@aliyunhost01html]#cat /var/www/html/1.sh | bash
hello world
2.3 管道符号 |
将左侧命令的输出结果作为右侧命令的处理对象。
示例:
[root@c7-1 ~]#grep "/sbin/nologin" /etc/passwd | awk -F: '{print $1,$7}'
bin /sbin/nologin
daemon /sbin/nologin
adm /sbin/nologin
lp /sbin/nologin
mail /sbin/nologin
......
2.4 交互式硬件设备
标准输入:从该设备接收用户输入的数据
标准输出:通过该设备向用户输出数据
标准错误:通过该设备报告执行出错信息
类型 | 设备文件 | 文件描述编号 | 默认设备 |
---|---|---|---|
标准输入 | /dev/stdin | 0 | 键盘 |
标准输出 | /dev/stdout | 1 | 显示器 |
标准错误输出 | /dev/stderr | 2 | 显示器 |
[root@c7-1 ~]#ll /dev/std*
lrwxrwxrwx 1 root root 15 9月 2 10:02 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 9月 2 10:02 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 9月 2 10:02 /dev/stdout -> /proc/self/fd/1
2.5 重定向操作
类型 | 操作符 | 用途 |
重定向输入 | < | 从指定的文件读取数据,而不是从键盘输入 |
重定向输出 | > | 将输出结果保存到指定的文件(覆盖原有内容) |
>> | 将输出结果追加到指定的文件尾部 | |
标准错误输出 | 2> | 将错误信息保存到指定的文件(覆盖原有内容) |
2>> | 将错误信息追加到指定的文件中 | |
混合输出 | &> | 将标准输出、标准错误的输出保存到同一个文件中 |
示例:
#将 ./test.sh 的输出重定向到 log.txt 文件中,同时将标准错误也重定向到 log.txt 文件中
./test.sh > log.txt 2>&1
# nohup ... & 代表后台运行并且生成 nohup.log 日志文件
# command>/dev/null 代表命令输出结果导入到空设备
# 2>&1 代表将标准错误的内容重定向到标准输出,即将程序运行中的错误信息也打印出来
nohup ./test.sh>/dev/null 2>&1 &
参考:重定向理解
2.6 变量
(1) 变量的赋值与引用
① 变量赋值
name='value'
value 可以是以下多种形式:
直接字串:name='root'
变量引用:name="$USER"
命令引用:name=`COMMAND` 或者 name=$(COMMAND)
变量赋值是临时生效,当退出终端后,变量会自动删除,无法持久保存,脚本中的变量随着脚本结束,也会自动删除。
使用 read 从标准输入读取数值:
read -p "提示信息" [变量名]
read -p "提示信息" [变量名] < [文件]
常用选项:
-p #指定要显示的提示信息
-s #静默输入,一般用于密码
-n N #指定输入的字符长度 N
-d '字符' #输入结束符
-t N #TIMEOUT 为 N 秒
示例:
[root@c7-1 ~]#read -p "请输入密码:" passwd
请输入密码:120604
[root@c7-1 ~]#echo $passwd
120604
[root@c7-1 ~]#vim IP.txt
[root@c7-1 ~]#read -p "请输入IP地址:" IP < IP.txt
[root@c7-1 ~]#echo $IP
47.117.130.238
[root@c7-1 ~]#cat IP.txt
47.117.130.238
#实现运维工作菜单
echo -en "\E[$[RANDOM%7+31];1m"
cat <<EOF
请选择:
1)备份数据库
2)清理日志
3)软件升级
4)软件回滚
5)删库跑路
EOF
echo -en '\E[0m'
read -p "请输入上面数字1-5: " MENU
[ $MENU -eq 1 ] && ./backup.sh
[ $MENU -eq 2 ] && echo "清理日志"
[ $MENU -eq 3 ] && echo "软件升级"
[ $MENU -eq 4 ] && echo "软件回滚"
[ $MENU -eq 5 ] && echo "删库跑路"
② 变量引用
" " 弱引用,允许通过 $ 符号引用其他变量值
' ' 强引用,禁止引用其他变量值,$ 视为普通字符
` ` 命令引用,提取命令执行后的输出结果
示例:
[root@c7-1 ~]#username=zc
[root@c7-1 ~]#echo "you are $username"
you are zc
[root@c7-1 ~]#echo 'you are $username'
you are $username
[root@c7-1 ~]#echo `whoami`
root
(2) 变量类型
- 自定义变量:由用户自己定义、修改、使用,只在用户自己的 shell 环境中有效,因此又称为本地变量
- 特殊变量:环境变量、只读变量、位置变量、预定义变量
① 自定义变量
基本格式:
变量名=变量值
示例:
[root@c7-1 ~]#username=zc
[root@c7-1 ~]#echo $username
zc
[root@c7-1 ~]#set | grep username #set 显示用户自定义的变量
username=zc
local remote_opts="--username= --config-dir= --no-auth-cache";
--no-auth-cache --username=
[root@c7-1 ~]#unset username
[root@c7-1 ~]#set | grep username
_=username
local remote_opts="--username= --config-dir= --no-auth-cache";
--no-auth-cache --username=
② 环境变量
环境变量分为:
- 全局变量:不仅对 shell 可见,对其子进程也可见
- 局部变量:只能在定义其的进程中可见
显示所有环境变量:
env
printenv
export
declare -x
Bash 内建的环境变量:
PATH
SHELL
USER
UID
HOME
PWD
SHLVL #shell 的嵌套层数
LANG
MAIL
HOSTNAME
HISTSIZE
_ #下划线,表示前一命令的最后一个参数
声明全局变量:
export [变量名]
declare -x [变量名]
声明的全局变量虽然在环境变化的时候仍然生效,但是当系统重启后不会保存,要想永久生效可以保存到配置文件中。
变量配置文件:
系统层面:/etc/profile、/etc/bashrc
用户层面:~/.bash_profile、~/.bashrc、~/.bash_history、~/.bash_logout
#系统层面的配置文件通常在登录时加载,用户层面的配置文件只对单个用户生效
③ 只读变量
只读变量的声明:
readonly [变量名]
declare -r [变量名]
查看:
readonly [-p]
declare -r
只读变量无法删除或更改,当退出登录或者重启系统会失效。
示例:
[root@c7-1 ~]#readonly PI=3.14
[root@c7-1 ~]#echo $PI
3.14
[root@c7-1 ~]#PI=3.14159
-bash: PI: 只读变量
[root@c7-1 ~]#unset PI
-bash: unset: PI: 无法反设定: 只读 variable
[root@c7-1 ~]#logout
Connection closed by foreign host.
Disconnected from remote host(c7-1) at 21:53:33.
Type `help' to learn how to use Xshell prompt.
[c:\~]$
Reconnecting in 5 seconds. Press any key to exit local shell.
...
Connecting to 192.168.10.20:22...
Connection established.
To escape to local shell, press Ctrl+Alt+].
Last login: Fri Sep 3 21:33:20 2021 from 192.168.10.1
[root@c7-1 ~]#echo $PI
[root@c7-1 ~]#
④ 位置变量
在 bash shell 中内置的变量,在脚本代码中调用通过命令行传递给脚本的参数。
$1, $2, ... 对应第1个、第2个等参数,shift [n] 换位置
$0 命令本身,包括路径
$* 传递给脚本的所有参数,全部参数合为一个字符串
$@ 传递给脚本的所有参数,每个参数为独立字符串
$# 传递给脚本的参数的个数
注意:$@ $* 只在被双引号包起来的时候才会有差异
清空所有位置变量:
set --
示例:
[root@c7-1 ~]#cat test.sh
echo $1 $2 $3
echo $0
echo $?
echo $$
echo $!
echo $#
echo $*
echo $@
[root@c7-1 ~]#bash test.sh a b c
a b c
test.sh
0
72999
3
a b c
a b c
#实现添加用户
[root@c7-1 ~]#cat useradd.sh
#!/bin/bash
useradd $1
echo $2 | passwd --stdin $1
[root@c7-1 ~]#bash useradd.sh zc 123456
更改用户 zc 的密码 。
passwd:所有的身份验证令牌已经成功更新。
[root@c7-1 ~]#cat /etc/passwd|grep zc
zc:x:1001:1001::/home/zc:/bin/bash
#删库跑路之命令 rm 的安全实现
WARNING_COLOR="echo -e \E[1;31m"
END="\E[0m"
DIR=/tmp/`date +%F_%H-%M-%S`
mkdir $DIR
mv $* $DIR
${WARNING_COLOR}Move $* to $DIR $END
⑤ 预定义变量
预定义变量是在 Shell 一开始时就定义的变量,这一点和默认环境变量有些类似。不同的是,预定义变量不能重新定义,用户只能根据 Shell 的定义来使用这些变量。严格来说,位置参数变量也是预定义变量的一种,只是位置参数变量的作用比较统一,所以我们把位置参数变量单独划分为一类数量。
$? 表示前一条命令执行后的返回状态,返回值为О表示执行正确,返回任何非О值均表示执行出现异常
$$ 表示返回当前进程的进程号
$! 返回最后一个后台进程的进程号
用户可以在脚本中使用以下命令自定义退出状态码:
exit [n]
#脚本中一旦遇到exit命令,脚本会立即终止;终止退出状态取决于exit命令后面的数字
#如果未给脚本指定退出状态码,整个脚本的退出状态码取决于脚本中执行的最后一条命令的状态码
示例:
#测试网络通信
[root@c7-1 ~]#ping -c1 -W1 47.117.130.238 &> /dev/null
[root@c7-1 ~]#echo $?
0
#测试服务端口
[root@aliyunhost01~]#curl 127.0.0.1:80 &> /dev/null
[root@aliyunhost01~]#echo $?
0
(3) 变量的查看与删除
① 查看已定义的所有变量
set
② 删除变量
unset [变量名]
#只读变量无法删除
(4) 变量的赋值运算
+ 加
- 减
\* 乘
/ 除
% 取模,即取余数,示例:9%4=1,5%3=2
** 乘方
示例:
[root@c7-1 ~]#expr 2 + 3
5
[root@c7-1 ~]#num1=2
[root@c7-1 ~]#num2=3
[root@c7-1 ~]#expr $num1 + $num2
5
[root@c7-1 ~]#expr $num2 / $num1
1
[root@c7-1 ~]#expr $num1 \* $num2
6
[root@c7-1 ~]#expr $num2 % $num1
1
[root@c7-1 ~]#sum=$(($num1**$num2))
[root@c7-1 ~]#echo $sum
8
2.7 算数运算
(1) 实现算数运算的方式
let var=算术表达式
((var=算术表达式))
var=$[算术表达式]
var=$((算术表达式))
var=$(expr arg1 arg2 arg3 ...)
declare –i var = 数值
echo '算术表达式' | bc
(2) 增强型赋值
+= # i+=j 相当于 i=i+j
-= # i-=j 相当于 i=i-j
*= # i*=j 相当于 i=i*j
/= # i/=j 相当于 i=i/j
%= # i%=j 相当于 i=i%j
++ # i++,++i 相当于 i=i+1
-- # i--,--i 相当于 i=i-1
说明:i++ 和 ++i,i-- 和 --i 含义是不同的,我们用具体的示例来解释
# i 先将值赋值给了 j ,然后再完成自加
[root@c7-1 ~]#unset i j; i=1; let j=i++; echo "i=$i,j=$j"
i=2,j=1
# i 完成自加后赋值给 j
[root@c7-1 ~]#unset i j; i=1; let j=++i; echo "i=$i,j=$j"
i=2,j=2
(3) 逻辑运算
布尔型变量bool的取值只有false和true
true 1
false 0
& 与
1 与 1 = 1
1 与 0 = 0
0 与 1 = 0
0 与 0 = 0
| 或
1 或 1 = 1
1 或 0 = 1
0 或 1 = 1
0 或 0 = 0
! 非
! 1 = 0
! 0 = 1
^ 异或
相同为假,不同为真
1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0
&& 短路与
CMD1 短路与 CMD2
第一个CMD1结果为真(1),第二个CMD2必须要参与运算,才能得到最终的结果
第一个CMD1结果为假(0),总的结果必定为0,因此不需要执行CMD2
|| 短路或
CMD1 短路或 CMD2
第一个CMD1结果为真(1),总的结果必定为1,因此不需要执行CMD2
第一个CMD1结果为假(0),第二个CMD2必须要参与运算,才能得到最终的结果
(4) 随机数生成器变量 – $RANDOM
bash 有内建的随机数生成器:$RANDOM(0-32767)
#生成 0-49 之间随机数
echo $[$RANDOM%50]
#更换字体颜色,每次输出的结果显示不同的颜色
echo -e "\E[1;$[RANDOM%7+31]m hello \e[0m"
(5) “空” 设备 – /dev/null
“空” 设备,也被称为黑洞。任何输入到这个设备的数据都将被直接丢弃(但是操作返回成功 $? 值为 0)。最常用的用法是把不需要的输出重定向到这个文件。例如:
#将标准输出和错误输出重定向到 /dev/null,运行这个脚本不会输出任何信息到终端
run.sh 1>/dev/null 2>&1
(6) “零” 设备 – /dev/zero
“零” 设备,可以无限的提供空字符(0x00,ASCII代码NUL)。常用来生成一个特定大小的文件。例如:
#产生一个 1k 大小的文件 output.txt
dd if=/dev/zero of=./output.txt bs=1024 count=1
(7) 随机数生成器设备 – /dev/random /dev/urandom
随机数设备,提供不间断的随机字节流。二者的区别是 /dev/random 产生随机数据依赖系统中断,当系统中断不足时,/dev/random 设备会 “挂起”,因而产生数据速度较慢,但随机性好;/dev/urandom 不依赖系统中断,数据产生速度快,但随机性较低。
#生成随机密码
< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;
openssl rand -base64 32
date +%s | sha256sum | base64 | head -c 32 ; echo
tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1
strings /dev/urandom | grep -o '[[:alnum:]]' | head -n 30 | tr -d '\n'; echo
dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64 -w 0 | rev | cut -b 2- | rev
date | md5sum
参考:Linux 中的虚拟设备
2.8 echo -n/-e 的用法
echo -n 不换行输出
[root@c7-1 ~]#echo "123456"
123456
[root@c7-1 ~]#echo -n "123456"
123456[root@c7-1 ~]#
echo -e 处理特殊字符
若字符串中出现以下字符,则特别加以处理,而不会将它当成一般文字输出
\a 发出警告声
\b 删除前一个字符
\c 最后不加上换行符号
\f 换行但光标仍旧停留在原来的位置
\n 换行且光标移至行首
\r 光标移至行首,但不换行
\t 插入tab
\v 与\f相同
\\ 插入\字符
\nnn 插入nnn(八进制)所代表的ASCII字符
参考:
echo -n/-e 用法
echo 命令详解
echo 显示内容带颜色
2.9 条件测试命令
判断某需求是否满足,需要由测试机制来实现,专用的测试表达式需要由测试命令辅助完成测试过程。
若真,则状态码变量 $? 返回 0
若假,则状态码变量 $? 返回 1
条件测试格式:
test <选项> <测试的内容>
[ 选项 测试的内容 ]
[[ 选项 测试的内容 ]]
常用测试选项:
-e 测试目录或文件是否存在,[ -a file] 等于 [ -e file ]
-d 测试是否为目录
-f 测试是否为文件
-r 测试当前用户是否有权限读取
-w 测试当前用户是否有权限写入
-x 测试当前用户是否有权限执行
-u 测试文件是否存在且拥有 suid 权限
-g 测试文件是否存在且拥有 sgid 权限
-k 测试文件是否存在且拥有 sticky 权限
-z 如果 STRING 的长度为零则为真
-n 如果 STRING 的长度非零则为真
-b 测试文件是否存在并且是块设备文件
-c 测试文件是否存在并且是字符设备文件
-L 测试文件是否存在并且是链接文件
-p 测试文件是否存在并且是管道文件
-S 测试文件是否存在并且是套接字文件
-s 测试文件是否存在并且文件大小为空
其他文件属性测试选项:
-t fd fd 文件描述符是否在某终端已经打开
-N FILE 文件自从上一次被读取之后是否被修改过
-O FILE 当前有效用户是否为文件属主
-G FILE 当前有效用户是否为文件属组
FILE1 -ef FILE2 FILE1 是否是 FILE2 的硬链接
FILE1 -nt FILE2 FILE1 是否新于 FILE2(mtime)
FILE1 -ot FILE2 FILE1 是否旧于 FILE2
字符串比较:
[ STRING1 = STRING2 ] 两个字符串相等为真
[ STRING1 != STRING2 ] 两个字符串不等为真
[ STRING1 < STRING2 ] 如果 STRING1 按字典顺序排在 STRING2 之前,则为真
[ STRING1 > STRING2 ] 如果 STRING1 按字典顺序排在 STRING2 之后,则为真
[[ STRING1 == STRING2 ]] 左侧字符串是否和右侧的 PATTERN 相同
[[ STRING1 =~ STRING2 ]] 左侧字符串是否能够被右侧的正则表达式的 PATTERN 所匹配
逻辑运算与、或、非(条件组合测试):
[ ! EXPR ] 逻辑非,与表达相反的结果为真
[ EXPR1 -a EXPR2 ] 逻辑与,要同时满足多个表达式才为真,等同于 &&
[ EXPR1 -o EXPR2 ] 逻辑或,满足其中任意一个表达式即为真,等同于 ||
#如果 && 和 || 混合使用,&& 要在前,|| 在后
变量测试:
-v VAR 变量 VAR 是否设置
-r VAR 变量 VAR 是否设置并引用
数值比较:
-eq 是否等于
-ne 是否不等于
-gt 是否大于
-ge 是否大于等于
-lt 是否小于
-le 是否小于等于
2.10 流程控制
(1) if 条件语句
格式:
if COMMANDS; then COMMANDS; [ elif COMMANDS; then COMMANDS; ]... [ else
COMMANDS; ] fi
单分支:
if 判断条件;then
条件为真的分支代码
fi
双分支:
if 判断条件;then
条件为真的分支代码
else
条件为假的分支代码
fi
多分支:
if 判断条件1;then
条件1为真的分支代码
elif 判断条件2;then
条件2为真的分支代码
elif 判断条件3;then
条件3为真的分支代码
...
else
以上条件都为假的分支代码
fi
示例:
###单分支
[root@c7-1 ~]#cat if1.sh
#!/bin/bash
if ping -c1 -w2 www.baidu.com &> /dev/null;then
echo "网络连通"
fi
[root@c7-1 ~]#bash if1.sh
网络连通
###双分支
[root@c7-1 ~]#cat if2.sh
#!/bin/bash
IP=47.117.130.238
if ping -c1 -i 0.2 -w2 $IP &> /dev/null;then
echo "主机在线"
else
echo "主机断联"
fi
[root@c7-1 ~]#bash if2.sh
主机在线
###多分支
[root@c7-1 ~]#cat if3.sh
#!/bin/bash
read -p "请输入分数:" GRADE
if [ $GRADE -le 100 -a $GRADE -ge 80 ];then
echo "成绩优秀"
elif [ $GRADE -lt 80 -a $GRADE -ge 60 ];then
echo "成绩合格"
else
echo "不及格"
fi
[root@c7-1 ~]#bash if3.sh
请输入分数:65
成绩合格
(2) case 多分支语句
格式:
case WORD in [PATTERN [| PATTERN]...) COMMANDS ;;]... esac
分支结构:
case 变量值 in
模式一)
命令序列
;;
模式二)
命令序列
;;
模式三)
命令序列
;;
......
*)
默认命令序列
esac
case 支持 glob 风格的通配符:
* 任意长度任意字符
? 任意单个字符
[] 指定范围内的任意单个字符
| 逻辑或
示例:
###判断输入yes或no
[root@c7-1 ~]#cat YESorNO.sh
#!/bin/bash
read -p "请输入YES或NO:" VAR
VAR=`echo $VAR | tr 'A-Z' 'a-z'`
case $VAR in
y|ye|ys|yes)
echo "你输入的是YES"
;;
n|no)
echo "你输入的是NO"
;;
*)
echo "输入错误,请重新输入YES或NO"
esac
[root@c7-1 ~]#bash YESorNO.sh
请输入YES或NO:y
你输入的是YES
###httpd 服务控制
[root@c7-1 ~]#cat httpd.sh
#!/bin/bash
case $1 in
start)
/usr/bin/systemctl $1 httpd
/usr/bin/ps aux | grep httpd
echo "httpd start"
;;
stop)
/usr/bin/systemctl $1 httpd
/usr/bin/ps aux | grep httpd
echo "httpd stop"
;;
restart)
echo "httpd stop......"
/usr/bin/ps aux | grep httpd
/usr/bin/systemctl $1 httpd
echo "httpd restart......"
/usr/bin/ps aux | grep httpd
;;
status)
/usr/bin/systemctl $1 httpd
;;
*)
echo "请输入 start|stop|restart|status"
esac
[root@c7-1 ~]#./httpd.sh status | grep active
Active: active (running) since 一 2021-09-06 11:20:30 CST; 35s ago
(3) for 循环
格式:
#格式一
for 变量名 in 取值列表;
do
循环体
done
#格式二
for ((控制变量初始化;条件判断表达式;控制变量的修正表达式))
do
循环体
done
示例:
#!/bin/bash
for i in `seq 10`
do
echo $i
done
————————————————————
#!/bin/bash
#输出 10 次 hello world
a=10
for i in `seq $a`
do
echo "hello world"
done
————————————————————
#!/bin/bash
#输出 0-10,间隔 2
for i in {0..10..2}
do
echo $i
done
————————————————————
#!/bin/bash
for ((i=1;i<=5;i++))
do
echo $i
done
————————————————————
#!/bin/bash
#批量创建用户并修改密码
for i in {1..5}
do
useradd user$i
echo "123456" | passwd --stdin user$i
done
————————————————————
#!/bin/bash
#根据用户列表文件批量创建用户并修改密码
ULIST=$(cat /opt/user.txt)
for UNAME in $ULIST
do
useradd $UNAME
echo "123456"|passwd --stdin $UNAME &> /dev/null
done
————————————————————
#!/bin/bash
#根据用户列表文件删除用户
ULIST=$(cat /opt/user.txt)
for UNAME in $ULIST
do
userdel -r $UNAME &> /dev/null
done
————————————————————
#!/bin/bash
#测试文件中的 IP 是否连通
IPLIST=$(cat /opt/ipaddrs.txt)
for IP in $IPLIST
do
ping -c 3 -i 0.2 -W 3 $IP &> /dev/null
if [ $? -eq 0 ];then
echo "Host $IP is up"
else
echo "Host $IP is down"
fi
done
————————————————————
#!/bin/bash
#测试 IP 网段
for i in {1..255}
do
ping -c 2 -i 0.1 -W 1 192.168.10.$i &>/dev/dull
if [ $? -eq 0 ]
then
echo "192.168.10.$i is up"
else
echo "192.168.10.$i is down"
fi
done
————————————————————
#!/bin/bash
#密码登录与确认
init=123456
for i in {1..3}
do
read -p "请输入免密:" pass
if [ $pass == $init ];then
echo "密码正确"
exit
fi
done
echo "密码错误"
————————————————————
#!/bin/bash
#九九乘法表一
for i in {1..9};do
for j in `seq $i`;do
echo -e "${j}x${i}=$[i*j]\t\c"
done
echo
done
————————————————————
#!/bin/bash
#九九乘法表二
for ((i=1;i<10;i++));do
for((j=1;j<=i;j++));do
echo -e "${j}x${i}=$[i*j]\t\c"
done
echo
done
————————————————————
#!/bin/bash
#生成菱形
read -p "请输入菱形行数:" RM
for ((i=1;i<=$RM;i++))
do
for ((j=$RM;j>$i;j--))
do
echo -n " "
done
for ((n=1;n<=$i;n++))
do
echo -n "* "
done
echo ""
done
for ((k=$RM-1;k>=1;k--))
do
for ((m=$RM;m>$k;m--))
do
echo -n " "
done
for ((l=1;l<=$k;l++))
do
echo -n "* "
done
echo ""
done
(4) while 循环
格式:
while CONDITION;
do
循环体
done
说明:
CONDITION:循环控制条件,进入循环之前,先做一次判断,每一次循环之后会再次做判断。条件为 “true” 则执行一次循环,直到条件测试状态为 “false” 终止循环,因此:CONDTION 一般应该有循环控制变量,而此变量的值会在循环体不断地被修正。
进入条件:CONDITION 为 true
退出条件:CONDITION 为 false
示例:
#!/bin/bash
i=1
while [ $i -le 5 -a $i -ge 0 ]
do
echo $i
let i++
done
echo -e "\033[31m最后的i值为:$i\033[0m"
echo -e "\033[31m循环结束\033[0m"
————————————————————
#!/bin/bash
#httpd 服务控制
while pgrep "httpd" &> /dev/null
do
echo -e "\033[32mhttpd 服务正在运行\033[0m"
sleep 2
done
echo -e "\033[31mhttpd 服务异常\033[0m"
echo -e "\033[32m安装并启动 httpd 服务......\033[0m"
yum -y install httpd &>/dev/null
systemctl start httpd &>/dev/null && systemctl enable httpd &> /dev/null
systemctl status httpd
————————————————————
#!/bin/bash
#猜数字,猜对了通过 break 退出循环
NUM=8
while true
do
read -p "请输入数字:" SHUZI
if [ $SHUZI -eq $NUM ];then
echo "猜对了"
break
elif [ $SHUZI -gt $NUM ];then
echo "猜大了"
elif [ $SHUZI -lt $NUM ];then
echo "猜小了"
fi
done
————————————————————
#无限循环,break 退出
while true;
do
循环体
done
while true;
do
循环体
break
done
while 循环实现磁盘超载邮件告警:
#配置发邮件的邮箱
[root@centos8 ~]#cat .mailrc
set from=1520509800@qq.com
set smtp=smtp.qq.com
set smtp-auth-user=1520509800@qq.com
set smtp-auth-password=esvnhbnqocirbicf
set smtp-auth=login
set ssl-verify=ignore
#告警脚本
[root@centos8 ~]#cat while_diskcheck.sh
WARNING=80
while :;do
USE=`df | sed -rn '/^\/dev\/sd/s#.* ([0-9]+)%.*#\1#p' |sort -nr|head -n1`
if [ $USE -gt $WARNING ];then
echo Disk will be full from `hostname -I` | mail -s "disk warning" 1520509800@qq.com
fi
sleep 10
done
(5) until 循环
重复测试某个条件,只要条件不成立则反复执行。
格式:
until CONDITION;
do
循环体
done
说明:
进入条件:CONDITION 为 false
退出条件:CONDITION 为 true
无限循环:
until false;
do
循环体
done
示例:
#!/bin/bash
i=1
sum=0
until [ $i -eq 10 ]
do
sum=$[$i+$sum]
let i++
done
echo $sum
(6) 循环控制语句 continue
continue [N]:提前结束第 N 层的本轮循环,而直接进入下一轮判断,最内层为第 1 层。
格式:
while CONDITION1;
do
CMD1
...
if CONDITION2; then
continue
fi
CMDn
...
done
示例:
#!/bin/bash
for ((i=0;i<10;i++));
do
for((j=0;j<10;j++));
do
[ $j -eq 5 ] && continue 2
echo $j
done
echo ---------------------------
done
(7) 循环控制语句 break
break [N]:提前结束第 N 层整个循环,最内层为第 1 层。
格式:
while CONDITION1;
do
CMD1
...
if CONDITION2;then
break
fi
CMDn
...
done
示例:
#!/bin/bash
#可测试 break 和 break 2 输出结果有什么不同
#break 退出单个循环,break 2 退出 2 层循环
for ((i=0;i<10;i++));
do
for((j=0;j<10;j++));
do
[ $j -eq 5 ] && break 2
echo $j
done
echo ---------------------------
done
(8) 循环控制 shift 命令
shift [n] 用于将参量列表 list 左移指定次数,缺省为左移一次。
参量列表 list 一旦被移动,最左端的那个参数就从列表中删除。while 循环遍历位置参量列表时,常用到 shift。
示例:
#!/bin/bash
#step through all the positional parameters
until [ -z "$1" ]
do
echo "$1"
shift
done
echo
./shift.sh a b c d e f g h
————————————————————
#!/bin/bash
if [ $# -eq 0 ];then
echo "Usage: `basename $0` user1 user2 ..."
exit
fi
while [ "$1" ];do
if id $1 &> /dev/null;then
echo $1 is exist
else
useradd $1
echo "$1 is created"
fi
shift
done
echo "All user is created"
(9) while read 特殊用法
while 循环的特殊用法,遍历文件或文本的每一行。
格式:
while read line;
do
循环体
done < /PATH/FROM/FILE
示例:
#!/bin/bash
WARNING=80
MAIL=root@gongboyi.com
df|sed -nr "/^\/dev\/sd/s#^([^ ]+) .* ([0-9]+)%.*#\1 \2#p"|while read DEVICE USE;
do
if [ $USE -gt $WARNING ] ;then
echo "$DEVICE will be full,use:$USE" | mail -s "DISK WARNING" $MAIL
fi
done
(10) select 循环与菜单
格式:
select variable in list;
do
循环体命令
done
示例:
#!/bin/bash
#----------
sum=0
PS3="请点菜(1-6): "
select MENU in 北京烤鸭 佛跳墙 小龙虾 羊蝎子 火锅 点菜结束;do
case $REPLY in
1)
echo $MENU 价格是 100
let sum+=100
;;
2)
echo $MENU 价格是 88
let sum+=88
;;
3)
echo $MENU价格是 66
let sum+=66
;;
4)
echo $MENU 价格是 166
let sum+=166
;;
5)
echo $MENU 价格是 200
let sum+=200
;;
6)
echo "点菜结束,退出"
break
;;
*)
echo "点菜错误,重新选择"
;;
esac
done
echo "总价格是: $sum"
2.11 Shell 函数
(1) 函数介绍
函数 function 是由若干条 shell 命令组成的语句块,实现代码重用和模块化编程。它与 shell 程序形式上是相似的,不同的是它不是一个单独的进程,不能独立运行,而是shell程序的一部分。
shell 程序在子 shell 中运行,而 shell 函数在当前 shell 中运行。因此在当前 shell 中,函数可对 shell 中变量进行修改。
函数由两部分组成:函数名和函数体。
(2) 函数管理
① 定义函数
语法一:
func_name () {
...函数体...
}
语法二:
function func_name {
...函数体...
}
语法三:
function func_name () {
...函数体...
}
② 查看函数
#查看当前已定义的函数名
declare -F
#查看当前已定义的函数定义
declare -f
#指定查看当前已定义的函数名
declare -F func_name
#指定查看当前已定义的函数名定义
declare -f func_name
③ 删除函数
unset func_name
(3) 函数调用
函数的调用方式:
可在交互式环境下定义函数
可将函数放在脚本文件中作为它的一部分
可放在只包含函数的单独文件中
函数的生命周期:
被调用时创建,返回时终止
调用的概念:
函数只有被调用才会执行,通过给定函数名调用函数,函数名出现的地方,会被自动替换为函数
代码。
① 交互式环境调用函数
[root@c7-1 ~]#dir () {
> ls -l
> }
[root@c7-1 ~]#dir
总用量 4
-rw-r--r-- 1 root root 187 9月 8 17:47 1.sh
② 在脚本中定义及使用函数
函数在使用前必须定义,因此应将函数定义放在脚本开始部分,直至 shell 首次发现它后才能使用,调用函数仅使用其函数名即可。
#!/bin/bash
function fun () {
echo "hello world!"
}
#通过函数名调用函数
fun
————————————————————
#!/bin/bash
function test {
read -p "请输入数字:" num
return $[$num*2]
}
test
echo $?
————————————————————
#!/bin/bash
sum () {
read -p "请输入第一个参数:" NUM1
read -p "请输入第二个参数:" NUM2
echo -e "\033[32m你输入的两个数为:$NUM1 和 $NUM2\033[0m"
SUM=$(($NUM1+$NUM2))
echo -e "\033[31m两个数之和为:$SUM\033[0m"
}
sum
—————————————————————
#!/bin/bash
#函数调用函数
# local 一般用于局部变量声明
fa () {
if [ $1 -eq 1 ];then
echo 1
else
local tp=$[ $1 - 1 ]
local res=$(fa $tp)
echo $[ $1 * $res ]
fi
}
read -p "请输入:" num
res=$(fa $num)
echo $res
————————————————————
#!/bin/bash
#修改本地 repo 源
#请事先挂载光盘镜像,VMware设置里选择已连接
function repobackup {
cd /etc/yum.repos.d
mkdir repo.bak
mv *.repo repo.bak
mount /dev/sr0 /mnt > /dev/null
}
makelocalrepo () {
echo -e '
[local]
name=local
baseurl=file:///mnt
enabled=1
gpgcheck=0' > local.repo
}
uselocalrepo () {
yum clean all > /dev/null
yum makecache > /dev/null
yum -y install httpd > /dev/null
systemctl start httpd
}
#-----main-----#
repobackup
makelocalrepo
uselocalrepo
③ 使用函数文件
- 可以将经常使用的函数存入一个单独的函数文件,然后将函数文件载入 shell,再进行调用函数
- 文件名可任意选取,但最好与相关任务有某种联系,例如:functions
- 一旦函数文件载入 shell,就可以在命令行或脚本中调用函数。可以使用 delcare -f 或 set 命令查看所有定义的函数,其输出列表包括已经载入 shell 的所有函数
- 若要改动函数,首先用 unset 命令从 shell 中删除函数,改动完毕后,再重新载入此文件
实现函数文件的过程:
创建函数文件,只存放函数的定义
在 shell 脚本或交互式 shell 中调用函数文件,格式如下:
. filename 或 source filename
示例:
[root@centos8 ~]#cat functions
#!/bin/bash
#functions
hello () {
echo Run hello Function
}
hello2 () {
echo Run hello2 Function
}
[root@centos8 ~]#. functions
[root@centos8 ~]#hello
Run hello Function
[root@centos8 ~]#hello2
Run hello2 Function
[root@centos8 ~]#declare -f hello hello2
hello ()
{
echo Run hello Function
}
hello2 ()
{
echo Run hello2 Function
}
(4) 函数返回值
函数的执行结果返回值:
使用 echo 等命令进行输出
函数体中调用命令的输出结果
函数的退出状态码:
默认取决于函数中执行的最后一条命令的退出状态码
自定义退出状态码,其格式为:
return 从函数中返回,用最后状态命令决定返回值
return 0 无错误返回
return 1-255 有错误返回
(5) 环境函数
类拟于环境变量,也可以定义环境函数,使子进程也可使用父进程定义的函数。
定义环境函数:
export -f function_name
declare -xf function_name
查看环境函数:
export -f
declare -xf
(6) 函数参数
函数可以接受参数:
传递参数给函数,在函数名后面以空白分隔给定参数列表即可,如:func arg1 arg2 ...
在函数体中当中,可使用$1, $2, ...调用这些参数。还可以使用$@, $*, $#等特殊变量
(7) 函数变量
变量作用域:
普通变量:只在当前 shell 进程有效,为执行脚本会启动专用子 shell 进程。因此,本地变量的作用范围是当前 shell 脚本程序文件,包括脚本中的函数
环境变量:当前 shell 和子 shell 有效
本地变量:函数的生命周期结束时变量被自动销毁
注意:
如果函数中定义了普通变量,且名称和局部变量相同,则使用本地变量
由于普通变量和局部变量会冲突,建议在函数中只使用本地变量
在函数中定义本地变量的方法:
local NAME=VALUE
(8) 函数递归
函数递归:函数直接或间接调用自身,注意递归层数,可能会陷入死循环。
递归示例:
阶乘是基斯顿·卡曼于 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)!
示例:
#!/bin/bash
#
fact() {
if [ $1 -eq 0 -o $1 -eq 1 ]; then
echo 1
else
echo $[$1*$(fact $[$1-1])]
fi
}
fact $1
fork 炸弹是一种恶意程序,它的内部是一个不断在 fork 进程的无限循环,实质是一个简单的递归程序。由于程序是递归的,如果没有任何限制,这会导致这个简单的程序迅速耗尽系统里面的所有资源。
参考:
fork 炸弹
函数实现:
:(){ :|:& };:
bomb() { bomb | bomb & }; bomb
脚本实现:
cat bomb.sh
#!/bin/bash
./$0|./$0&
2.12 一些脚本相关工具
(1) 信号捕捉 trap
trap '触发指令' 信号
进程收到系统发出的指定信号后,将执行自定义指令,而不会执行原操作
trap '' 信号
忽略信号的操作
trap '-' 信号
恢复原信号的操作
trap -p
列出自定义信号操作
trap finish EXIT
当脚本退出时,执行 finish 函数
示例:
#!/bin/bash
trap 'echo "Press ctrl+c"' int quit
trap -p
for((i=0;i<=10;i++))
do
sleep 1
echo $i
done
trap '' int
trap -p
for((i=11;i<=20;i++))
do
sleep 1
echo $i
done
trap '-' int
trap -p
for((i=21;i<=30;i++))
do
sleep 1
echo $i
done
(2) 创建临时文件 mktemp
mktemp 命令用于创建并显示临时文件,可避免冲突。
格式:
mktemp [OPTION]... [TEMPLATE]
常见选项:
-d 创建临时目录
-p DIR或--tmpdir=DIR 指明临时文件所存放目录位置
示例:
mktemp /tmp/testXXX
tmpdir=`mktemp –d /tmp/testdirXXX`
mktemp --tmpdir=/testdir testXXXXXX
(3) 安装复制文件 install
install 命令格式:
install [OPTION]... [-T] SOURCE DEST 单文件
install [OPTION]... SOURCE... DIRECTORY
install [OPTION]... -t DIRECTORY SOURCE...
install [OPTION]... -d DIRECTORY...创建空目录
选项:
-m MODE,默认755
-o OWNER
-g GROUP
示例:
install -m 700 -o wang -g admins srcfile desfile
install -m 770 -d /testdir/installdir
(4) 交互式转化批处理工具 expect
expect 主要应用于自动化交互式操作的场景,借助 expect 处理交互的命令,可以将交互过程如:ssh 登录,ftp 登录等写在一个脚本上,使之自动完成。尤其适用于需要对多台服务器执行相同操作的环境中,可以大大提高系统管理人员的工作效率。
expect 语法:
expect [选项] [ -c cmds ] [ [ -[f|b] ] cmdfile ] [ args ]
常见选项:
-c:从命令行执行expect脚本,默认expect是交互执行的
-d:可以输出调试信息
expect 中相关命令:
spawn 启动新的进程
expect 从进程接收字符串
send 用于向进程发送字符串
interact 允许用户交互
exp_continue 匹配多个字符串在执行动作后加此命令
详细使用参考:交互式转化批处理工具 expect
2.13 数组
(1) 数组介绍
变量:存储单个元素的内存空间。
数组:存储多个元素的连续的内存空间,相当于多个变量的集合。
数组名和索引:
索引的编号从0开始,属于数值索引
索引可支持使用自定义的格式,而不仅是数值格式,即为关联索引,bash4.0 版本之后开始支持
bash 的数组支持稀疏格式(索引不连续)
(2) 声明数组
#普通数组可以不事先声明,直接使用
declare -a ARRAY_NAME
#关联数组必须先声明,再使用
declare -A ARRAY_NAME
#两者不可相互转换
(3) 数组赋值
① 一次只赋值一个元素
ARRAY_NAME[INDEX]=VALUE
示例:
weekdays[0]="Sunday"
weekdays[4]="Thursday"
② 一次赋值全部元素
ARRAY_NAME=("VAL1" "VAL2" "VAL3" ...)
示例:
title=("ceo" "coo" "cto")
num=({0..10})
alpha=({a..g})
file=( *.sh )
③ 只赋值特定元素
ARRAY_NAME=([0]="VAL1" [3]="VAL2" ...)
④ 交互式数组值对赋值
read -a ARRAY
(4) 显示所有数组
declare -a
示例:
[root@aliyunhost01~]#declare -a
declare -a BASH_ARGC='()'
declare -a BASH_ARGV='()'
declare -a BASH_LINENO='()'
declare -a BASH_SOURCE='()'
declare -ar BASH_VERSINFO='([0]="4" [1]="2" [2]="46" [3]="2" [4]="release" [5]="x86_64-redhat-linux-gnu")'
declare -a DIRSTACK='()'
declare -a FUNCNAME='()'
declare -a GROUPS='()'
declare -a PIPESTATUS='([0]="0")'
(5) 引用数组
① 引用数组元素
${ARRAY_NAME[INDEX]}
#如果省略[INDEX]表示引用下标为0的元素
示例:
[root@centos8 ~]#declare -a title=([0]="ceo" [1]="coo" [2]="cto")
[root@centos8 ~]#echo ${title[1]}
coo
[root@centos8 ~]#echo ${title}
ceo
[root@centos8 ~]#echo ${title[2]}
cto
[root@centos8 ~]#echo ${title[3]}
② 引用数组所有元素
${ARRAY_NAME[*]}
${ARRAY_NAME[@]}
示例:
[root@centos8 ~]#echo ${title[@]}
ceo coo cto
[root@centos8 ~]#echo ${title[*]}
ceo coo cto
③ 数组的长度,即数组中元素的个数
${#ARRAY_NAME[*]}
${#ARRAY_NAME[@]}
示例:
[root@centos8 ~]#echo ${#title[*]}
3
(6) 删除数组
① 删除数组中的某个元素
unset ARRAY[INDEX]
示例:
[root@centos8 ~]#echo ${title[*]}
ceo coo cto
[root@centos8 ~]#unset title[1]
[root@centos8 ~]#echo ${title[*]}
ceo cto
② 删除整个数组
unset ARRAY
示例:
[root@centos8 ~]#unset title
[root@centos8 ~]#echo ${title[*]}
[root@centos8 ~]#
(7) 数组数据处理
① 数组切片
${ARRAY[@]:offset:number}
offset #要跳过的元素个数
number #要取出的元素个数
#取偏移量之后的所有元素
{ARRAY[@]:offset}
示例:
[root@centos8 ~]#num=({0..10})
[root@centos8 ~]#echo ${num[*]:2:3}
2 3 4
[root@centos8 ~]#echo ${num[*]:6}
6 7 8 9 10
② 向数组中追加元素
ARRAY[${#ARRAY[*]}]=value
ARRAY[${#ARRAY[@]}]=value
示例:
[root@centos8 ~]#num[${#num[@]}]=11
[root@centos8 ~]#echo ${#num[@]}
12
[root@centos8 ~]#echo ${num[@]}
0 1 2 3 4 5 6 7 8 9 10 11
(8) 关联数组
declare -A ARRAY_NAME
ARRAY_NAME=([idx_name1]='val1' [idx_name2]='val2‘...)
注意:关联数组必须先声明再调用
示例:
[root@centos8 ~]#name[ceo]=mage
[root@centos8 ~]#name[cto]=wang
[root@centos8 ~]#name[coo]=zhang
[root@centos8 ~]#echo ${name[ceo]}
zhang
[root@centos8 ~]#echo ${name[cto]}
zhang
[root@centos8 ~]#echo ${name[coo]}
zhang
[root@centos8 ~]#echo ${name}
zhang
[root@centos8 ~]#declare -A name
-bash: declare: name: cannot convert indexed to associative array
[root@centos8 ~]#unset name
[root@centos8 ~]#declare -A name
[root@centos8 ~]#name[ceo]=mage
[root@centos8 ~]#name[cto]=wang
[root@centos8 ~]#name[coo]=zhang
[root@centos8 ~]#echo ${name[coo]}
zhang
[root@centos8 ~]#echo ${name[ceo]}
mage
[root@centos8 ~]#echo ${name[cto]}
wang
[root@centos8 ~]#echo ${name[*]}
mage wang zhang
关联数组参考:
Shell 关联数组
Shell 中的关联数组,获取数组索引
(9) 数组脚本示例
范例:生成 10 个随机数保存于数组中,并找出其最大值和最小值
#!/bin/bash
declare -i min max
declare -a nums
for ((i=0;i<10;i++));do
nums[$i]=$RANDOM
[ $i -eq 0 ] && min=${nums[0]} && max=${nums[0]}&& continue
[ ${nums[$i]} -gt $max ] && max=${nums[$i]}
[ ${nums[$i]} -lt $min ] && min=${nums[$i]}
done
echo "All numbers are ${nums[*]}"
echo Max is $max
echo Min is $min
范例:定义一个数组,数组中的元素对应的值是 /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"
范例:shell 脚本实现冒泡排序
#!/bin/bash
#a test about sort
echo "please input a number list"
read -a arrs
for((i=0;i<${#arrs[@]};i++)){
for((j=0;j<${#arrs[@]}-1;j++)){
if [[ ${arrs[j]} -gt ${arrs[j+1]} ]];then
tmp=${arrs[j]}
arrs[j]=${arrs[j+1]}
arrs[j+1]=$tmp
fi
}
}
echo "after sort"
echo ${arrs[@]}
数组参考:
文章目录
- 1. shell 概述
- 2. shell 脚本编程基础
- 1. shell 概述
- 2. shell 脚本编程基础
1. shell 概述
1.1 shell 和 shell 脚本
shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务,在用户和内核之间充当翻译官的角色,是一个命令解释器。Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 shell。
shell 脚本(shell script),是一种为 shell 编写的脚本程序。业界所说的 shell 通常都是指 shell 脚本,shell 和 shell script 是两个不同的概念。
1.2 Shell 脚本应用场景
- 重复性操作
- 交互性任务
- 批量事务处理
- 服务运行状态监控
- 定时任务执行
…
1.3 Linux 中的 shell 类型
以 CentOS 7 为例
[root@c7-1 ~]#cat /etc/shells
/bin/sh #/bin/sh 是 bash 命令的软链接(已经被 /bin/bash 所替换)
/bin/bash #基准于 GNU 的框架下发展出的 shell
/usr/bin/sh #己经被 bash 所替换
/usr/bin/bash #centos 和 redhat 系统默认使用 bash shell
/bin/tcsh #csh 的增强版,与 csh 完全兼容,提供更多的功能
/bin/csh #已经被 /bin/bash 所替换(整合 c shell,提供更多的功能)
/sbin/nologin #奇怪的 shel1,这个 shell 可以让用户无法登录主机
1.4 脚本实例
[root@c7-1 ~]#curl -s http://47.117.130.238/hello.sh
# ------------------------------------------
# Filename: hello.sh
# Version: 1.0
# Date: 2021/06/06
# Author: gongboyi
# Email: 1520509800@qq.com
# Website: https://www.cnblogs.com/shenyuanhaojie/
# Description: This is the first script
# Copyright: 2021 zhou
# License: GPL
# ------------------------------------------
echo "hello world!"
[root@c7-1 ~]#curl -s http://47.117.130.238/hello.sh|bash
hello world!
2. shell 脚本编程基础
2.1 脚本调试
只检测脚本中的语法错误,无法检查出命令错误,不真正执行脚本
bash -n /path/to/script.sh
调试并执行
bash -x /path/to/script.sh
脚本错误常见的有三种:
语法错误:会导致后续的命令不继续执行,可以用 bash -n 检查错误,提示的出错行数不一定是准确的。
命令错误:后续的命令还会继续执行,用 bash -n 无法检查出来 ,可以使用 bash -x 进行观察。
逻辑错误:只能使用 bash -x 进行观察。
2.2 脚本执行方法
相对路径执行( ./script.sh )在脚本当前目录,脚本需要执行权限
绝对路径执行( /PATH/to/script.sh )无需在脚本目录,脚本需要执行权限
bash 执行( bash /PATH/to/script.sh )bash 后可跟绝对路径和相对路径,脚本无需执行权限
source 执行( source /PATH/to/script.sh )source 后可跟绝对路径和相对路径,脚本无需执行权限
bash < script.sh 或 cat script.sh | bash
示例:
[root@aliyunhost01html]#./hello.sh
hello world!
[root@aliyunhost01html]#/var/www/html/1.sh
hello world
[root@aliyunhost01html]#bash /var/www/html/1.sh
hello world
[root@aliyunhost01html]#source /var/www/html/1.sh
hello world
[root@aliyunhost01html]#bash < /var/www/html/1.sh
hello world
[root@aliyunhost01html]#cat /var/www/html/1.sh | bash
hello world
2.3 管道符号 |
将左侧命令的输出结果作为右侧命令的处理对象。
示例:
[root@c7-1 ~]#grep "/sbin/nologin" /etc/passwd | awk -F: '{print $1,$7}'
bin /sbin/nologin
daemon /sbin/nologin
adm /sbin/nologin
lp /sbin/nologin
mail /sbin/nologin
......
2.4 交互式硬件设备
标准输入:从该设备接收用户输入的数据
标准输出:通过该设备向用户输出数据
标准错误:通过该设备报告执行出错信息
类型 | 设备文件 | 文件描述编号 | 默认设备 |
---|---|---|---|
标准输入 | /dev/stdin | 0 | 键盘 |
标准输出 | /dev/stdout | 1 | 显示器 |
标准错误输出 | /dev/stderr | 2 | 显示器 |
[root@c7-1 ~]#ll /dev/std*
lrwxrwxrwx 1 root root 15 9月 2 10:02 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 9月 2 10:02 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 9月 2 10:02 /dev/stdout -> /proc/self/fd/1
2.5 重定向操作
类型 | 操作符 | 用途 |
重定向输入 | < | 从指定的文件读取数据,而不是从键盘输入 |
重定向输出 | > | 将输出结果保存到指定的文件(覆盖原有内容) |
>> | 将输出结果追加到指定的文件尾部 | |
标准错误输出 | 2> | 将错误信息保存到指定的文件(覆盖原有内容) |
2>> | 将错误信息追加到指定的文件中 | |
混合输出 | &> | 将标准输出、标准错误的输出保存到同一个文件中 |
示例:
#将 ./test.sh 的输出重定向到 log.txt 文件中,同时将标准错误也重定向到 log.txt 文件中
./test.sh > log.txt 2>&1
# nohup ... & 代表后台运行并且生成 nohup.log 日志文件
# command>/dev/null 代表命令输出结果导入到空设备
# 2>&1 代表将标准错误的内容重定向到标准输出,即将程序运行中的错误信息也打印出来
nohup ./test.sh>/dev/null 2>&1 &
参考:重定向理解
2.6 变量
(1) 变量的赋值与引用
① 变量赋值
name='value'
value 可以是以下多种形式:
直接字串:name='root'
变量引用:name="$USER"
命令引用:name=`COMMAND` 或者 name=$(COMMAND)
变量赋值是临时生效,当退出终端后,变量会自动删除,无法持久保存,脚本中的变量随着脚本结束,也会自动删除。
使用 read 从标准输入读取数值:
read -p "提示信息" [变量名]
read -p "提示信息" [变量名] < [文件]
常用选项:
-p #指定要显示的提示信息
-s #静默输入,一般用于密码
-n N #指定输入的字符长度 N
-d '字符' #输入结束符
-t N #TIMEOUT 为 N 秒
示例:
[root@c7-1 ~]#read -p "请输入密码:" passwd
请输入密码:120604
[root@c7-1 ~]#echo $passwd
120604
[root@c7-1 ~]#vim IP.txt
[root@c7-1 ~]#read -p "请输入IP地址:" IP < IP.txt
[root@c7-1 ~]#echo $IP
47.117.130.238
[root@c7-1 ~]#cat IP.txt
47.117.130.238
#实现运维工作菜单
echo -en "\E[$[RANDOM%7+31];1m"
cat <<EOF
请选择:
1)备份数据库
2)清理日志
3)软件升级
4)软件回滚
5)删库跑路
EOF
echo -en '\E[0m'
read -p "请输入上面数字1-5: " MENU
[ $MENU -eq 1 ] && ./backup.sh
[ $MENU -eq 2 ] && echo "清理日志"
[ $MENU -eq 3 ] && echo "软件升级"
[ $MENU -eq 4 ] && echo "软件回滚"
[ $MENU -eq 5 ] && echo "删库跑路"
② 变量引用
" " 弱引用,允许通过 $ 符号引用其他变量值
' ' 强引用,禁止引用其他变量值,$ 视为普通字符
` ` 命令引用,提取命令执行后的输出结果
示例:
[root@c7-1 ~]#username=zc
[root@c7-1 ~]#echo "you are $username"
you are zc
[root@c7-1 ~]#echo 'you are $username'
you are $username
[root@c7-1 ~]#echo `whoami`
root
(2) 变量类型
- 自定义变量:由用户自己定义、修改、使用,只在用户自己的 shell 环境中有效,因此又称为本地变量
- 特殊变量:环境变量、只读变量、位置变量、预定义变量
① 自定义变量
基本格式:
变量名=变量值
示例:
[root@c7-1 ~]#username=zc
[root@c7-1 ~]#echo $username
zc
[root@c7-1 ~]#set | grep username #set 显示用户自定义的变量
username=zc
local remote_opts="--username= --config-dir= --no-auth-cache";
--no-auth-cache --username=
[root@c7-1 ~]#unset username
[root@c7-1 ~]#set | grep username
_=username
local remote_opts="--username= --config-dir= --no-auth-cache";
--no-auth-cache --username=
② 环境变量
环境变量分为:
- 全局变量:不仅对 shell 可见,对其子进程也可见
- 局部变量:只能在定义其的进程中可见
显示所有环境变量:
env
printenv
export
declare -x
Bash 内建的环境变量:
PATH
SHELL
USER
UID
HOME
PWD
SHLVL #shell 的嵌套层数
LANG
MAIL
HOSTNAME
HISTSIZE
_ #下划线,表示前一命令的最后一个参数
声明全局变量:
export [变量名]
declare -x [变量名]
声明的全局变量虽然在环境变化的时候仍然生效,但是当系统重启后不会保存,要想永久生效可以保存到配置文件中。
变量配置文件:
系统层面:/etc/profile、/etc/bashrc
用户层面:~/.bash_profile、~/.bashrc、~/.bash_history、~/.bash_logout
#系统层面的配置文件通常在登录时加载,用户层面的配置文件只对单个用户生效
③ 只读变量
只读变量的声明:
readonly [变量名]
declare -r [变量名]
查看:
readonly [-p]
declare -r
只读变量无法删除或更改,当退出登录或者重启系统会失效。
示例:
[root@c7-1 ~]#readonly PI=3.14
[root@c7-1 ~]#echo $PI
3.14
[root@c7-1 ~]#PI=3.14159
-bash: PI: 只读变量
[root@c7-1 ~]#unset PI
-bash: unset: PI: 无法反设定: 只读 variable
[root@c7-1 ~]#logout
Connection closed by foreign host.
Disconnected from remote host(c7-1) at 21:53:33.
Type `help' to learn how to use Xshell prompt.
[c:\~]$
Reconnecting in 5 seconds. Press any key to exit local shell.
...
Connecting to 192.168.10.20:22...
Connection established.
To escape to local shell, press Ctrl+Alt+].
Last login: Fri Sep 3 21:33:20 2021 from 192.168.10.1
[root@c7-1 ~]#echo $PI
[root@c7-1 ~]#
④ 位置变量
在 bash shell 中内置的变量,在脚本代码中调用通过命令行传递给脚本的参数。
$1, $2, ... 对应第1个、第2个等参数,shift [n] 换位置
$0 命令本身,包括路径
$* 传递给脚本的所有参数,全部参数合为一个字符串
$@ 传递给脚本的所有参数,每个参数为独立字符串
$# 传递给脚本的参数的个数
注意:$@ $* 只在被双引号包起来的时候才会有差异
清空所有位置变量:
set --
示例:
[root@c7-1 ~]#cat test.sh
echo $1 $2 $3
echo $0
echo $?
echo $$
echo $!
echo $#
echo $*
echo $@
[root@c7-1 ~]#bash test.sh a b c
a b c
test.sh
0
72999
3
a b c
a b c
#实现添加用户
[root@c7-1 ~]#cat useradd.sh
#!/bin/bash
useradd $1
echo $2 | passwd --stdin $1
[root@c7-1 ~]#bash useradd.sh zc 123456
更改用户 zc 的密码 。
passwd:所有的身份验证令牌已经成功更新。
[root@c7-1 ~]#cat /etc/passwd|grep zc
zc:x:1001:1001::/home/zc:/bin/bash
#删库跑路之命令 rm 的安全实现
WARNING_COLOR="echo -e \E[1;31m"
END="\E[0m"
DIR=/tmp/`date +%F_%H-%M-%S`
mkdir $DIR
mv $* $DIR
${WARNING_COLOR}Move $* to $DIR $END
⑤ 预定义变量
预定义变量是在 Shell 一开始时就定义的变量,这一点和默认环境变量有些类似。不同的是,预定义变量不能重新定义,用户只能根据 Shell 的定义来使用这些变量。严格来说,位置参数变量也是预定义变量的一种,只是位置参数变量的作用比较统一,所以我们把位置参数变量单独划分为一类数量。
$? 表示前一条命令执行后的返回状态,返回值为О表示执行正确,返回任何非О值均表示执行出现异常
$$ 表示返回当前进程的进程号
$! 返回最后一个后台进程的进程号
用户可以在脚本中使用以下命令自定义退出状态码:
exit [n]
#脚本中一旦遇到exit命令,脚本会立即终止;终止退出状态取决于exit命令后面的数字
#如果未给脚本指定退出状态码,整个脚本的退出状态码取决于脚本中执行的最后一条命令的状态码
示例:
#测试网络通信
[root@c7-1 ~]#ping -c1 -W1 47.117.130.238 &> /dev/null
[root@c7-1 ~]#echo $?
0
#测试服务端口
[root@aliyunhost01~]#curl 127.0.0.1:80 &> /dev/null
[root@aliyunhost01~]#echo $?
0
(3) 变量的查看与删除
① 查看已定义的所有变量
set
② 删除变量
unset [变量名]
#只读变量无法删除
(4) 变量的赋值运算
+ 加
- 减
\* 乘
/ 除
% 取模,即取余数,示例:9%4=1,5%3=2
** 乘方
示例:
[root@c7-1 ~]#expr 2 + 3
5
[root@c7-1 ~]#num1=2
[root@c7-1 ~]#num2=3
[root@c7-1 ~]#expr $num1 + $num2
5
[root@c7-1 ~]#expr $num2 / $num1
1
[root@c7-1 ~]#expr $num1 \* $num2
6
[root@c7-1 ~]#expr $num2 % $num1
1
[root@c7-1 ~]#sum=$(($num1**$num2))
[root@c7-1 ~]#echo $sum
8
2.7 算数运算
(1) 实现算数运算的方式
let var=算术表达式
((var=算术表达式))
var=$[算术表达式]
var=$((算术表达式))
var=$(expr arg1 arg2 arg3 ...)
declare –i var = 数值
echo '算术表达式' | bc
(2) 增强型赋值
+= # i+=j 相当于 i=i+j
-= # i-=j 相当于 i=i-j
*= # i*=j 相当于 i=i*j
/= # i/=j 相当于 i=i/j
%= # i%=j 相当于 i=i%j
++ # i++,++i 相当于 i=i+1
-- # i--,--i 相当于 i=i-1
说明:i++ 和 ++i,i-- 和 --i 含义是不同的,我们用具体的示例来解释
# i 先将值赋值给了 j ,然后再完成自加
[root@c7-1 ~]#unset i j; i=1; let j=i++; echo "i=$i,j=$j"
i=2,j=1
# i 完成自加后赋值给 j
[root@c7-1 ~]#unset i j; i=1; let j=++i; echo "i=$i,j=$j"
i=2,j=2
(3) 逻辑运算
布尔型变量bool的取值只有false和true
true 1
false 0
& 与
1 与 1 = 1
1 与 0 = 0
0 与 1 = 0
0 与 0 = 0
| 或
1 或 1 = 1
1 或 0 = 1
0 或 1 = 1
0 或 0 = 0
! 非
! 1 = 0
! 0 = 1
^ 异或
相同为假,不同为真
1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0
&& 短路与
CMD1 短路与 CMD2
第一个CMD1结果为真(1),第二个CMD2必须要参与运算,才能得到最终的结果
第一个CMD1结果为假(0),总的结果必定为0,因此不需要执行CMD2
|| 短路或
CMD1 短路或 CMD2
第一个CMD1结果为真(1),总的结果必定为1,因此不需要执行CMD2
第一个CMD1结果为假(0),第二个CMD2必须要参与运算,才能得到最终的结果
(4) 随机数生成器变量 – $RANDOM
bash 有内建的随机数生成器:$RANDOM(0-32767)
#生成 0-49 之间随机数
echo $[$RANDOM%50]
#更换字体颜色,每次输出的结果显示不同的颜色
echo -e "\E[1;$[RANDOM%7+31]m hello \e[0m"
(5) “空” 设备 – /dev/null
“空” 设备,也被称为黑洞。任何输入到这个设备的数据都将被直接丢弃(但是操作返回成功 $? 值为 0)。最常用的用法是把不需要的输出重定向到这个文件。例如:
#将标准输出和错误输出重定向到 /dev/null,运行这个脚本不会输出任何信息到终端
run.sh 1>/dev/null 2>&1
(6) “零” 设备 – /dev/zero
“零” 设备,可以无限的提供空字符(0x00,ASCII代码NUL)。常用来生成一个特定大小的文件。例如:
#产生一个 1k 大小的文件 output.txt
dd if=/dev/zero of=./output.txt bs=1024 count=1
(7) 随机数生成器设备 – /dev/random /dev/urandom
随机数设备,提供不间断的随机字节流。二者的区别是 /dev/random 产生随机数据依赖系统中断,当系统中断不足时,/dev/random 设备会 “挂起”,因而产生数据速度较慢,但随机性好;/dev/urandom 不依赖系统中断,数据产生速度快,但随机性较低。
#生成随机密码
< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;
openssl rand -base64 32
date +%s | sha256sum | base64 | head -c 32 ; echo
tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1
strings /dev/urandom | grep -o '[[:alnum:]]' | head -n 30 | tr -d '\n'; echo
dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64 -w 0 | rev | cut -b 2- | rev
date | md5sum
参考:Linux 中的虚拟设备
2.8 echo -n/-e 的用法
echo -n 不换行输出
[root@c7-1 ~]#echo "123456"
123456
[root@c7-1 ~]#echo -n "123456"
123456[root@c7-1 ~]#
echo -e 处理特殊字符
若字符串中出现以下字符,则特别加以处理,而不会将它当成一般文字输出
\a 发出警告声
\b 删除前一个字符
\c 最后不加上换行符号
\f 换行但光标仍旧停留在原来的位置
\n 换行且光标移至行首
\r 光标移至行首,但不换行
\t 插入tab
\v 与\f相同
\\ 插入\字符
\nnn 插入nnn(八进制)所代表的ASCII字符
参考:
echo -n/-e 用法
echo 命令详解
echo 显示内容带颜色
2.9 条件测试命令
判断某需求是否满足,需要由测试机制来实现,专用的测试表达式需要由测试命令辅助完成测试过程。
若真,则状态码变量 $? 返回 0
若假,则状态码变量 $? 返回 1
条件测试格式:
test <选项> <测试的内容>
[ 选项 测试的内容 ]
[[ 选项 测试的内容 ]]
常用测试选项:
-e 测试目录或文件是否存在,[ -a file] 等于 [ -e file ]
-d 测试是否为目录
-f 测试是否为文件
-r 测试当前用户是否有权限读取
-w 测试当前用户是否有权限写入
-x 测试当前用户是否有权限执行
-u 测试文件是否存在且拥有 suid 权限
-g 测试文件是否存在且拥有 sgid 权限
-k 测试文件是否存在且拥有 sticky 权限
-z 如果 STRING 的长度为零则为真
-n 如果 STRING 的长度非零则为真
-b 测试文件是否存在并且是块设备文件
-c 测试文件是否存在并且是字符设备文件
-L 测试文件是否存在并且是链接文件
-p 测试文件是否存在并且是管道文件
-S 测试文件是否存在并且是套接字文件
-s 测试文件是否存在并且文件大小为空
其他文件属性测试选项:
-t fd fd 文件描述符是否在某终端已经打开
-N FILE 文件自从上一次被读取之后是否被修改过
-O FILE 当前有效用户是否为文件属主
-G FILE 当前有效用户是否为文件属组
FILE1 -ef FILE2 FILE1 是否是 FILE2 的硬链接
FILE1 -nt FILE2 FILE1 是否新于 FILE2(mtime)
FILE1 -ot FILE2 FILE1 是否旧于 FILE2
字符串比较:
[ STRING1 = STRING2 ] 两个字符串相等为真
[ STRING1 != STRING2 ] 两个字符串不等为真
[ STRING1 < STRING2 ] 如果 STRING1 按字典顺序排在 STRING2 之前,则为真
[ STRING1 > STRING2 ] 如果 STRING1 按字典顺序排在 STRING2 之后,则为真
[[ STRING1 == STRING2 ]] 左侧字符串是否和右侧的 PATTERN 相同
[[ STRING1 =~ STRING2 ]] 左侧字符串是否能够被右侧的正则表达式的 PATTERN 所匹配
逻辑运算与、或、非(条件组合测试):
[ ! EXPR ] 逻辑非,与表达相反的结果为真
[ EXPR1 -a EXPR2 ] 逻辑与,要同时满足多个表达式才为真,等同于 &&
[ EXPR1 -o EXPR2 ] 逻辑或,满足其中任意一个表达式即为真,等同于 ||
#如果 && 和 || 混合使用,&& 要在前,|| 在后
变量测试:
-v VAR 变量 VAR 是否设置
-r VAR 变量 VAR 是否设置并引用
数值比较:
-eq 是否等于
-ne 是否不等于
-gt 是否大于
-ge 是否大于等于
-lt 是否小于
-le 是否小于等于
2.10 流程控制
(1) if 条件语句
格式:
if COMMANDS; then COMMANDS; [ elif COMMANDS; then COMMANDS; ]... [ else
COMMANDS; ] fi
单分支:
if 判断条件;then
条件为真的分支代码
fi
双分支:
if 判断条件;then
条件为真的分支代码
else
条件为假的分支代码
fi
多分支:
if 判断条件1;then
条件1为真的分支代码
elif 判断条件2;then
条件2为真的分支代码
elif 判断条件3;then
条件3为真的分支代码
...
else
以上条件都为假的分支代码
fi
示例:
###单分支
[root@c7-1 ~]#cat if1.sh
#!/bin/bash
if ping -c1 -w2 www.baidu.com &> /dev/null;then
echo "网络连通"
fi
[root@c7-1 ~]#bash if1.sh
网络连通
###双分支
[root@c7-1 ~]#cat if2.sh
#!/bin/bash
IP=47.117.130.238
if ping -c1 -i 0.2 -w2 $IP &> /dev/null;then
echo "主机在线"
else
echo "主机断联"
fi
[root@c7-1 ~]#bash if2.sh
主机在线
###多分支
[root@c7-1 ~]#cat if3.sh
#!/bin/bash
read -p "请输入分数:" GRADE
if [ $GRADE -le 100 -a $GRADE -ge 80 ];then
echo "成绩优秀"
elif [ $GRADE -lt 80 -a $GRADE -ge 60 ];then
echo "成绩合格"
else
echo "不及格"
fi
[root@c7-1 ~]#bash if3.sh
请输入分数:65
成绩合格
(2) case 多分支语句
格式:
case WORD in [PATTERN [| PATTERN]...) COMMANDS ;;]... esac
分支结构:
case 变量值 in
模式一)
命令序列
;;
模式二)
命令序列
;;
模式三)
命令序列
;;
......
*)
默认命令序列
esac
case 支持 glob 风格的通配符:
* 任意长度任意字符
? 任意单个字符
[] 指定范围内的任意单个字符
| 逻辑或
示例:
###判断输入yes或no
[root@c7-1 ~]#cat YESorNO.sh
#!/bin/bash
read -p "请输入YES或NO:" VAR
VAR=`echo $VAR | tr 'A-Z' 'a-z'`
case $VAR in
y|ye|ys|yes)
echo "你输入的是YES"
;;
n|no)
echo "你输入的是NO"
;;
*)
echo "输入错误,请重新输入YES或NO"
esac
[root@c7-1 ~]#bash YESorNO.sh
请输入YES或NO:y
你输入的是YES
###httpd 服务控制
[root@c7-1 ~]#cat httpd.sh
#!/bin/bash
case $1 in
start)
/usr/bin/systemctl $1 httpd
/usr/bin/ps aux | grep httpd
echo "httpd start"
;;
stop)
/usr/bin/systemctl $1 httpd
/usr/bin/ps aux | grep httpd
echo "httpd stop"
;;
restart)
echo "httpd stop......"
/usr/bin/ps aux | grep httpd
/usr/bin/systemctl $1 httpd
echo "httpd restart......"
/usr/bin/ps aux | grep httpd
;;
status)
/usr/bin/systemctl $1 httpd
;;
*)
echo "请输入 start|stop|restart|status"
esac
[root@c7-1 ~]#./httpd.sh status | grep active
Active: active (running) since 一 2021-09-06 11:20:30 CST; 35s ago
(3) for 循环
格式:
#格式一
for 变量名 in 取值列表;
do
循环体
done
#格式二
for ((控制变量初始化;条件判断表达式;控制变量的修正表达式))
do
循环体
done
示例:
#!/bin/bash
for i in `seq 10`
do
echo $i
done
————————————————————
#!/bin/bash
#输出 10 次 hello world
a=10
for i in `seq $a`
do
echo "hello world"
done
————————————————————
#!/bin/bash
#输出 0-10,间隔 2
for i in {0..10..2}
do
echo $i
done
————————————————————
#!/bin/bash
for ((i=1;i<=5;i++))
do
echo $i
done
————————————————————
#!/bin/bash
#批量创建用户并修改密码
for i in {1..5}
do
useradd user$i
echo "123456" | passwd --stdin user$i
done
————————————————————
#!/bin/bash
#根据用户列表文件批量创建用户并修改密码
ULIST=$(cat /opt/user.txt)
for UNAME in $ULIST
do
useradd $UNAME
echo "123456"|passwd --stdin $UNAME &> /dev/null
done
————————————————————
#!/bin/bash
#根据用户列表文件删除用户
ULIST=$(cat /opt/user.txt)
for UNAME in $ULIST
do
userdel -r $UNAME &> /dev/null
done
————————————————————
#!/bin/bash
#测试文件中的 IP 是否连通
IPLIST=$(cat /opt/ipaddrs.txt)
for IP in $IPLIST
do
ping -c 3 -i 0.2 -W 3 $IP &> /dev/null
if [ $? -eq 0 ];then
echo "Host $IP is up"
else
echo "Host $IP is down"
fi
done
————————————————————
#!/bin/bash
#测试 IP 网段
for i in {1..255}
do
ping -c 2 -i 0.1 -W 1 192.168.10.$i &>/dev/dull
if [ $? -eq 0 ]
then
echo "192.168.10.$i is up"
else
echo "192.168.10.$i is down"
fi
done
————————————————————
#!/bin/bash
#密码登录与确认
init=123456
for i in {1..3}
do
read -p "请输入免密:" pass
if [ $pass == $init ];then
echo "密码正确"
exit
fi
done
echo "密码错误"
————————————————————
#!/bin/bash
#九九乘法表一
for i in {1..9};do
for j in `seq $i`;do
echo -e "${j}x${i}=$[i*j]\t\c"
done
echo
done
————————————————————
#!/bin/bash
#九九乘法表二
for ((i=1;i<10;i++));do
for((j=1;j<=i;j++));do
echo -e "${j}x${i}=$[i*j]\t\c"
done
echo
done
————————————————————
#!/bin/bash
#生成菱形
read -p "请输入菱形行数:" RM
for ((i=1;i<=$RM;i++))
do
for ((j=$RM;j>$i;j--))
do
echo -n " "
done
for ((n=1;n<=$i;n++))
do
echo -n "* "
done
echo ""
done
for ((k=$RM-1;k>=1;k--))
do
for ((m=$RM;m>$k;m--))
do
echo -n " "
done
for ((l=1;l<=$k;l++))
do
echo -n "* "
done
echo ""
done
(4) while 循环
格式:
while CONDITION;
do
循环体
done
说明:
CONDITION:循环控制条件,进入循环之前,先做一次判断,每一次循环之后会再次做判断。条件为 “true” 则执行一次循环,直到条件测试状态为 “false” 终止循环,因此:CONDTION 一般应该有循环控制变量,而此变量的值会在循环体不断地被修正。
进入条件:CONDITION 为 true
退出条件:CONDITION 为 false
示例:
#!/bin/bash
i=1
while [ $i -le 5 -a $i -ge 0 ]
do
echo $i
let i++
done
echo -e "\033[31m最后的i值为:$i\033[0m"
echo -e "\033[31m循环结束\033[0m"
————————————————————
#!/bin/bash
#httpd 服务控制
while pgrep "httpd" &> /dev/null
do
echo -e "\033[32mhttpd 服务正在运行\033[0m"
sleep 2
done
echo -e "\033[31mhttpd 服务异常\033[0m"
echo -e "\033[32m安装并启动 httpd 服务......\033[0m"
yum -y install httpd &>/dev/null
systemctl start httpd &>/dev/null && systemctl enable httpd &> /dev/null
systemctl status httpd
————————————————————
#!/bin/bash
#猜数字,猜对了通过 break 退出循环
NUM=8
while true
do
read -p "请输入数字:" SHUZI
if [ $SHUZI -eq $NUM ];then
echo "猜对了"
break
elif [ $SHUZI -gt $NUM ];then
echo "猜大了"
elif [ $SHUZI -lt $NUM ];then
echo "猜小了"
fi
done
————————————————————
#无限循环,break 退出
while true;
do
循环体
done
while true;
do
循环体
break
done
while 循环实现磁盘超载邮件告警:
#配置发邮件的邮箱
[root@centos8 ~]#cat .mailrc
set from=1520509800@qq.com
set smtp=smtp.qq.com
set smtp-auth-user=1520509800@qq.com
set smtp-auth-password=esvnhbnqocirbicf
set smtp-auth=login
set ssl-verify=ignore
#告警脚本
[root@centos8 ~]#cat while_diskcheck.sh
WARNING=80
while :;do
USE=`df | sed -rn '/^\/dev\/sd/s#.* ([0-9]+)%.*#\1#p' |sort -nr|head -n1`
if [ $USE -gt $WARNING ];then
echo Disk will be full from `hostname -I` | mail -s "disk warning" 1520509800@qq.com
fi
sleep 10
done
(5) until 循环
重复测试某个条件,只要条件不成立则反复执行。
格式:
until CONDITION;
do
循环体
done
说明:
进入条件:CONDITION 为 false
退出条件:CONDITION 为 true
无限循环:
until false;
do
循环体
done
示例:
#!/bin/bash
i=1
sum=0
until [ $i -eq 10 ]
do
sum=$[$i+$sum]
let i++
done
echo $sum
(6) 循环控制语句 continue
continue [N]:提前结束第 N 层的本轮循环,而直接进入下一轮判断,最内层为第 1 层。
格式:
while CONDITION1;
do
CMD1
...
if CONDITION2; then
continue
fi
CMDn
...
done
示例:
#!/bin/bash
for ((i=0;i<10;i++));
do
for((j=0;j<10;j++));
do
[ $j -eq 5 ] && continue 2
echo $j
done
echo ---------------------------
done
(7) 循环控制语句 break
break [N]:提前结束第 N 层整个循环,最内层为第 1 层。
格式:
while CONDITION1;
do
CMD1
...
if CONDITION2;then
break
fi
CMDn
...
done
示例:
#!/bin/bash
#可测试 break 和 break 2 输出结果有什么不同
#break 退出单个循环,break 2 退出 2 层循环
for ((i=0;i<10;i++));
do
for((j=0;j<10;j++));
do
[ $j -eq 5 ] && break 2
echo $j
done
echo ---------------------------
done
(8) 循环控制 shift 命令
shift [n] 用于将参量列表 list 左移指定次数,缺省为左移一次。
参量列表 list 一旦被移动,最左端的那个参数就从列表中删除。while 循环遍历位置参量列表时,常用到 shift。
示例:
#!/bin/bash
#step through all the positional parameters
until [ -z "$1" ]
do
echo "$1"
shift
done
echo
./shift.sh a b c d e f g h
————————————————————
#!/bin/bash
if [ $# -eq 0 ];then
echo "Usage: `basename $0` user1 user2 ..."
exit
fi
while [ "$1" ];do
if id $1 &> /dev/null;then
echo $1 is exist
else
useradd $1
echo "$1 is created"
fi
shift
done
echo "All user is created"
(9) while read 特殊用法
while 循环的特殊用法,遍历文件或文本的每一行。
格式:
while read line;
do
循环体
done < /PATH/FROM/FILE
示例:
#!/bin/bash
WARNING=80
MAIL=root@gongboyi.com
df|sed -nr "/^\/dev\/sd/s#^([^ ]+) .* ([0-9]+)%.*#\1 \2#p"|while read DEVICE USE;
do
if [ $USE -gt $WARNING ] ;then
echo "$DEVICE will be full,use:$USE" | mail -s "DISK WARNING" $MAIL
fi
done
(10) select 循环与菜单
格式:
select variable in list;
do
循环体命令
done
示例:
#!/bin/bash
#----------
sum=0
PS3="请点菜(1-6): "
select MENU in 北京烤鸭 佛跳墙 小龙虾 羊蝎子 火锅 点菜结束;do
case $REPLY in
1)
echo $MENU 价格是 100
let sum+=100
;;
2)
echo $MENU 价格是 88
let sum+=88
;;
3)
echo $MENU价格是 66
let sum+=66
;;
4)
echo $MENU 价格是 166
let sum+=166
;;
5)
echo $MENU 价格是 200
let sum+=200
;;
6)
echo "点菜结束,退出"
break
;;
*)
echo "点菜错误,重新选择"
;;
esac
done
echo "总价格是: $sum"
2.11 Shell 函数
(1) 函数介绍
函数 function 是由若干条 shell 命令组成的语句块,实现代码重用和模块化编程。它与 shell 程序形式上是相似的,不同的是它不是一个单独的进程,不能独立运行,而是shell程序的一部分。
shell 程序在子 shell 中运行,而 shell 函数在当前 shell 中运行。因此在当前 shell 中,函数可对 shell 中变量进行修改。
函数由两部分组成:函数名和函数体。
(2) 函数管理
① 定义函数
语法一:
func_name () {
...函数体...
}
语法二:
function func_name {
...函数体...
}
语法三:
function func_name () {
...函数体...
}
② 查看函数
#查看当前已定义的函数名
declare -F
#查看当前已定义的函数定义
declare -f
#指定查看当前已定义的函数名
declare -F func_name
#指定查看当前已定义的函数名定义
declare -f func_name
③ 删除函数
unset func_name
(3) 函数调用
函数的调用方式:
可在交互式环境下定义函数
可将函数放在脚本文件中作为它的一部分
可放在只包含函数的单独文件中
函数的生命周期:
被调用时创建,返回时终止
调用的概念:
函数只有被调用才会执行,通过给定函数名调用函数,函数名出现的地方,会被自动替换为函数
代码。
① 交互式环境调用函数
[root@c7-1 ~]#dir () {
> ls -l
> }
[root@c7-1 ~]#dir
总用量 4
-rw-r--r-- 1 root root 187 9月 8 17:47 1.sh
② 在脚本中定义及使用函数
函数在使用前必须定义,因此应将函数定义放在脚本开始部分,直至 shell 首次发现它后才能使用,调用函数仅使用其函数名即可。
#!/bin/bash
function fun () {
echo "hello world!"
}
#通过函数名调用函数
fun
————————————————————
#!/bin/bash
function test {
read -p "请输入数字:" num
return $[$num*2]
}
test
echo $?
————————————————————
#!/bin/bash
sum () {
read -p "请输入第一个参数:" NUM1
read -p "请输入第二个参数:" NUM2
echo -e "\033[32m你输入的两个数为:$NUM1 和 $NUM2\033[0m"
SUM=$(($NUM1+$NUM2))
echo -e "\033[31m两个数之和为:$SUM\033[0m"
}
sum
—————————————————————
#!/bin/bash
#函数调用函数
# local 一般用于局部变量声明
fa () {
if [ $1 -eq 1 ];then
echo 1
else
local tp=$[ $1 - 1 ]
local res=$(fa $tp)
echo $[ $1 * $res ]
fi
}
read -p "请输入:" num
res=$(fa $num)
echo $res
————————————————————
#!/bin/bash
#修改本地 repo 源
#请事先挂载光盘镜像,VMware设置里选择已连接
function repobackup {
cd /etc/yum.repos.d
mkdir repo.bak
mv *.repo repo.bak
mount /dev/sr0 /mnt > /dev/null
}
makelocalrepo () {
echo -e '
[local]
name=local
baseurl=file:///mnt
enabled=1
gpgcheck=0' > local.repo
}
uselocalrepo () {
yum clean all > /dev/null
yum makecache > /dev/null
yum -y install httpd > /dev/null
systemctl start httpd
}
#-----main-----#
repobackup
makelocalrepo
uselocalrepo
③ 使用函数文件
- 可以将经常使用的函数存入一个单独的函数文件,然后将函数文件载入 shell,再进行调用函数
- 文件名可任意选取,但最好与相关任务有某种联系,例如:functions
- 一旦函数文件载入 shell,就可以在命令行或脚本中调用函数。可以使用 delcare -f 或 set 命令查看所有定义的函数,其输出列表包括已经载入 shell 的所有函数
- 若要改动函数,首先用 unset 命令从 shell 中删除函数,改动完毕后,再重新载入此文件
实现函数文件的过程:
创建函数文件,只存放函数的定义
在 shell 脚本或交互式 shell 中调用函数文件,格式如下:
. filename 或 source filename
示例:
[root@centos8 ~]#cat functions
#!/bin/bash
#functions
hello () {
echo Run hello Function
}
hello2 () {
echo Run hello2 Function
}
[root@centos8 ~]#. functions
[root@centos8 ~]#hello
Run hello Function
[root@centos8 ~]#hello2
Run hello2 Function
[root@centos8 ~]#declare -f hello hello2
hello ()
{
echo Run hello Function
}
hello2 ()
{
echo Run hello2 Function
}
(4) 函数返回值
函数的执行结果返回值:
使用 echo 等命令进行输出
函数体中调用命令的输出结果
函数的退出状态码:
默认取决于函数中执行的最后一条命令的退出状态码
自定义退出状态码,其格式为:
return 从函数中返回,用最后状态命令决定返回值
return 0 无错误返回
return 1-255 有错误返回
(5) 环境函数
类拟于环境变量,也可以定义环境函数,使子进程也可使用父进程定义的函数。
定义环境函数:
export -f function_name
declare -xf function_name
查看环境函数:
export -f
declare -xf
(6) 函数参数
函数可以接受参数:
传递参数给函数,在函数名后面以空白分隔给定参数列表即可,如:func arg1 arg2 ...
在函数体中当中,可使用$1, $2, ...调用这些参数。还可以使用$@, $*, $#等特殊变量
(7) 函数变量
变量作用域:
普通变量:只在当前 shell 进程有效,为执行脚本会启动专用子 shell 进程。因此,本地变量的作用范围是当前 shell 脚本程序文件,包括脚本中的函数
环境变量:当前 shell 和子 shell 有效
本地变量:函数的生命周期结束时变量被自动销毁
注意:
如果函数中定义了普通变量,且名称和局部变量相同,则使用本地变量
由于普通变量和局部变量会冲突,建议在函数中只使用本地变量
在函数中定义本地变量的方法:
local NAME=VALUE
(8) 函数递归
函数递归:函数直接或间接调用自身,注意递归层数,可能会陷入死循环。
递归示例:
阶乘是基斯顿·卡曼于 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)!
示例:
#!/bin/bash
#
fact() {
if [ $1 -eq 0 -o $1 -eq 1 ]; then
echo 1
else
echo $[$1*$(fact $[$1-1])]
fi
}
fact $1
fork 炸弹是一种恶意程序,它的内部是一个不断在 fork 进程的无限循环,实质是一个简单的递归程序。由于程序是递归的,如果没有任何限制,这会导致这个简单的程序迅速耗尽系统里面的所有资源。
参考:
fork 炸弹
函数实现:
:(){ :|:& };:
bomb() { bomb | bomb & }; bomb
脚本实现:
cat bomb.sh
#!/bin/bash
./$0|./$0&
2.12 一些脚本相关工具
(1) 信号捕捉 trap
trap '触发指令' 信号
进程收到系统发出的指定信号后,将执行自定义指令,而不会执行原操作
trap '' 信号
忽略信号的操作
trap '-' 信号
恢复原信号的操作
trap -p
列出自定义信号操作
trap finish EXIT
当脚本退出时,执行 finish 函数
示例:
#!/bin/bash
trap 'echo "Press ctrl+c"' int quit
trap -p
for((i=0;i<=10;i++))
do
sleep 1
echo $i
done
trap '' int
trap -p
for((i=11;i<=20;i++))
do
sleep 1
echo $i
done
trap '-' int
trap -p
for((i=21;i<=30;i++))
do
sleep 1
echo $i
done
(2) 创建临时文件 mktemp
mktemp 命令用于创建并显示临时文件,可避免冲突。
格式:
mktemp [OPTION]... [TEMPLATE]
常见选项:
-d 创建临时目录
-p DIR或--tmpdir=DIR 指明临时文件所存放目录位置
示例:
mktemp /tmp/testXXX
tmpdir=`mktemp –d /tmp/testdirXXX`
mktemp --tmpdir=/testdir testXXXXXX
(3) 安装复制文件 install
install 命令格式:
install [OPTION]... [-T] SOURCE DEST 单文件
install [OPTION]... SOURCE... DIRECTORY
install [OPTION]... -t DIRECTORY SOURCE...
install [OPTION]... -d DIRECTORY...创建空目录
选项:
-m MODE,默认755
-o OWNER
-g GROUP
示例:
install -m 700 -o wang -g admins srcfile desfile
install -m 770 -d /testdir/installdir
(4) 交互式转化批处理工具 expect
expect 主要应用于自动化交互式操作的场景,借助 expect 处理交互的命令,可以将交互过程如:ssh 登录,ftp 登录等写在一个脚本上,使之自动完成。尤其适用于需要对多台服务器执行相同操作的环境中,可以大大提高系统管理人员的工作效率。
expect 语法:
expect [选项] [ -c cmds ] [ [ -[f|b] ] cmdfile ] [ args ]
常见选项:
-c:从命令行执行expect脚本,默认expect是交互执行的
-d:可以输出调试信息
expect 中相关命令:
spawn 启动新的进程
expect 从进程接收字符串
send 用于向进程发送字符串
interact 允许用户交互
exp_continue 匹配多个字符串在执行动作后加此命令
详细使用参考:交互式转化批处理工具 expect
2.13 数组
(1) 数组介绍
变量:存储单个元素的内存空间。
数组:存储多个元素的连续的内存空间,相当于多个变量的集合。
数组名和索引:
索引的编号从0开始,属于数值索引
索引可支持使用自定义的格式,而不仅是数值格式,即为关联索引,bash4.0 版本之后开始支持
bash 的数组支持稀疏格式(索引不连续)
(2) 声明数组
#普通数组可以不事先声明,直接使用
declare -a ARRAY_NAME
#关联数组必须先声明,再使用
declare -A ARRAY_NAME
#两者不可相互转换
(3) 数组赋值
① 一次只赋值一个元素
ARRAY_NAME[INDEX]=VALUE
示例:
weekdays[0]="Sunday"
weekdays[4]="Thursday"
② 一次赋值全部元素
ARRAY_NAME=("VAL1" "VAL2" "VAL3" ...)
示例:
title=("ceo" "coo" "cto")
num=({0..10})
alpha=({a..g})
file=( *.sh )
③ 只赋值特定元素
ARRAY_NAME=([0]="VAL1" [3]="VAL2" ...)
④ 交互式数组值对赋值
read -a ARRAY
(4) 显示所有数组
declare -a
示例:
[root@aliyunhost01~]#declare -a
declare -a BASH_ARGC='()'
declare -a BASH_ARGV='()'
declare -a BASH_LINENO='()'
declare -a BASH_SOURCE='()'
declare -ar BASH_VERSINFO='([0]="4" [1]="2" [2]="46" [3]="2" [4]="release" [5]="x86_64-redhat-linux-gnu")'
declare -a DIRSTACK='()'
declare -a FUNCNAME='()'
declare -a GROUPS='()'
declare -a PIPESTATUS='([0]="0")'
(5) 引用数组
① 引用数组元素
${ARRAY_NAME[INDEX]}
#如果省略[INDEX]表示引用下标为0的元素
示例:
[root@centos8 ~]#declare -a title=([0]="ceo" [1]="coo" [2]="cto")
[root@centos8 ~]#echo ${title[1]}
coo
[root@centos8 ~]#echo ${title}
ceo
[root@centos8 ~]#echo ${title[2]}
cto
[root@centos8 ~]#echo ${title[3]}
② 引用数组所有元素
${ARRAY_NAME[*]}
${ARRAY_NAME[@]}
示例:
[root@centos8 ~]#echo ${title[@]}
ceo coo cto
[root@centos8 ~]#echo ${title[*]}
ceo coo cto
③ 数组的长度,即数组中元素的个数
${#ARRAY_NAME[*]}
${#ARRAY_NAME[@]}
示例:
[root@centos8 ~]#echo ${#title[*]}
3
(6) 删除数组
① 删除数组中的某个元素
unset ARRAY[INDEX]
示例:
[root@centos8 ~]#echo ${title[*]}
ceo coo cto
[root@centos8 ~]#unset title[1]
[root@centos8 ~]#echo ${title[*]}
ceo cto
② 删除整个数组
unset ARRAY
示例:
[root@centos8 ~]#unset title
[root@centos8 ~]#echo ${title[*]}
[root@centos8 ~]#
(7) 数组数据处理
① 数组切片
${ARRAY[@]:offset:number}
offset #要跳过的元素个数
number #要取出的元素个数
#取偏移量之后的所有元素
{ARRAY[@]:offset}
示例:
[root@centos8 ~]#num=({0..10})
[root@centos8 ~]#echo ${num[*]:2:3}
2 3 4
[root@centos8 ~]#echo ${num[*]:6}
6 7 8 9 10
② 向数组中追加元素
ARRAY[${#ARRAY[*]}]=value
ARRAY[${#ARRAY[@]}]=value
示例:
[root@centos8 ~]#num[${#num[@]}]=11
[root@centos8 ~]#echo ${#num[@]}
12
[root@centos8 ~]#echo ${num[@]}
0 1 2 3 4 5 6 7 8 9 10 11
(8) 关联数组
declare -A ARRAY_NAME
ARRAY_NAME=([idx_name1]='val1' [idx_name2]='val2‘...)
注意:关联数组必须先声明再调用
示例:
[root@centos8 ~]#name[ceo]=mage
[root@centos8 ~]#name[cto]=wang
[root@centos8 ~]#name[coo]=zhang
[root@centos8 ~]#echo ${name[ceo]}
zhang
[root@centos8 ~]#echo ${name[cto]}
zhang
[root@centos8 ~]#echo ${name[coo]}
zhang
[root@centos8 ~]#echo ${name}
zhang
[root@centos8 ~]#declare -A name
-bash: declare: name: cannot convert indexed to associative array
[root@centos8 ~]#unset name
[root@centos8 ~]#declare -A name
[root@centos8 ~]#name[ceo]=mage
[root@centos8 ~]#name[cto]=wang
[root@centos8 ~]#name[coo]=zhang
[root@centos8 ~]#echo ${name[coo]}
zhang
[root@centos8 ~]#echo ${name[ceo]}
mage
[root@centos8 ~]#echo ${name[cto]}
wang
[root@centos8 ~]#echo ${name[*]}
mage wang zhang
关联数组参考:
Shell 关联数组
Shell 中的关联数组,获取数组索引
(9) 数组脚本示例
范例:生成 10 个随机数保存于数组中,并找出其最大值和最小值
#!/bin/bash
declare -i min max
declare -a nums
for ((i=0;i<10;i++));do
nums[$i]=$RANDOM
[ $i -eq 0 ] && min=${nums[0]} && max=${nums[0]}&& continue
[ ${nums[$i]} -gt $max ] && max=${nums[$i]}
[ ${nums[$i]} -lt $min ] && min=${nums[$i]}
done
echo "All numbers are ${nums[*]}"
echo Max is $max
echo Min is $min
范例:定义一个数组,数组中的元素对应的值是 /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"
范例:shell 脚本实现冒泡排序
#!/bin/bash
#a test about sort
echo "please input a number list"
read -a arrs
for((i=0;i<${#arrs[@]};i++)){
for((j=0;j<${#arrs[@]}-1;j++)){
if [[ ${arrs[j]} -gt ${arrs[j+1]} ]];then
tmp=${arrs[j]}
arrs[j]=${arrs[j+1]}
arrs[j+1]=$tmp
fi
}
}
echo "after sort"
echo ${arrs[@]}