自动化运维(一):Shell编程基础

知见

实验环境准备

虚拟机固定IP设置

使用 VMware 创建4个虚拟机,每个虚拟机添加3块30G的虚拟盘。首先在虚机中给每个主机加上固定IP,控制节点修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@localhost ~]# cat /etc/sysconfig/network-scripts/ifcfg-ens160
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=static # 静态分配
DEFROUTE=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=eui64
NAME=ens160
UUID=cdea53bc-a032-4987-b69a-b01c55f67950
DEVICE=ens160
ONBOOT=yes # 改为yes
IPADDR=192.168.26.140 # 以下为新增
PREFIX=24
GATEWAY=192.168.26.2
DNS1=114.114.114.114
[root@localhost ~]# nmcli connection down ens160
[root@localhost ~]# nmcli connection up ens160 # 重启网络(也可以用nmtui)

集群节点基础环境配置

依次修改其他三台虚拟机,分配内网IP为141~143,并给每台主机进行命名,方便通过主机名访问节点:

1
2
3
4
5
6
7
8
[root@localhost ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.26.140 center
192.168.26.141 sp-1
192.168.26.142 sp-2
192.168.26.143 sp-3

为了之后的多节点操作,需要将这个/etc/hosts文件下发到受控节点sp-1sp-3上。先通过 Ansible 完成这样的多节点操作,后续再深入了解。

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# yum install epel-release
[root@localhost ~]# yum makecache # 刷新缓存
[root@localhost ~]# yum install ansible
[root@localhost ~]# ansible --version
ansible [core 2.14.2]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.11/site-packages/ansible
ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.11.2 (main, Feb 18 2023, 08:12:16) [GCC 8.5.0 20210514 (Red Hat 8.5.0-18)] (/usr/bin/python3.11)
jinja version = 3.1.2
libyaml = True

新建一个目录autoops,在其中准备配置文件和 hosts 文件。

/etc/ansible/ansible.cfg中有如下说明:

Since Ansible 2.12 (core):
To generate an example config file (a “disabled” one with all default settings, commented out):
$ ansible-config init –disabled > ansible.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@localhost autoops]# ansible-config init --disabled > ansible.cfg

[root@localhost autoops]# cat hosts
[master] # 指定组,下面是对应组内的主机
center

[nodes] # 指定组,下面是对应组内的主机
sp-[1:3]

[master:vars] # 指定组变量,下面是组变量,组内节点共享变量
ansible_connection=local

[nodes:vars] # 指定组变量
ansible_ssh_pass=YourPassword # 指定远程ssh连接密码,使用时去掉注释

[root@localhost autoops]# ansible --version
ansible [core 2.14.2]
config file = /root/autoops/ansible.cfg # 确认配置文件
configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.11/site-packages/ansible
ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.11.2 (main, Feb 18 2023, 08:12:16) [GCC 8.5.0 20210514 (Red Hat 8.5.0-18)] (/usr/bin/python3.11)
jinja version = 3.1.2
libyaml = True

第一次尝试连接所有节点,受控节点报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@localhost autoops]# ansible all -i hosts -m ping
sp-1 | FAILED! => {
"msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host's fingerprint to your known_hosts file to manage this host."
}
sp-2 | FAILED! => {
"msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host's fingerprint to your known_hosts file to manage this host."
}
sp-3 | FAILED! => {
"msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this. Please add this host's fingerprint to your known_hosts file to manage this host."
}
center | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}

由于 sshd 服务在初次连接时会要求用户接受一次对方主机的指纹信息,需要输入 yes 确认。例如,正常的第一次SSH远程连接过程是这样的:

1
2
3
4
5
[root@localhost autoops]# ssh sp-1
The authenticity of host 'sp-1 (192.168.26.141)' can't be established.
ECDSA key fingerprint is SHA256:8IrsRv/9KyqKu8mK0tdN8p5QXIB3hrs6+SmwwKdjsrI.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'sp-1,192.168.26.141' (ECDSA) to the list of known hosts.

在配置文件中将修改以下参数,设置成默认不需要SSH协议的指纹验证,无需重启服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@localhost autoops]# vi ansible.cfg
# (boolean) Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host
host_key_checking=False
[root@localhost autoops]# ansible all -i hosts -m ping
center | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}
sp-1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}
# 其余略

