目录
过程式编程语言基本控制结构:顺序执行、选择执行、循环执行
一、bash基础特性
命令行展开:~, {}
命令别名:alias/unalias
命令历史:history
命令和路径补全:$PATH
glob: *, ?, [], [^]
快捷键:Ctrl+{a,e,l,c,u,k}
命令hash:hash
二、条件判断:if语句
选择执行:
单分支:
if CONDITION; then
if-true
fi
双分支:
if CONDITION; then
if-true
else
if-false
fi
多分支:
if CONDITION1; then
if-true
elif CONDITION2; then
if-ture
elif CONDITION3; then
if-ture
...
esle
all-false
fi
逐条件进行判断,第一次遇为“真”条件时,执行其分支,而后结束
判断条件CONDITION就是命令 [ ] 或shell命令
CONDITION:
bash命令:
用命令的执行状态结果
成功:true
失败:flase
P.S. 成功或失败的意义:取决于用到的命令
通常情况下脚本程序应当判断执行者是否符合条件
练习
用户键入文件路径,脚本来判断文件类型
#!/bin/bash
#
read -p "Enter a file path:" filename
if [ -z "$filename" ]; then
echo "Usage:Enter a file path."
exit 2
fi
if [ ! -e $filename ]; then
echo "No such file."
exit 3
fi
if [ -f $filename ]; then
echo "A common file."
elif [ -d $filename ]; then
echo "A directory."
elif [ -L $filename ]; then
echo "A symbolic file."
else
echo "Other type."
fi
注意:if语句可嵌套
三、用户交互
read [option]... [name ...]
-p 'PROMPT' 提示信息
-t TIMEOUT 等待超时( s )
bash -n /path/to/some_script
检测脚本中的语法错误
bash -x /path/to/some_script
调试执行
示例
#!/bin/bash
# Version: 0.0.1
# Author: MageEdu
# Description: read testing
read -p "Enter a disk special file: " diskfile
[ -z "$diskfile" ] && echo "Fool" && exit 1
if fdisk -l | grep "^Disk $diskfile" &> /dev/null; then
fdisk -l $diskfile
else
echo "Wrong disk special file."
exit 2
fi
四、循环
循环:for, while, until
循环体:要执行的代码;可能要执行n遍
进入条件:
退出条件:
1.for
for 变量名 in 列表; do
循环体
done
P.S. 循环体通常会用到变量
执行机制:
依次将列表中的元素赋值给“变量名”; 每次赋值后即执行一次循环体; 直到列表中的元素耗尽,循环结束
示例:添加10个用户, user1-user10;密码同用户名
#!/bin/bash
#
if [ ! $UID -eq 0 ]; then
echo "Only root."
exit 1
fi
for i in {1..10}; do
if id user$i &> /dev/null; then
echo "user$i exists."
else
useradd user$i
if [ $? -eq 0 ]; then
echo "user$i" | passwd --stdin user$i &> /dev/null
echo "Add user$i finished."
fi
fi
done
列表生成方式:
①直接给出列表
②整数列表:
(a) {start..end}
(b) $(seq [start [step]] end)
③返回列表的命令
$(COMMAND)
④glob
(b) 变量引用;
$@, $* 参数列表
示例:判断某路径下所有文件的类型
#!/bin/bash
#
for file in $(ls /var); do
if [ -f /var/$file ]; then
echo "Common file."
elif [ -L /var/$file ]; then
echo "Symbolic file."
elif [ -d /var/$file ]; then
echo "Directory."
else
echo "Other type."
fi
done
示例:统计出总共有多少种类型,每种类型有多少个
#!/bin/bash
#
declare -i estab=0
declare -i listen=0
declare -i other=0
for state in $( netstat -tan | grep "^tcp\>" | awk '{print $NF}'); do
if [ "$state" == 'ESTABLISHED' ]; then
let estab++
elif [ "$state" == 'LISTEN' ]; then
let listen++
else
let other++
fi
done
echo "ESTABLISHED:$estab"
echo "LISTEN:$listen"
echo "Unkown:$other"
练习
①/etc/rc.d/rc3.d目录下分别有多个以K开头和以S开头的文件
分别读取每个文件,以K开头的文件输出为文件加stop,以S开头的文件输出为文件名加start
“K34filename stop”
“S66filename start”
for file in $(ls /etc/rc.d/rc3.d)
do
if [[ $file =~ ^K.* ]]; then
echo "$file stop"
elif [[ $file =~ ^S.* ]]; then
echo "$file start"
fi
done
②写一个脚本,使用ping命令探测172.16.250.1-254之间的主机的在线状态 P.S. -c 指明次数 &>/dev/null
#!/bin/bash
#
net='172.16.250'
uphosts=0
downhosts=0
for i in {1..154};do
ping -c 1 -w 1 ${net}.$i &> /dev/null
if [ $? -eq 0 ] ;then
echo "${net}.$i is up."
let uphosts++
else
echo "${net}.$i is down."
let downhosts++
fi
done
echo "Up hosts: $uphosts."
echo "Down hosts: $downhosts."
2.while
while CONDITION; do
循环体
done
CONDITION:循环控制条件;进入循环之前,先做一次判断;每一次循环之后会再次做判断
条件为“true”,则执行一次循环;直到条件测试状态为“false”终止循环
因此:CONDTION一般应该有循环控制变量;而此变量的值会在循环体不断地被修正
示例:求100以内所有正整数之和
#!/bin/bash
declare -i sum=0
declare -i i=1
while [ $i -le 100 ]; do
let sum+=$i
let i++
done
echo "Summary:$sum."
练习
①添加10个用户
#!/bin/bash
#
declare -i n=1
while [ $n -le 10 ];do
id user$n &> /dev/null
if [ $? -eq 0 ];then
echo "user$n is exits."
else
useradd user$n &> /dev/null
echo "user$n" | passwd --stdin user$n
echo "user$n adds successfully!"
fi
let n++
done
②通过ping命令探测172.16.250.1-254范围内的所有主机的在线状态
declare -i n=1
declare -i upnum=0 && downnum=0
while [ $n -le 254 ];do
ping -c 1 -w 1 172.16.250.$n &> /dev/null
if [ $? -eq 0 ];then
echo "172.16.250.$n is up."
let upnum++
else
echo "172.16.250.$n is down."
let downnum++
fi
let n++
done
echo "upnum:$upnum"
echo "downnum:$downnum"
③打印九九乘法表 (分别使用for和while循环实现)
for j in {1..9};do
for i in $(seq 1 $j);do
echo -n -e "${i}X${j}=$[$i*$j]\t"
let i++
done
echo
done
declare -i j=1 && i=1
while [ $j -le 9 ];do
while [ $i -le $j ];do
echo -ne "${i}X${j}=$[$i*$j]\t"
let i++
done
echo
let j++
let i=1
done
④利用RANDOM生成10个随机数字,输出这个10数字,并显示其中的最大者和最小者
declare -i max=0 min=0 i=1
while [ $i -le 10 ];do
rand=$RANDOM
echo $rand
if [ $i -eq 1 ];then
max=$rand
min=$rand
fi
if [ $max -lt $rand ];then
max=$rand
fi
if [ $min -gt $rand ];then
min=$rand
fi
let i++
done
echo "max=$max"
echo "min=$min"
3.until
until CONDITION; do
循环体
done
进入条件:false
退出条件:true
练习
①求100以内所正整数之和
#!/bin/bash
#
declare -i i=1
declare -i sum=0
until [ $i -gt 100 ]; do
let sum+=$i
let i++
done
echo "Sum:$sum"
②打印九九乘法表
declare -i j=1
declare -i i=1
until [ $j -gt 9 ]; do
until [ $i -gt $j ]; do
echo -n -e "${i}X${j}=$[$i*$j]\t"
let i++
done
echo
let i=1
let j++
done
4.循环控制语句(用于循环体中)
①continue [N]:提前结束第N层的本轮循环,而直接进入下一轮判断
while CONDTIITON1; do
CMD1
...
if CONDITION2; then
continue
fi
CMDn
...
done
②break [N]:提前结束循环
while CONDTIITON1; do
CMD1
...
if CONDITION2; then
break
fi
CMDn
...
done
示例:求100以内所有偶数之和;要求循环遍历100以内的所正整数
#!/bin/bash
#
declare -i i=0
declare -i sum=0
until [ $i -gt 100 ]; do
let i++
if [ $[$i%2] -eq 1 ]; then
continue
fi
let sum+=$i
done
echo "Even sum:$sum"
5.创建死循环
while true; do
循环体
done
until false; do
循环体
done
示例:每隔3秒钟到系统上获取已经登录的用户的信息;如果docker登录了,则记录于日志中,并退出
#!/bin/bash
#
read -p "Enter a user name:" username
while true; do
if who | grep "^$username" &> /dev/null; then
break
fi
sleep 3
done
echo "$username logged on." >> /tmp/user.log
第二种实现:
read -p "Enter a user name:" username
until who | grep "^$username" &> /dev/null; do
sleep 3
done
echo "$username logged on." >> /tmp/user.log
6.while循环的特殊用法(遍历文件的每一行)
while read line; do
循环体
done < /PATH/FROM/SOMEFILE
依次读取/PATH/FROM/SOMEFILE文件中的每一行,且将行赋值给变量line
示例:找出其ID号为偶数的所有用户,显示其用户名及ID号
#!/bin/bash
while read line;do
if [ $[`echo $line | cut -d:-f3` % 2] -eq 0 ];then
echo -e -n "username:`echo $line | cut -d:-f1`\t"
echo "uid:`echo $line | cut -d:-f3 `"
fi
done < /etc/passwd
7.for循环的特殊格式
for ((控制变量初始化;条件判断表达式;控制变量的修正表达式)); do
循环体
done
控制变量初始化:仅在运行到循环代码段时执行一次;
控制变量的修正表达式:每轮循环结束会先进行控制变量修正运算,而后再做条件判断
示例:求100以内所正整数之和
declare -i sum=0
for ((i=1;i<=100;i++)); do
let sum+=$i
done
echo "Sum:$sum."
示例:打印九九乘法表
for((j=1;j<=9;j++));do
for((i=1;i<=j;i++))do
echo -e -n "${i}X${j}=$[$i*$j]\t"
done
echo
done
练习
写一个脚本,完成如下任务
①显示一个如下菜单:
cpu) show cpu information;
mem) show memory information;
disk) show disk information;
quit) quit
②提示用户选择选项
③显示用户选择的内容
cat << EOF
cpu) show cpu information;
mem) show memory information;
disk) show disk information;
quit) quit
============================
EOF
read -p "Enter a option:" option
while [ "$option" != 'cpu' -a "$option" != 'mem' -a "$option" != 'disk' -a "$option" != 'quit' ]; do
read -p "Wrong option, Enter again:" option
done
if [ "$option" == 'cpu' ]; then
lscpu
elif [ "$option" == 'mem' ]; then
cat /proc/meminfo
elif [ "$option" == 'disk' ]; then
fdisk -l
else
echo "Quit"
exit 0
fi
五、条件判断:case语句
case 变量引用 in
PAT1)
分支1
;;
PAT2)
分支2
;;
...
*)
默认分支
;;
esac
示例:用户选择,并显示完成后不退出脚本;而是提示用户继续选择显示其它内容;直到使用quit方始退出
#!/bin/bash
#
cat << EOF
cpu) show cpu information
mem) show memory information
disk) show disk information
quit) quit
============================
EOF
read -p "Enter a option:" option
until [ $option == "quit" ];do
while [ "$option" != 'cpu' -a "$option" != 'mem' -a "$option" != 'disk' -a "$option" != 'quit' ];do
read -p "Wrong option, enter again:" option
done
case "$option" in
cpu)
lscpu
;;
mem)
cat /proc/meminfo
;;
disk)
fdisk -l
;;
*)
echo "Quit"
exit 0
esac
read -p "Enter a option:" option
done
echo "Quit"
exit 0
总结:until, while, for, case
六、函数
function:函数
过程式编程:代码重用
模块化编程
结构化编程
语法一:
function f_name {
...函数体...
}
语法二:
f_name() {
...函数体...
}
调用:函数只有被调用才会执行
调用方式:给定函数名
函数名出现的地方,会被自动替换为函数代码
函数的生命周期:被调用时创建,返回时终止
return命令返回自定义状态结果
0:成功
1-255:失败
示例:添加用户
function adduser {
if id $username &> /dev/null; then
echo "$username exists."
return 1
else
useradd $username
[ $? -eq 0 ] && echo "Add $username finished." && return 0
fi
}
for i in {1..10}; do
username=myuser$i
adduser
done
练习
写一个服务脚本,完成如下要求
①脚本可接受参数:start, stop, restart, status
②如果参数非此四者之一,提示使用格式后报错退出
③如果是start:则创建/var/lock/subsys/SCRIPT_NAME, 并显示“启动成功”
考虑:如果事先已经启动过一次,该如何处理?
④如果是stop:则删除/var/lock/subsys/SCRIPT_NAME, 并显示“停止完成”
考虑:如果事先已然停止过了,该如何处理?
⑤如果是restart,则先stop, 再start
考虑:如果本来没有start,如何处理?
⑥如果是status, 则
如果/var/lock/subsys/SCRIPT_NAME文件存在,则显示“SCRIPT_NAME is running...”
如果/var/lock/subsys/SCRIPT_NAME文件不存在,则显示“SCRIPT_NAME is stopped...”
其中:SCRIPT_NAME为当前脚本名
#!/bin/bash
#
#chkconfig:- 88 12
#description:test service script
#
program=$(basename $0)
lockfile=/var/lock/subsys/$program
start(){
if [ -e $lockfile ];then
echo "$program is already running."
return 0
else
touch $lockfile
[ $? -eq 0 ] && echo "starting $program finished."
fi
}
stop(){
if [ -e $lockfile ];then
rm -rf $lockfile && echo "Stop $program OK."
else
echo "$program is stopped yet."
fi
}
status(){
if [ -e $lockfile ];then
echo "$program is running."
else
echo "$program is stopped."
fi
}
usage(){
echo "Usage:$program { start | stop | restart | status }"
}
if [ $# -lt 1 ];then
usage
exit 1
fi
case $1 in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
status)
status
;;
*)
usage
;;
esac
函数返回值:
函数的执行结果返回值:
①使用echo或print命令进行输出
② 函数体中调用命令的执行结果
函数的退出状态码:
①默认取决于函数体中执行的最后一条命令的退出状态码
②自定义退出状态码:
return
函数可以接受参数:
传递参数给函数:调用函数时,在函数名后面以空白分隔给定参数列表即可;例如“testfunc arg1 arg2 ...”
在函数体中当中,可使用$1, $2, ...调用这些参数;还可以使用$@, $*, $#等特殊变量
示例:添加10个用户
#!/bin/bash
#
function adduser {
if [ $# -lt 1 ]; then
return 2
# 2:no arguments
fi
if id $1 &> /dev/null; then
echo "$1 exists."
return 1
else
useradd $1
[ $? -eq 0 ] && echo "Add $1 finished." && return 0
fi
}
for i in {1..10}; do
adduser myuser$i
done
变量作用域:
本地变量:当前shell进程;为了执行脚本会启动专用的shell进程;因此,本地变量的作用范围是当前shell脚本程序文件
局部变量:函数的生命周期;函数结束时变量被自动销毁
在函数中定义局部变量的方法:
local NAME=VALUE
函数递归:
函数直接或间接调用自身
示例: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
示例:求n阶斐波那契数列
#!/bin/bash
#
fab() {
if [ $1 -eq 1 ]; then
echo 1
elif [ $1 -eq 2 ]; then
echo 1
else
echo $[$(fab $[$1-1])+$(fab $[$1-2])]
fi
}
fab $1
示例:打印NN乘法表,使用函数实现
declare -i i=1 && j=1
mul(){
until [ $j -gt $1 ];do
until [ $i -gt $j ];do
echo -en "${i}X${j}=$[$i*$j]\t"
let i++
done
echo
let i=1
let j++
done
}
mul $1
七、数组
变量:存储单个元素的内存空间
数组:存储多个元素的连续的内存空间
数组名
索引:编号从0开始,属于数值索引
P.S. 索引也可支持使用自定义的格式,而不仅仅是数值格式
bash的数组支持稀疏格式
引用数组中的元素:${ARRAY_NAME[INDEX]}
声明数组:
declare -a ARRAY_NAME
declare -A ARRAY_NAME:关联数组(又称键值列表)
1.数组元素的赋值
①一次只赋值一个元素
ARRAY_NAME[INDEX]=VALUE
weekdays[0]="Sunday"
weekdays[4]="Thursday"
②一次赋值全部元素:
ARRAY_NAME=("VAL1" "VAL2" "VAL3" ...)
③只赋值特定元素:
ARRAY_NAME=([0]="VAL1" [3]="VAL2" ...)
④read -a ARRAY
2.引用数组元素
①单个元素
${ARRAY_NAME[INDEX]}
P.S. 省略[INDEX]表示引用下标为0的元素,花括号绝对不能漏
②所有元素
${ARRAY[@]}, ${ARRAY[*]}
3.数组的长度(数组中元素的个数)
${#ARRAY_NAME[*]}, ${#ARRAY_NAME[@]}
示例:生成10个随机数保存于数组中,并找出其最大值和最小值
#!/bin/bash
declare -a rand
declare -i max=0 && min=32767
for i in {0..9};do
rand[$i]=$RANDOM
echo ${rand[$i]}
[ ${rand[$i]} -gt $max ] && max=${rand[$i]}
[ ${rand[$i]} -lt $min ] && min=${rand[$i]}
let i++
done
echo "max=$max min=$min"
写一个脚本 :
定义一个数组,数组中的元素是/var/log目录下所有以.log结尾的文件;要统计其下标为偶数的文件中的行数之和
declare -a files
files=(/var/log/*.log)
declare -i lines=0
for i in $(seq 0 2 $[${#files[*]}-1]);do
let lines+=$(wc -l ${files[$i]}| cut -d' ' -f1)
done
echo "lines=$lines"
4.数组切片
${ARRAY[@]:offset:number}
offset:要跳过的元素个数
number:要取出的元素个数
取偏移量之后的所有元素:${ARRAY[@]:offset}
5.向数组中追加元素
ARRAY[${#ARRAY[*]}]
6.删除数组中的某元素
unset ARRAY[INDEX]
7.关联数组
declare -A ARRAY_NAME
ARRAY_NAME=([index_name1]='val1' [index_name2]='val2' ...)
练习:生成10个随机数,升序或降序排序
declare -a rand
declare -i t
for i in {0..9};do
rand[$i]=$RANDOM
done
for((i=0;i<=9;i++))do
for((j=i+1;j<=9;j++))do
if [ ${rand[$i]} -gt ${rand[$j]} ];then
let t=${rand[$i]}
let rand[i]=${rand[$j]}
let rand[j]=t
fi
done
echo "${rand[$i]}"
done
八、bash的字符串处理工具
1.字符串切片
${var:offset:number}
取字符串的最右侧几个字符:${var:-lengh}
注意:冒号后必须有一空白字符
2.基于模式取子串
${var#*word}:其中word可以是指定的任意字符;功能:自左而右,查找var变量所存储的字符串中,第一次出现的word, 删除字符串开头至第一次出现word字符之间的所有字符
${var##*word}:同上,不过,删除的是字符串开头至最后一次由word指定的字符之间的所有内容
file="/var/log/messages"
${file##*/}:messages
${var%word*}:其中word可以是指定的任意字符;功能:自右而左,查找var变量所存储的字符串中,第一次出现的word, 删除字符串最后一个字符向左至第一次出现word字符之间的所有字符
file="/var/log/messages"
${file%/*}:/var/log
${var%%word*}:删除字符串最右侧的字符向左至最后一次出现word字符之间的所有字符
示例:url=http://www.tudou.com:80
${url##*:}
${url%%:*}
3.查找替换
${var/pattern/substi}:查找var所表示的字符串中,第一次被pattern所匹配到的字符串,以substi替换之
${var//pattern/substi}:查找var所表示的字符串中,所有能被pattern所匹配到的字符串,以substi替换
${var/#pattern/substi}:查找var所表示的字符串中,行首被pattern所匹配到的字符串,以substi替换之
${var/%pattern/substi}:查找var所表示的字符串中,行尾被pattern所匹配到的字符串,以substi替换之
4.查找并删除
${var/pattern}:查找var所表示的字符串中,删除第一次被pattern所匹配到的字符串
${var//pattern}
${var/#pattern}
${var/%pattern}
5.字符大小写转换
${var^^}:把var中的所有小写字母转换为大写
${var,,}:把var中的所有大写字母转换为小写
6.变量赋值
${var:-value}:如果var为空或未设置,那么返回value;否则,返回var的值
${var:=value}:如果var为空或未设置,那么返回value,并将value赋值给var;否则,返回var的值
${var:+value}:如果var不空,返回value
${var:?error_info}:如果var为空或未设置,那么返回error_info;否则,返回var的值
为脚本程序使用配置文件:
①定义文本文件,每行定义“filename=value”
②在脚本中source此文件即可
九、bash脚本编程用到的命令
1.mktemp
mktemp [OPTION]... [TEMPLATE]
TEMPLATE:filename.XXX
XXX至少要出现三个
OPTION:
-d:创建临时目录
--tmpdir=/PATH/TO/SOMEDIR:指明临时文件目录位置
2.install
install [OPTION]... [-T] SOURCE DEST
install [OPTION]... SOURCE... DIRECTORY
install [OPTION]... -t DIRECTORY SOURCE...
install [OPTION]... -d DIRECTORY...
选项:
-m MODE
-o OWNER
-g GROUP
练习
写一个脚本
①提示用户输入一个可执行命令名称
②获取此命令所依赖到的所有库文件列表
③复制命令至某目标目录(例如/mnt/sysroot)下的对应路径下
/bin/bash ==> /mnt/sysroot/bin/bash
/usr/bin/passwd ==> /mnt/sysroot/usr/bin/passwd
④复制此命令依赖到的所有库文件至目标目录下的对应路径下:
/lib64/ld-linux-x86-64.so.2 ==> /mnt/sysroot/lib64/ld-linux-x86-64.so.2
进一步地:
每次复制完成一个命令后,不要退出,而是提示用户键入新的要复制的命令,并重复完成上述功能;直到用户输入quit退出
#!/bin/bash
declare -i lines=0
read -p "Please type a command name: " cmd
while [ -z $cmd ];do
read -p "Please type a command name,again: " cmd
done
while [ $cmd != "quit" ];do
install -d /test/sysroot/bin/$cmd
cp /bin/$cmd /test/sysroot/bin/$cmd
let lines=`ldd /bin/$cmd | wc -l | cut -d' ' -f1`
for i in `seq 2 $lines`;do
file=`ldd /bin/$cmd | head -"$i" | tail -1`
file=${file#*/}
file=${file%%(*}
install -d /test/sysroot/$file
cp /$file /test/sysroot/$file
done
echo "Copy the command-$cmd finished!"
read -p "Please type a command name: " cmd
while [ -z $cmd ];do
read -p "Please type a command name,again: " cmd
done
done
回顾
bash脚本编程
编程语言:
数据结构
顺序执行
选择执行if, case
条件测试
运行命令或[[ EXPRESSION ]]
执行状态返回值
循环执行for, while, until
将某代码段重复运行多次
重复运行多少次?
循环次数事先已知
循环次数事先未知
P.S. 必须有进入条件和退出条件
函数:结构化编程及代码重用
function
——bash -n、bash -x
——if语句
单分支:
if CONDITION; then
if-true
fi
双分支:
if CONDITION; then
if-true
else
if-false
fi
多分支:
if CONDITION1; then
if-true
elif CONDITION2; then
if-ture
elif CONDITION3; then
if-ture
...
esle
all-false
fi
——case语句
case 变量引用 in
PAT1)
分支1
;;
PAT2)
分支2
;;
...
*)
分支n
;;
esac
case支持glob风格的通配符:
*:任意长度任意字符;
?:任意单个字符;
[]:指定范围内的任意单个字符;
a|b:a或b
——for循环
for NAME in LIST; do
循环体
done
——while循环
while CONDITION; do
循环体
done
进入条件:CONDITION为true
退出条件:false
——until循环
until CONDITION; do
循环体
done
进入条件:CONDITION为false
退出条件:true
——函数:模块化编程
function f_name {
...函数体...
}
f_name() {
...函数体...
}
return命令
参数:
函数体中调用参数:$1, $2, ...
$*, $@, $#
向函数传递参数:
函数名 参数列表
——数组
数组声明:declare -a
index:0-
关联数组:declare -A
——字符串处理
切片、查找替换、查找删除、变量赋值
参考资料:
马哥随堂笔记
注:诚恳欢迎读者对本文提出批评意见,若发现存在错误,我定第一时间修改。如果读者觉得文章对您有帮助,欢迎点赞鼓励一下哟٩(๑❛ᴗ❛๑)۶。