提示:以下是本篇文章正文内容,下面案例可供参考
一、Shell脚本编写入门
1. Shell是什么?
Shell是介于外部程序和操作系统内核之间的一层解释层,它作用是能够将用户输入的Linux命令翻译为操作系统能够调用或理解的指令。shell与操作系统及物理硬件的关系大致如下图所示:
2. Shell脚本
Shell脚本是包含一系列命令的文本文件,通过编写一系列Shell命令、控制结构(如循环、条件语句等)、正则表达式和管道等机制能够完成自动化任务和管理计算机。例如,编写一个Shell脚本帮我们完成下载并运行MySQL服务。该脚本首先会创建相关文件夹用于保存相关数据,之后下载MySQL,然后进行相关信息配置,最后启动MySQL服务。而对于使用者只需指定Shell解释器执行该脚本即可,如果别的服务器也需要安装运行MySQL,只需要将该脚本上传并执行即可。通过编写Shell脚本完成自动化任务能够简化管理实现快速开发的目的。
3. Shell解释器
Shell解释器是操作系统中负责解析和执行用户输入命令的程序。Shell脚本编写第一行往往会定义执行该脚本的解释器即”shebang“,如:#!/bin/bash。执行脚本时也可以指定解释器,如:sh helloword.sh。常见的解释器有以下几种:
- Bash (Bourne-Again Shell):大多数现代Linux发行版默认使用的shell,功能丰富且兼容sh。
- sh (Bourne Shell):最早的Unix shell之一,许多其他shell都基于或兼容sh语法。
- csh (C Shell) 和 tcsh (TENEX C Shell):提供了一些类似于C语言语法结构的特性。
- ksh (Korn Shell):由贝尔实验室开发,包含了sh的功能,并添加了许多增强特性。
- zsh (Z Shell):提供了丰富的扩展功能,包括更强大的自动补全、提示符定制等
4. Shell脚本执行
编写一个输出"Hello world!"的shell脚本:
# 创建并编辑脚本
vim sayHelloWorld.sh
# 以下为脚本内容
#!/bin/bash
echo 'Hello World!'
PS:与Windows不同,在Linux中文件的后缀名并不决定该文件执行方式,只是作为类型区分的标识。例如,Shell脚本的后缀名可以是.config、.bat或者没有后缀名。
查看脚本权限:
新建的脚本是没有执行权限的,需要为脚本添加执行权限:
//所有用户均可执行
chmod a+x sayHelloWorld.sh
执行脚本:
- 也可以使用sh sayHelloWorld.sh的方式指定解释器运行脚本
二、Shell语法学习
1. Shell变量
Shell变量主要包括:用户自定义变量、特殊变量和环境变量。在Shell中(如Bash)定义的变量默认类型都是字符串类型。下面依次介绍:
1.1. 用户自定义变量
该变量是由用户自定义的变量,只在当前Shell中生效。声明方式为:变量名=变量值(注意:等号两边不能有空格)
- ${变量名} 和 $变量名都可以使用该变量的值,区别在于前者可以避免变量和后续字符串混淆。
- 局部变量的声明方式为:local 变量名,该变量只在定义变量的函数范围内有效。
1.2. 特殊变量
在Shell中以下变量具有特殊含义:
变量 | 含义 |
---|---|
$? | 表示上一条指令或函数的退出状态码,0表示成功,非零为失败 |
$0 | 表示当前执行脚本的名称 |
$n | 表示当前脚本执行传递的第n个参数,n取值为1 - n |
$# | 表示当前脚本执行参数的个数 |
$* | $* 表示用空格将当前脚本执行传递的参数连接成一个字符串,"$*" 表示意义同$* |
$@ | $@ 表示用空格将当前脚本执行传递的参数连接成一个字符串,"$@" 表示参数独立性会分别输出每个参数 。如:for arg in “$@” do echo “当前参数: ${arg}” |
$! | 表示最近在后台运行进程的PID,运行进程时末尾加上&表示当前进程在后台执行 |
1.3. 环境变量
环境变量又分为自定义环境变量(使用export声明,unset撤销变量)和内置环境变量,如:PATH、HOME和PWD等。
- 使用以上方式声明的环境变量仅在当前Shell会话中有效,一旦打开一个新的会话,该变量就会失效。
- 如果想让变量永久生效可以在用户家目录中的.bashrc中加入:export NAME=xiaoming,之后使用source重新加载配置文件或在用户下一次登录就会加载该变量。
以上操作仅对当前用户有效,定义全局变量即所有用户都可以访问到,可以修改/etc/profile。用户定义在/etc/profile和用户家目录中的.bashrc变量相同时会优先使用用户家目录中的配置。
2. 特殊符号
以下符号均具有特殊含义,也是编写Shell脚本最常用的符号:
符号 | 含义 |
---|---|
; | 分割多个命令,使一行中的多个命令能够被顺序执行 |
& | 表示在后台运行指令或进程,如果&左右两边连接的是两个命令,如:command1 & command2那么表示立即在后台运行command1,在前台运行command2可以实现异步执行的效果 |
&& | 连接两个命令,表示第一个命令执行成功后第二个命令才会执行 与l l符号作用相反 |
> | 重定向输出,如 echo “Hello” > sayHelloWorld.sh,指令执行后覆盖sayHelloWorld.sh中的内容 |
>> | 追加输出,如: echo “Hello” > sayHelloWorld.sh,指令执行后会将Hello追加到sayHelloWorld.sh文件末尾 |
’ ’ | 与其它语言一样,字符串原样返回 |
" " | 与其它语言一样,会解析特殊字符如”$(echo hello)“ |
` ` | 优先执行` `中的命令并将命令的输出结果作为字符串插入到当前命令行中 |
| | 管道符,将前一个命令的执行结果传递给后一个命令 |
(()) | 用于算术运算和整数比较。在双小括号内部,可以执行整数的加减乘除、位运算、比较运算以及变量的赋值等 |
[ ] | 用于条件测试,它是一个命令,用于测试文件属性、字符串比较和整数比较等 |
3. 数学运算及字符串操作
3.1 数学运算
算术运算符号:
算数运算符 | 含义 |
---|---|
+、-、\ *、\ 、% | 加、减、乘、除 、取模 |
** | 幂运算 |
++、– | 自增、自减 |
||、&&、! | 或、与、非 |
==、!=、= | 是否相等、是否不等、=用于字符串比较相当于== |
<<、>> | 左移位操作、右移位操作 |
由于定义的变量为字符串类型,所以两个变量进行数学运算时需要不能直接使用运算符,例如:num1=1;num2=2;echo $num1+$num2输出结果为"1+2"。变量之间进行数学运算可以使用以下几种方式(进行乘法运算时需要使用\ *,*在Shell中是通配符):
- 使用(( )):echo $((num1 + num2))
- 使用[ ]:echo $[num1 + num2]
- 使用expr:expr $num1 + $num2
3.2. 字符串操作
定义变量str=abcdefg,有以下操作:
命令 | 含义 |
---|---|
echo ${#str} | 返回变量str的长度:7 |
echo ${str:2} | 从下标为2的字符开始截取到最后:cdefg |
echo ${str:0:3} | 从下标为0的字符开始截取3个字符并返回 |
echo ${str/abc/hello} | 用hello替换第一个匹配到的abc子串:hellodefg |
echo ${str/abc//hello} | 用hello替换所有匹配到的abc子串:hellodefg |
echo ${str#abc} | 从变量开头删除最短匹配的abc子串,使用%表示从末尾匹配 |
echo ${str##abc} | 从变量末尾删除最长匹配的abc子串,使用%表示从末尾匹配 |
4. 条件表达式
在Shell中使用[ ]和[[ ]]可以进行文件和字符串操作判断,如果是数值比较还可以使用(( ))。
- [ ]和[[ ]]的区别:[[ ]]提供了更加强大的功能,而[ ]指提供较少功能,此外在[[ ]]中可以直接使用==和模式匹配,而[ ]则不适用。使用[ ]时两边必须有空格,如:[ -e “sayHelloWorld.sh” ]
4.1. 操作符
- 文件操作符:
操作符 | 含义 |
---|---|
[ -e “sayHelloWorld.sh” ] | 测试文件是否存在 |
[ -f "sayHelloWorld.sh " ] | 判断文件是否为普通文件 |
[ -d “sayHelloWorld.sh” ] | 测试文件是否为目录 |
[ -r “sayHelloWorld.sh” ] | 测试文件是否可读 |
[ -w “sayHelloWorld.sh” ] | 测试文件是否可写 |
[ -x “sayHelloWorld.sh” ] | 测试文件是否可执行 |
[ -s “sayHelloWorld.sh” ] | 测试文件是否为空(为空返回真) |
[ -h “sayHelloWorld.sh” ] | 测试文件是否为符号链接 |
- 字符串操作符:
操作符 | 含义 |
---|---|
= | 检查两个字符串是否相等 |
!= | 检查两个字符串是否不相等 |
-z | 检查 字符串是否为空 |
-n | 检查字符串是否非空 |
- 数值操作符,如果是浮点数比较需要使用bc或awk命令,如:
if (( $(echo "3.14 > 2.66" | bc -l) )); then
echo "$num1 is greater than $num2"
fi
操作符 | 含义 |
---|---|
-eq | 检查两边的整数值是否相等 |
-ne | 检查两边的整数值是否不相等 |
-gt | 检查左边的整数值是否大于右边的整数值 |
-lt | 检查左边的整数值是否小于右边的整数值 |
-ge | 检查左边的整数值是否大于等于右边的整数值 |
-le | 检查左边的整数值是否小于等于右边的整数值 |
4.2. if语句语法
- 单分支:
if <条件表达式>; then
[语句1]
else
[语句2]
fi
## fi代表if语句结束,也可写成
if <条件表达式>
then
[语句1]
else
[语句2]
fi
- 多分支
if <条件表达式>; then
[语句1]
else
[语句2]
fi
## fi代表if语句结束,也可写成
if <条件表达式>
then
[语句1]
elif <条件表达式>
then
[语句2]
else
[语句3]
fi
- 判断脚本输入参数是数字还是字母
#!/bin/bash
# 获取第一个参数
arg="$1"
# 使用正则表达式判断参数是否只包含数字或字母
if [[ $arg =~ ^-?[0-9]+$ ]]; then
echo "入参是数字"
elif [[ $arg =~ ^[[:alpha:]]+$ ]]; then
echo "入参是字母"
else
echo "入参既非纯数字也非纯字母"
fi
4.3. case语句语法
- 语法
case 值 in
模式1)
commands...
;;
模式2)
commands...
;;
模式N)
commands...
;;
*)
default_commands...
;;
esac
- 如果一个脚本允许多种执行模式,往往会使用到case语句
#!/bin/bash
action="$1"
case "$action" in
start)
echo "Starting the service..."
;;
stop)
echo "Stopping the service..."
;;
restart)
echo "Restarting the service..."
;;
*)
echo "Invalid action. Valid actions are: start, stop, restart."
;;
esac
5. 循环语句语法
Shell中有三种循环结构分别是for循环、while循环、until循环,编写脚本是需要根据具体情况选择合适的循环结构。
5.1. for循环语句语法
# 最基础的for循环,常用于遍历列表或数组
for var in item1 item2 item3; do
echo "$var"
done
# C-style的for循环,用于计数或迭代
for (( 初始化表达式 ; 循环条件 ; 更新表达式 )); do
# 循环体内的命令
done
- 用于打印从1到10的整数
#!/bin/bash
# for循环打印1到10的整数
for (( i=1; i<=10; i++ )); do
echo "当前打印数: $i"
done
5.2. while循环语句语法
# 条件满足时持续执行循环体内的命令
while [ condition ]; do
# 循环体内的命令
done
# 或者使用双括号语法
while [[ condition ]]; do
# 循环体内的命令
done
- 读取用户输入直到用户输入“quit”为止
#!/bin/bash
while true; do
read -p "请输入(输入 quit 退出):" user_input
if [[ $user_input == "quit" ]]; then
break
else
echo "你输入的是:$user_input"
fi
done
5.3. until循环语句语法
# 条件不满足时持续执行循环体内的命令
until [ 条件]; do
# 循环体内的命令
done
# 或者使用双括号语法
until [[ condition ]]; do
# 循环体内的命令
done
- 用户输入密码,直到输入正确的密码才能进入系统:
#!/bin/bash
# 正确的密码
password="secret"
# 使用until循环直到正确输入密码
until [ "$input_password" = "$correct_password" ]; do
# 请求用户输入密码
read -s -p "请输入密码: " input_password
# 检查密码是否正确
if [ "$input_password" = "$password" ]; then
echo "密码正确!"
break
else
echo "密码错误,请重新输入。"
fi
done
echo "欢迎进入系统!"
6. 函数定义与调用
6.1. 函数定义语法
#标准写法
function 函数名(){
# 函数体
# return可有可无
return 返回值
}
#省略()写法
function 函数名{
# 函数体
# return可有可无
return 返回值
}
# 省略function关键字写法,需要加()
函数名(){
# 函数体
# return可有可无
return 返回值
}
6.2. 函数调用
- 执行shell函数,只需要写函数名称即可不需要写( )
- 函数调用要在函数定义后才能调用,因为Shell脚本是从上到下加载的
- 使用local关键字定义函数体内的局部变量
- 可以使用source加载另一个文件中定义的函数
#!/bin/bash
# 定义一个函数,接受两个参数
function greet {
echo "你好, $1!"
echo "你喜欢吃$2水果吗?"
}
# 调用函数并传入参数
greet "xiaoming" "草莓"
# 再次调用函数,传入不同的参数
greet "kangkang" "苹果"
7. Shell常用命令
7.1. echo 命令
打印指定文本或变量值
命令 | 含义 |
---|---|
echo -n ”Hello“ | 不在输出末尾添加换行符。默认情况下,echo 命令会输出一个换行符 |
echo -e ”Hello\t“ | 会识别" "中的转义字符,与-E作用相反 |
7.2. wc命令
统计文本或文件包含的字节、行数、单词数
命令 | 含义 |
---|---|
wc - l sayHelloWorld.sh | 统计sayHelloWorld.sh文件中的行数 |
wc - L sayHelloWorld.sh | 统计sayHelloWorld.sh文件中最长行的行数 |
wc - w sayHelloWorld.sh | 统计sayHelloWorld.sh文件中单词的个数(words,以空格、换行符、制表符等作为单词分隔符) |
wc - m sayHelloWorld.sh | 统计sayHelloWorld.sh文件中字符的个数 |
wc - c sayHelloWorld.sh | 统计sayHelloWorld.sh文件中的字节数 |
7.3. read命令
交互式读取用户输入的一行文本,并将其内容赋值给指定的变量,常用于函数中
命令 | 含义 |
---|---|
read var | 用户输入一行文本后,read 将这一行的内容赋值给 变量var |
read -p "请输入您的名字: " name | 提示信息,指定用户输入之前显示的提示文本 |
read -t 5 -p "请输入您的名字: " name | 等待5秒,若无输入则退出 |
read -n 3 var | 读取用户输入文本的前三个字符并将其赋值给var变量 |
read -s -p "请输入密码: " password | 隐藏用户输入的内容 |
read -a array | 读取输入并将其以输入的空格为分割拆分成数组的元素 |
read -d ‘;’ var | 读取到指定的字符";"时停止 |
7.4. cut命令
用于从文件或标准输入中截取指定的字符、字段或字节范围。它可以基于分隔符来切割文本,并将切割出来的部分输出到屏幕或重定向到其他文件。
命令 | 含义 |
---|---|
echo “Hello, World!” | cut -b1-5 | 以字节为单位切割文件内容,输出Hello |
echo “Hello, World!” | cut -c1-5 | 与 -b 类似,但以字符为单位切割,而非字节,输出:Hello |
echo “apple :banana :cherry” | cut -d: -f1 | 指定字段(field)分隔符。默认分隔符是Tab,但可以更改。 |
echo ‘apple :banana :cherry’ | cut -d: -f1,3 | 指定要截取的字段编号或范围,输出apple:cherry |
7.5. grep命令
用于搜索文本文件中匹配特定模式(如正则表达式)的行的命令
命令 | 含义 |
---|---|
grep ‘hello’ textfile.txt | 在 textfile.txt 文件中查找包含 “hello” 的行 |
grep -i ‘Hello’ textfile.txt | 忽略大小写,查找包含 “HELLO”、“hello” 等大小写形式的行 |
grep -v ‘hello’ textfile.txt | 反向匹配,输出不包含 “hello” 的行 |
grep -n ‘hello’ textfile.txt | 输出匹配行时显示对应的行号 |
grep -c ‘hello’ textfile.txt | 输出包含 “hello” 的行数 |
grep -e ‘hello’ -e ‘world’ textfile.txt | 查找包含 “hello” 或 “world” 的行 |
grep -E ‘hello|world’ textfile.txt | 查找包含 “hello” 或 “world” 的行 |
grep -w ‘hello’ textfile.txt | 查找完整单词 “hello”,而不是包含 “hello” 的其他单词,如 “hellos” 或 “yellow” |
grep -A 2 ‘hello’ textfile.txt | 输出包含 “hello” 的行及其后两行 |
grep -B 2 ‘hello’ textfile.txt | 输出包含 “hello” 的行及其前两行 |
grep -C 2 ‘hello’ textfile.txt | 输出包含 “hello” 的行及其前后两行 |
grep -r ‘hello’ directory/ | 在 directory/ 及其子目录下的所有文件中搜索包含 “hello” 的行 |
7.6. sed命令
用于对文本进行查找、替换、删除、插入等多种行级别的编辑操作。它可以从文件、管道或其他输入源读取文本,对每一行进行处理后,将结果输出到屏幕或重定向到另一个文件
选项 | 含义 |
---|---|
-n | 抑制默认的打印操作,只有经过命令修改的行才会被打印 |
-i | 直接编辑原文件,如果不指定备份扩展名,则覆盖原文件;如果指定了备份扩展名(如 -i.bak),则在编辑前先备份原文件 |
-e | 允许在同一行内指定多个编辑命令 |
-f | 从指定的脚本文件中读取编辑命令 |
命令 | 含义 |
---|---|
s/pattern/replacement/flags | 替换命令,用 replacement 替换每一行中匹配 pattern 的部分。flags 可能包括:g:全局替换,同一行内所有匹配项都被替换、i:不区分大小写匹配、p:打印模式空间内容 |
sed -i ‘s/hello/hi/g’ file.txt | 替换 file.txt 中所有出现的 “hello” 为 “hi” |
sed -i ‘/^$/d’ file.txt | 删除 file.txt 中的所有空白行 |
sed -n ‘/hello/p’ file.txt | 只打印 file.txt 中包含 “hello” 的行 |
sed -i ‘/^#/a This is an appended line.’ file.txt | 在注释行(以 # 开头的行)后面追加文本 “This is an appended line.” |
sed -i ‘/^Start/i Inserted before’ file.txt | 在以 “Start” 开头的行之前插入文本 “Inserted before” |
7.7. awk命令
awk 是一种强大的文本分析和报告生成工具,它能够处理每一行数据,并基于指定的模式和动作对数据进行处理。语法:awk ‘条件{ 执行动作}’ 文件
内置变量 | 含义 |
---|---|
$0 | 整行的内容,表示当前处理的行 |
NF | 当前行的字段总数 |
NR | 当前已读取的记录行数,从1开始计数 |
FNR | 当前文件的记录数,也是从1开始计数,但在处理多个文件时,每处理一个新文件,FNR会重置为1 |
FILENAME | 当前正在处理的文件名 |
FS | 字段分隔符,默认是空格或制表符,通过 -F 选项更改 |
命令 | 含义 |
---|---|
awk ‘NR>2 {print}’ file.txt | 从第三行(NR 为3)开始,打印文件的每一行内容 |
awk -F: ‘{print $1, $2}’ /etc/passwd | 将默认的字段分隔符改为冒号并打印 |
awk ‘$2 >= 10 { print $1, $2 }’ input.txt | 根据条件打印特定字段 |
awk -v limit=5 ‘$1 > limit { print }’ input.txt | 在处理过程中使用变量limit,并打印出 input.txt 中第一列大于limit值的所有行 |
awk ‘{ sum += $1; n++ } END { print “Average:”, sum/n }’ numbers.txt | 计算 numbers.txt 文件中每一行第一个字段的总和,并计算平均值。END 关键字指示在所有行处理完成后执行的动作 |
三、实战
1. 编写一个使用安装并运行ngnix的脚本
#!/bin/bash
#创建挂载目录
makeDir(){
#进入到opt目录下
cd /opt
if [ -d "/ngnix" ]; then
rm -rf /ngnix
mkdir -p ./nginx/conf
mkdir -p ./nginx/conf/conf.d
mkdir -p ./nginx/html
mkdir -p ./nginx/log
else
mkdir -p ./nginx/conf
mkdir -p ./nginx/conf/conf.d
mkdir -p ./nginx/html
mkdir -p ./nginx/log
fi
}
#检查Docker环境
checkDockerEnv(){
if ! command -v docker &> /dev/null
then
echo "Docker未安装,请先安装Docker。"
exit 1
else
# 检查Docker守护进程是否正在运行
if ! systemctl is-active --quiet docker
then
echo "启动docker..."
systemctl start docker
#删除已存在nginx容器和镜像
nginx_containers=$(docker ps -aqf "name=nginx")
if [[ -n "$nginx_containers" ]]; then
docker stop $nginx_containers
docker rm $nginx_containers
docker rmi -f $nginx_containers
fi
nginx_image_ids=$(docker images --filter reference='*nginx*' -q)
# 强制删除找到的所有镜像
for image_id in $nginx_image_ids; do
docker rmi -f $image_id
done
fi
fi
}
runNginx(){
#拉取nginx镜像
echo "开始拉取Nginx镜像..."
docker pull nginx
#启动Nginx
echo "启动nginx..."
docker run -d \
--name my_nginx \
-p 80:80 \
nginx
#copy容器中的配置文件
echo "copy配置文件..."
docker cp my_nginx:/etc/nginx/nginx.conf /opt/nginx/conf
docker cp my_nginx:/usr/share/nginx/html/index.html /opt/nginx/html
docker cp my_nginx:/etc/nginx/conf.d /opt/nginx/conf
#删除容器
docker stop my_nginx
docker rm my_nginx
#重新启动
echo "重新启动nginx..."
docker run -d \
--name my_nginx \
-p 80:80 \
-v /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /opt/nginx/conf/conf.d:/etc/nginx/conf.d \
-v /opt/nginx/html:/usr/share/nginx/html \
-v /opt/nginx/log:/var/log/nginx \
nginx
}
case "$1" in
start)
echo "Starting nginx service..."
makeDir
checkDockerEnv
runNginx
;;
restart)
if [ -n "`docker ps -qf "name=my_nginx"`" ]; then
echo "Restarting nginx service..."
docker restart my_nginx
else
echo "The nginx service is not running"
fi
;;
status)
if [ -n "`docker ps -qf "name=my_nginx"`" ]; then
echo "The nginx service is running"
else
echo "The nginx service is not running"
fi
;;
*)
echo "Usage: $0 {start|restart|status}"
exit 1
;;
esac
exit 0
总结
Shell脚本的编写在开发中经常能够接触到,从基础Linux命令到完成自动化运维任务,只有不断的练习才能提高脚本的编写水平,本文介绍了Shell脚本的基础知识希望对大家学习Shell有所帮助(●’◡’●)。