接下来是下发文件以及对所有节点进行改名操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
[root@localhost autoops]# ansible nodes -i hosts -m copy -a "src=/etc/hosts dest=/etc/hosts"
sp-2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"checksum": "d55b6c38d88db076cac42dcfe03fdd979ca9ed34",
"dest": "/etc/hosts",
"gid": 0,
"group": "root",
"md5sum": "c85a4c67b0df03cfae33928f975e7eed",
"mode": "0644",
"owner": "root",
"secontext": "unconfined_u:object_r:net_conf_t:s0",
"size": 245,
"src": "/root/.ansible/tmp/ansible-tmp-1685295184.8170862-2271-210696309543931/source",
"state": "file",
"uid": 0
}
# 其余略
[root@localhost autoops]# ansible all -i hosts -m hostname -a "name={{ inventory_hostname }}"
center | CHANGED => {
"ansible_facts": {
"ansible_domain": "",
"ansible_fqdn": "center",
"ansible_hostname": "center",
"ansible_nodename": "center",
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"name": "center"
}
sp-2 | CHANGED => {
"ansible_facts": {
"ansible_domain": "",
"ansible_fqdn": "sp-2",
"ansible_hostname": "sp-2",
"ansible_nodename": "sp-2",
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"name": "sp-2"
}
# 其余略

断开 Shell 重连,可以发现节点的重命名已经生效。通过 Ansible 可以批量查看所有的节点的主机名:

1
2
3
4
5
6
7
8
9
[root@center autoops]# ansible all -i hosts -m shell -a "hostname"
center | CHANGED | rc=0 >>
center
sp-1 | CHANGED | rc=0 >>
sp-1
sp-2 | CHANGED | rc=0 >>
sp-2
sp-3 | CHANGED | rc=0 >>
sp-3

VSCode 配置

在 VSCode 中安装 Python 和 SFTP (@Natizyskunk) 插件。按 ctrl+shift+p 弹出的框中选择SFTP配置。

在当前打开的目录下会生成一个.vscode目录,同时在该目录中会生成一个sftp.json配置文件,默认内容如下:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "My Server",
"host": "localhost",
"protocol": "sftp",
"port": 22,
"username": "username",
"remotePath": "/",
"uploadOnSave": false,
"useTempFile": false,
"openSsh": false
}

要将本地代码映射到 center 节点,修改配置内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "本地虚拟机",
"host": "192.168.26.140",
"protocol": "sftp",
"port": 22,
"username": "root",
"password": "YourPassword",
"remotePath": "/root/autoops/imooc-ops",
"passive": false,
"interactiveAuth": false,
"uploadOnSave": true,
"syncMode": "update",
"ignore": [
"**/.vscode/**",
"**/.git/**"
]
}

右键SFTP插件的相关选项,可以实现目录同步以及上传或者下载目录。

选择Upload Folder会将整个目录下的文件上传到远端服务器上的对应目录。执行该操作后的结果如下:

1
2
[root@center autoops]# ls    # 会自动创建目录
ansible.cfg hosts imooc-ops

Python 虚拟环境搭建

安装依赖环境

1
yum install make gcc patch zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel xz-devel libuuid-devel gdbm-devel

Pyenv 的安装配置:

1
2
3
4
5
6
7
8
# 安装
curl https://pyenv.run | bash
# 设置环境变量
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
# 重启shell
exec "$SHELL"

构建虚拟环境:

1
2
3
4
5
6
7
8
9
10
11
12
# 列出可用版本
pyenv install -l
# 安装3.8.16版本
pyenv install 3.8.16
# 创建一个3.8.16版本的虚拟环境, 命名为imooc-ops
pyenv virtualenv 3.8.16 imooc-ops
# 列出虚拟环境
pyenv virtualenvs
# 激活imooc-ops环境
pyenv activate imooc-ops
# 关闭环境
pyenv deactivate

echo 命令

空格压缩

1
2
3
4
5
6
echo "hello,      world"
echo hello, world, 你好 # 长段空格会压缩成1个空格
# 结果是 hello, world, 你好
var1="a b c d"
echo $var1 # 只保留1个空格
echo "$var1" # 保持长段空格

常用选项

1
2
3
4
5
6
7
8
9
10
11
12
# -n
echo "hello" # 默认输出的最后会有一个换行符
echo -n "hello" # 不要加默认的换行符

echo # 输出一个换行符
echo -n "please input your name: "
read name # 输入读到name变量
echo "hello, $name"

# -e
echo "hello\nwor\tld" # 转义并没有被解释
echo -e "hello\nwor\tld" # 允许转义

输出带颜色的字符

使用ANSI转义序列。

1
2
3
4
5
6
7
8
9
10
11
echo -e "\033[31mhello\033[0m"    # 输出红色hello
# 各种文本颜色
echo -e "\033[30m 黑色字 \033[0m"
echo -e "\033[31m 红色字 \033[0m"
echo -e "\033[32m 绿色字 \033[0m"
echo -e "\033[33m 黄色字 \033[0m"
echo -e "\033[34m 蓝色字 \033[0m"
echo -e "\033[35m 紫色字 \033[0m"
echo -e "\033[36m 青色字 \033[0m"
echo -e "\033[37m 白色字 \033[0m"
# 40m 到 47m 代表不同的背景颜色,使用方式同上

变量

${}

${}用于对变量进行操作。例如,${var}可以用于替换变量var的值。

1
2
3
4
5
6
7
8
var1=hello
echo $var1
var1_xyz=abc
# 期望:输出$var1_xyz=hello_xyz
echo "输出\$var1_xyz=$var1_xyz"
# 实际:输出$var1_xyz=abc

echo "继续输出: ${var1}_xyz" # 成功

declare 与 set

declare命令用于声明和设置变量的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
var2=123    # 直接给变量赋值
# declare 显示声明变量
# -i 指定整型 -r 只读变量 -a 指定数组 -f 指定函数名
declare -i var3
var3=xyz # 赋值语句失败
echo "\$var3=$var3" # var3解释为0

var3=245 # 赋值成功
echo "\$var3=$var3"

declare -r var4=abc # 只读变量,需声明时赋值
# var4=xyz 失败,shell报错
echo $var4
1
2
3
4
5
6
7
[root@center chap01]# declare    # 显示当前系统中已定义的全部变量信息
[root@center chap01]# var1=hello
[root@center chap01]# declare | grep ^var1=
var1=hello
[root@center chap01]# var2=123
[root@center chap01]# declare | grep ^var2=
var2=123

set命令用于设置或修改 shell 的运行时选项。

1
2
3
[root@center chap01]# set    # 与declare类似,也可以列出Shell中定义的变量
[root@center chap01]# unset var1 # 删除已定义的变量
[root@center chap01]# declare | grep ^var1= # 没有输出

`` 与 $()

反引号``与$()的作用类似,都可以返回命令输出的结果。

反引号容易与其他引号产生歧义,而$()没有这个问题。在嵌套命令时,反引号需要反斜杠转义,而$()不需要,因此更推荐使用$()

1
2
3
4
LOCAL_IP_1=`cat /etc/hosts | grep \`hostname\` | awk '{print $1}'`
LOCAL_IP_2=$(cat /etc/hosts | grep $(hostname) | awk '{print $1}')
echo "\$LOCAL_IP_1=$LOCAL_IP_1, \$LOCAL_IP_2=$LOCAL_IP_2"
# 二者输出一致

数组

数组的定义与长度

使用${array[@]}${array[*]}表示整个数组。${array[i]}表示第i个元素。数组变量前加#表示数组或其中某个元素的长度。

*通常表示将整个数组作为一个整体。当使用*作为索引时,它会展开为数组的所有元素,每个元素之间用空格分隔。这意味着将数组视为单个参数,而不是逐个处理数组的元素。

@表示数组的所有元素,每个元素都是独立的。使用@作为索引时,它会将数组的每个元素视为一个独立的实体,可以逐个处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arr1=(xx    xyz    "hello,      world"      1234   20.0    "你好")
echo "数组arr1的长度: ${#arr1[@]}" # 获取数组长度
echo "数组arr1的长度: ${#arr1[*]}"

echo "${#arr1}" # 不写索引表示首元素,长度为2

len=${#arr1[@]}
last=`expr $len - 1` # 末元素的索引
echo "arr1的最后一个元素: ${arr1[$last]}"

# 另一种定义方式
arr2[0]=abc
arr2[1]=xyz
arr2[2]="世界,你好"
arr2[3]=1234
echo "arr2数组内容: ${arr2[@]}"
echo "数组arr2的长度: ${#arr2[@]}"

数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 第一种方式
len_2=${#arr2[@]}
# last_2=`expr $len_2 - 1` 或
last_2=$((len_2-1))
for i in `seq 0 $last_2`
do
# pos=`expr $i + 1` 或
pos=$((i+1)) # 序号,令其从1开始
echo "arr2中第${pos}个元素为: ${arr2[$i]}"
done

# 第二种方式
count=1
for ele in "${arr1[@]}" # 必须加引号,否则 hello world 拆成两个元素
do
echo "arr1中第${count}个元素为: ${ele}"
# count=`expr $count + 1` 或 count=$((count+1)) 或
((count++)) # 自增运算
done

函数

函数的定义与调用

Shell 中函数不设返回类型、函数参数。

1
2
3
4
5
6
# 定义
xyz() {
echo "hello, function xyz!"
}
# 调用
xyz

函数传参

使用位置参数向定义的函数传参。

位置参数是指在执行脚本时传递给脚本的参数,按照顺序分配到特殊变量 $1$2$3 等中。其中,$1 表示第一个参数,$2 表示第二个参数,以此类推。

例如,如果你执行以下命令:

1
./myscript.sh arg1 arg2 arg3

那么在 myscript.sh 脚本中,$1 的值将是 arg1$2 的值将是 arg2$3 的值将是 arg3

需要注意的是,$0 表示脚本的名称,$# 表示传递给脚本的参数个数,$* 表示所有参数的值,$@ 表示所有参数的值(但是每个参数都被双引号包围)。

1
2
3
4
5
6
7
8
9
xyz_with_params() {
echo "函数传入参数个数为:$#"
for i in `seq 1 $#`
do
echo "第${i}个位置参数: $1"
shift # 向左移动参数
done
}
xyz_with_params abc xyz 123

shift能够将命令接收到的参数逐个向左移动一位,即原本的$3变量会覆盖$2变量,原本的$2变量会覆盖$1变量。这样我们只需要每执行一次shift命令后调用$1变量,就能够实现对全部参数的处理工作了。

函数的返回值

函数可以返回一个状态码$?。在大多数 Unix/Linux 系统中,命令的状态码范围是 0 到 255。其中,状态码 0 表示命令执行成功,非 0 表示命令执行失败。如果状态码超出了这个范围,它将对 256 取余数,截断为 0 到 255 之间的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xyz_with_return() {
echo "设置返回值 1000"
return 1000
}

xyz_with_return
echo "函数返回值: $?" # 返回232

if xyz_with_return # 注意该判断与其他语言中的区别
then
echo "返回值零,成功"
else
echo "返回值非零,失败"
fi

计算

expr 命令

expr是进行数学运算和字符串操作的工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a=$(expr 3 + 2)
echo "a = $a" # 5

b=$(expr $a + 10)
echo "b = $a + 10 = $b" # 5 + 10 = 15

c=$(expr $a \* 10)
echo "c = $a * 10 = $c" # 5 * 10 = 50

d=$(expr $a / 3)
echo "d = $a / 3 = $d" # 5 / 3 = 1

e=$(expr $a % 3)
echo "e = $a % 3 = $e" # 5 % 3 = 2

需要注意,在进行乘法运算时需要使用反斜杠转义符号,否则会被 Shell 解释为通配符。

let 命令

let 命令是一个用于计算数学表达式的 Bash 内置命令。变量计算中,因变量不需要加上$来表示。如果表达式中包含了空格或其他特殊字符,则必须引起来。

1
2
3
4
5
6
7
8
9
10
11
let "a += 10"
echo "a + 10 = $a" # a + 10 = 15

let "a /= 10"
echo "a / 10 = $a" # a / 10 = 1

let "a++"
echo "a++ = $a" # a++ = 2

let "b = $a + $b + $c"
echo "b = a + b + c = $b" # a + b + c = 67

$(())

$(()) 是 Shell 中的一种算术扩展方式,可以用于执行数学运算。它的基本语法如下:

1
$((expression))

其中,expression 是一个数学表达式,可以包含数字、变量、算术运算符和括号等元素,Shell 会对这个表达式进行求值,并将结果输出到标准输出。

可以使用的算术运算符包括:

  • +:加法
  • -:减法
  • *:乘法
  • /:除法
  • %:取模
  • **:幂运算
1
2
3
4
5
6
7
8
9
a=2
b=3
c=4
d=$((a + b * c))
echo $d # 输出 14
((a++))
echo $a # 输出 3
((b %= 2))
echo $b # 输出 1

exprlet$(()) 只能用于整数运算,如果进行浮点数运算,可能会得到不准确的结果。如果需要进行浮点数运算,可以考虑使用 bc 命令。

bc 命令

输入bc进入交互模式:

1
2
3
[root@center chap01]# bc
1.2345 * 3
3.7035

使用scale设定精度:

1
2
3
4
[root@center chap01]# bc
scale = 3
3 / 8
.375

利用管道符直接取得结果:

1
echo "scale=3; 1.1 * 1.23" | bc    # 1.353

字符串处理

取字符串长度

1
2
3
4
5
6
str1="hello world"
echo "原始字符串: $str1"
echo "字符串长度: ${#str1}" # 11

str2="我是中文"
echo "中文字符串长度: ${#str2}" # 长度为4

还有许多方式可以统计字符串长度,例如expr length "hello world"。Shell 实践中,1个中文字符占1个长度。

取子串

1
2
echo "从第2个字符开始取字符串: ${str1:2}"   # llo world
echo "从第2个字符开始连续取3个字符: ${str1:2:3}" # llo

此处索引从0开始,即首个字符是第0个字符。

但是通过expr index $string $substring返回的索引又是从1开始的。

删除子串

#:从开头删除最短匹配的子串

1
echo "从开头删除最短匹配*o的子串: ${str1#*o}"   #  world

##:从开头删除最长匹配的子串

1
echo "从开头删除最长匹配*o的子串: ${str1##*o}"  # rld

%:从末尾删除最短匹配的子串

1
echo "从末尾删除最短匹配o*的子串: ${str1%o*}"   # hello w

%%:从末尾删除最长匹配的子串

1
echo "从末尾删除最长匹配o*的子串: ${str1%%o*}"  # hell

以上四个符号只能删除字符串开头或结尾与特定模式匹配的子串。如果想要删除字符串中间的子串,可以使用其他命令,如sedawk

替换子串

1
2
3
4
${string/substring/replacement}
${string//substring/replacement}
${string/#substring/replacement}
${string/%substring/replacement}

其中string是要操作的字符串,substring是要被替换的子串,replacement是用来替换的文本。

string

  • /符号表示替换string中第一个出现的substring
  • //符号表示替换string中所有的substring
  • /#符号表示替换string开头的substring
  • /%符号表示替换string末尾的substring
1
2
3
4
5
str3="Hi World! Hello World! Hello Seres!"
echo ${str3/Hello/Goodbye} # Hi World! Goodbye World! Hello Seres!
echo ${str3//Hello/Goodbye} # Hi World! Goodbye World! Goodbye Seres!
echo ${str3/#Hi/Goodbye} # Goodbye World! Hello World! Hello Seres!
echo ${str3/%Seres!/China!} # Hi World! Hello World! Hello China!

默认值

  • ${param:-default}:如果变量param定义且不为空,则返回param的值;否则返回default

  • ${param:+default}:如果变量param定义且不为空,则返回default;否则返回空字符串。

  • ${param:?default}:如果变量param定义且不为空,则返回param的值;否则显示错误信息param: default并终止脚本执行。

1
2
3
4
5
6
7
8
9
10
11
str4="123"
echo ${str4:-"abc"} # 123
echo ${str5:-"abc"} # abc

str6="123"
echo ${str6:+abc} # abc
echo ${str7:+abc} # 空串

str8="123"
echo ${str8:?abc} # 123
echo ${str9:?abc} # 返回 str9: abc 并终止

判断语句

1
2
3
4
5
6
if [ condition ]
then
# code to be executed if the condition is true
else
# code to be executed if the condition is false
fi

如果condition成立,则会执行then后的代码块,否则执行else后的代码块。else语句是可选的,可以省略。

文件测试

操作符 作用
-d 文件是否为目录类型
-e 文件是否存在
-f 是否为一般文件
-r 当前用户是否有权限读取
-w 当前用户是否有权限写入
-x 当前用户是否有权限执行
1
2
3
$ [ -f /etc/fstab ]
$ echo $?
0

逻辑运算

逻辑 格式
[ condition1 -a condition2][ condition1 ] && [ condition 2 ]
[ condition1 -o condition2][ condition1 ] || [ condition 2 ]
[ ! condition ]
1
2
3
4
5
6
7
$ [ -d "/root" ] && [ -d "/tmp" ]
$ echo $?
0

$ [ -d "/root" -a -d "/tmp" ]
$ echo $?
0

整数比较

操作符 作用
-eq 等于
-ne 不等于
-gt 大于
-lt 小于
-ge 大于等于
-le 小于等于
1
2
3
$ [ 10 -eq 10 ]
$ echo $?
0

字符串比较

操作符 作用
=== 等于
!= 不等于
-z 空串
1
2
3
$ [ -z $String ]
$ echo $?
0

循环语句

for 循环

基本语法:

1
2
3
4
for variable_name in item1 item2 ... itemN
do
# command
done

遍历数组

1
array=(xx  abc  "hello,      world"   12)
  1. 遍历${array[@]}

    1
    2
    3
    4
    for i in ${array[@]}
    do
    echo "$i"
    done

    输出:

    1
    2
    3
    4
    5
    xx
    abc
    hello,
    world
    12
  2. 遍历"${array[@]}"

    1
    2
    3
    4
    for i in "${array[@]}"
    do
    echo "$i"
    done

    输出:

    1
    2
    3
    4
    xx
    abc
    hello, world
    12
  3. 遍历${array[*]}

    1
    2
    3
    4
    for i in ${array[*]}
    do
    echo "$i"
    done

    输出:

    1
    2
    3
    4
    5
    xx
    abc
    hello,
    world
    12
  4. 遍历"${array[*]}"

    1
    2
    3
    4
    for i in "${array[*]}"
    do
    echo "$i"
    done

    输出:

    1
    xx abc hello,      world 12

输出for_test.txt

1
2
3
hello apple
hello banana
hello cherry
  1. 按分隔符输出

    IFS是一个环境变量,它代表 Internal Field Separator(内部字段分隔符),用于指定用于分隔字符串中字段的字符或字符串。默认情况下,IFS的值设置为包含空格、制表符和换行符的字符串: \t\n

    1
    2
    [root@center chap01]# declare | grep ^IFS
    IFS=$' \t\n'
    1
    2
    3
    4
    for line in $(cat for_test.txt)
    do
    echo "$line"
    done

    输出:

    1
    2
    3
    4
    5
    6
    hello
    apple
    hello
    banana
    hello
    cherry
  2. 按行输出

    更改IFS之前将其当前值保存到另一个变量中,以便稍后恢复。

    1
    2
    3
    4
    5
    6
    7
    originalIFS=$IFS    # 保存原始的IFS值
    IFS=$'\n' # 设置为换行符
    for line in $(cat for_test.txt)
    do
    echo "$line"
    done
    IFS=$originalIFS # 还原IFS为原始值

    输出:

    1
    2
    3
    hello apple
    hello banana
    hello cherry

也可以使用双括号实现类似C语言风格的for循环:

1
2
3
4
for ((i = 0; i < 5; i++))
do
echo "Iteration: $i"
done

while 循环

基本语法:

1
2
3
4
while [ condition ]
do
# command
done
  1. 按分隔符输出

    1
    2
    3
    4
    while read greeting fruit   # 需要与源文件列数对应
    do
    echo "greeting: $greeting, fruit: $fruit"
    done < loop_test.txt

    输出:

    1
    2
    3
    greeting: hello, fruit: apple
    greeting: hello, fruit: banana
    greeting: hello, fruit: cherry
  2. 按行输出

    1
    2
    3
    4
    while read line
    do
    echo "$line" # 逐行输出文件内容
    done < loop_test.txt

    输出:

    1
    2
    3
    hello apple
    hello banana
    hello cherry

    文件末尾需要有换行符,否则会少读一行。

until 循环

基本语法:

1
2
3
4
until [ condition ]
do
# command
done

while循环类似,不同的是当condition为假时,执行循环体内的指令。

文件描述符

文件描述符(File Descriptor)是用于标识和操作打开的文件的整数值,允许Shell进程与文件进行输入、输出和错误处理。

有三个默认的文件描述符:

  1. 标准输入(stdin):文件描述符为0,用于接收输入数据。通常连接到键盘,允许用户从键盘输入数据。

  2. 标准输出(stdout):文件描述符为1,用于发送输出数据。通常连接到终端,可以将结果打印到屏幕上。

  3. 标准错误(stderr):文件描述符为2,用于发送错误消息。通常连接到终端,可以将错误信息打印到屏幕上。

重定向操作

使用重定向操作符来改变文件描述符的默认行为,可以将文件重定向到命令,或者将输出发送到文件中(而不是终端)。例如,使用>符号(等同于1>)可以将标准输出重定向到文件中,使用<符号可以将文件内容作为标准输入,使用2>符号将标准错误重定向到文件。

符号 作用
> file 将标准输出重定向到一个文件中(清空原有文件的数据)
2> file 将错误输出重定向到一个文件中(清空原有文件的数据)
>> file 将标准输出重定向到一个文件中(追加到原有内容的后面)
2>> file 将错误输出重定向到一个文件中(追加到原有内容的后面)
>> file 2>&1&>> file 将标准输出与错误输出共同写入到文件中(追加到原有内容的后面)
>& fd 将标准输出重定向到文件描述符中(追加到原有内容的后面)

自定义文件描述符

除了默认的文件描述符0、1和2之外,可以自定义文件描述符。使用exec命令将一个文件描述符重定向到其他文件。

例如,你可以将文件描述符3和4重定向到一个文件,然后通过该文件描述符进行读取或写入操作。

1
2
3
4
5
6
7
8
9
10
11
12
exec 3> output.txt  # 将文件描述符3重定向到output.txt文件
echo "Hello, World!" >&3 # 将文本写入文件描述符3所关联的文件

echo -e "hello1\nhello2\nhello3" > input.txt
exec 4< input.txt # 将文件描述符4重定向到input.txt文件
while read line
do
echo "Line: $line"
done <&4 # 从文件描述符4读取行

exec 3>&- # 关闭文件描述符3
exec 4<&- # 关闭文件描述符4

脚本实例

nginx 状态判断,如不存在则启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/bin/bash

check_nginx() {
ps -ef | grep "/usr/sbin/nginx" | grep -v grep > /dev/null
return $?
}

while true
do
if check_nginx
then
echo "nginx存在 休息2s"
sleep 2
continue
fi
echo "启动nginx..."
systemctl start nginx
sleep 2
if check_nginx
then
echo "重新启动nginx成功"
continue
fi
echo "nginx无法启动 请检查配置"
break
done

日志文件压缩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash

LOG_DIR=./logs
[ -d ${LOG_DIR} ] || mkdir ${LOG_DIR}
cd ${LOG_DIR}

# 写入压缩日志
log_out() {
log_path="compress.log"
echo $1 >> $log_path
}

# 每10秒生成日志文件
produce_log_file() {
while true
do
host_count=$(($RANDOM % 7 + 2)) # 随机生成[2,8]之间的整数
now_time=$(date "+%H%M%S") # date命令的格式化
declare -a host_arr
for i in $(seq 0 $host_count)
do
host_arr[$i]="10.83.26.$(($i + 10))"
done

for host in "${host_arr[@]}"
do
echo "生成日志文件:${host}_${now_time}.access.log"
touch ${host}_${now_time}.access.log
done
sleep 10
done
}

# 压缩日志文件
compress_log_file() {
while true
do
sleep 1
compress_time=$(find . -name "*.access.log" -exec basename {} \; | awk -F _ '{print $2}' | awk -F . '{print $1}' | sort -r | uniq | awk 'NR==1{print}')
if [ -z "$compress_time" ]
then
log_out "当前没有日志文件要压缩"
continue
fi
log_out "压缩 ${compress_time} 文件..."
tar czf log_compress_${compress_time}.tar.gz $(find . -name "*${compress_time}.access.log")
if [ $? -eq 0 ] && [ -e "log_compress_${compress_time}.tar.gz" ]
then
find . -name "*${compress_time}.access.log" -exec rm -f {} \;
fi
log_out "压缩 ${compress_time} 文件完成"
done
}

produce_log_file &
compress_log_file &

EOF

本文作者:liyijie

本文链接:https://liyijie.cn/2023/ops-automation-1/

文章默认以 署名-非商业性使用-相同方式共享 授权。