该篇文章主要介绍shell是什么、shell脚本的基本语法以及一些实例展示,带我们快速入门shell脚本编程。
shell与shell脚本
Shell 是操作系统中用户与内核之间的桥梁,它是一种"命令行解释器"(Command Line Interpreter),可以接收用户输入的命令并将其传递给操作系统执行。我们通常所说的 "Shell 编程" 或 "Shell 脚本",就是用这种命令行语言编写的程序。所以我们要区分两种概念,shell本身是一个"命令行解释器",是用户输入的命令与操作系统之间交互的桥梁;而shell脚本就是一组写在文件里的命令,本质上shell脚本就是把多个命令写在一个文件中,让shell解释器一次执行完。
shell在类Unix系统中扮演的角色
在类 Unix 系统中,系统结构大致如下:
+----------------------------+
| 用户 (User) |
+-------------+--------------+
|
v
+-------------+--------------+
| Shell(命令行) | ⇦⇨ 用户输入命令 & 获取输出
+-------------+--------------+
|
v
+-------------+--------------+
| 系统调用接口(API) | ⇦⇨ Shell 通过系统调用与内核沟通
+-------------+--------------+
|
v
+-------------+--------------+
| 操作系统内核(Kernel)| ⇦⇨ 管理硬件资源、调度程序等
+-------------+--------------+
|
v
+-------------+--------------+
| 硬件(CPU、内存等) |
+----------------------------+
假设我们在命令行中输入了 ls -l:
用户输入命令:
+-------------------+
| User Terminal |
| 输入:ls -l |
+---------+---------+
|
v
Shell 解释命令:
+---------+---------+
| Shell (bash) |
| 解析为 exec('ls')|
+---------+---------+
|
v
通过系统调用执行程序:
+---------+---------+
|系统调用接口 (syscall)
+---------+---------+
|
v
操作系统调度运行 ls:
+---------+---------+
| Kernel(内核)|
+---------+---------+
|
v
调用磁盘、文件系统等
+---------+---------+
| 硬件/文件系统 |
+-------------------+
Shell 的基本作用
-
解析命令:用户输入的命令由 Shell 解释并执行。
-
提供交互式界面:用户可以输入命令、查看输出、进行交互操作。
-
脚本执行:Shell 可以将一系列命令写入文件中,以脚本形式批量执行。
-
控制程序流程:支持变量、条件判断、循环等控制结构。
shell脚本的创建
我们可以通过vim编辑器直接创建脚本文件或使用touch创建脚本文件
vim myscript.sh
touch myscript.sh
在创建好.sh文件后,在该文件的第一行一般我们要指定下shell解释器(注意这不是必须的,但推荐在sh文件的第一行指定下具体的shell解释器,否则直接运行./xxx.sh时会根据系统默认的解释器如/bin/sh来执行脚本,如果你的.sh是基于bash解释器编写的那么可能会导致不兼容或报错)。
bash
#指定解释器为sh
#!/bin/sh
#指定解释器为bash
#!/bin/bash
.....
常见的shell解释器
shell解释器是一种统称,具体又会有不同的解释器,常见的shell解释器有sh、bash、zsh、dash等
解释器名称 | 路径 | 特点简述 |
---|---|---|
sh |
/bin/sh |
最基础的 Unix Shell,兼容性强,功能有限(POSIX 标准) |
bash |
/bin/bash |
Linux 默认 Shell,功能强大,支持数组、[[ ]] 条件表达式等 |
zsh |
/bin/zsh |
更强大、可配置、语法更灵活,配合 oh-my-zsh 很流行 |
dash |
/bin/dash |
Ubuntu 中 /bin/sh 的默认指向,轻量、执行快,但语法限制多 |
shell脚本的执行
shell脚本的执行方式有多种:
方式一:
首先为shell脚本加权限
chmod +x myscript.sh
直接运行该脚本
./myscript.sh
注意:如果创建脚本时我们在第一行制定了具体的解释器,那此时执行该脚本时会使用制定好的解释器,否则使用默认shell解释器sh
方式二:
显示指定解释器执行
bash
#指定用sh解释器
sh myscript.sh
#指定用bash解释器
bash myscript.sh
方式三:
在子shell中执行脚本内容(仍在当前shell环境中执行脚本,不是在新进程中),常用于设置环境变量、加载配置等
bash
source myscript.sh
或
. source myscript.sh
注意:source和.是shell命令,用来在当前shell环境中执行脚本内容,这意味着脚本中的所有命令都会在当前的shell环境中执行,而不是启动一个新进程。如果我们用./myscript.sh执行脚本,系统会启动一个新的shell进程来执行脚本,脚本中的环境变量或设置(如定义的变量、函数)会只在这个进程中有效 ,执行完后就消失了。但是,如果你用 source myscript.sh
或 . myscript.sh
执行脚本,脚本中的内容会在当前的 Shell 环境 中执行,这样脚本里设置的环境变量、函数等都能在执行完脚本后继续存在。
示例:
bash
#!/bin/bash
export MY_VAR="hello"
如果用./执行后再打印MY_VAR
bash
$ ./myscript.sh
$ echo $MY_VAR
输出:
bash
(没有任何输出)
但如果使用source再打印
bash
$ source myscript.sh
$ echo $MY_VAR
输出:
bash
hello
shell脚本中的变量
1.系统环境变量
系统环境变量是在操作系统启动或用户登录时自动定义的变量,用于保存系统配置、用户信息、路径设置等。
变量名 | 含义 |
---|---|
$HOME |
当前用户的主目录路径(如 /home/username ) |
$USER |
当前登录的用户名(如 root , john ) |
$PATH |
系统查找可执行命令的路径列表(冒号 : 分隔) |
$SHELL |
当前默认使用的 Shell 解释器路径(如 /bin/bash ) |
$PWD |
当前工作目录的完整路径(print working directory) |
$LANG |
当前语言/区域设置(如 en_US.UTF-8 ) |
bash
echo "你的用户名是:$USER"
echo "你的家目录是:$HOME"
echo "当前目录是:$PWD"
echo "Shell 程序是:$SHELL"
echo "命令搜索路径是:$PATH"
2.shell特殊变量
这些变量在脚本执行过程中自动生效,不需要定义,直接使用。
特殊变量 | 说明 |
---|---|
$0 |
当前脚本的文件名 |
$1 ~$9 |
传入脚本的第 1~9 个参数 |
$# |
传入参数的个数 |
$@ |
以"参数列表"的方式展开所有参数(保留参数引用) |
$* |
以"一个整体"的方式展开所有参数(不保留引用) |
$? |
上一条命令的退出状态码(0 表示成功) |
$$ |
当前脚本运行的进程 ID |
$! |
最后一个后台运行的进程的 PID |
bash
#!/bin/bash
echo "脚本名是:$0"
echo "第一个参数是:$1"
echo "第二个参数是:$2"
echo "参数个数是:$#"
echo "所有参数(\$@):$@"
echo "所有参数(\$*):$*"
echo "当前进程 PID:$$"
执行脚本
bash
chmod +x test.sh
./test.sh apple banana
输出
bash
脚本名是:./test.sh
第一个参数是:apple
第二个参数是:banana
参数个数是:2
所有参数($@):apple banana
所有参数($*):apple banana
当前进程 PID:3921
3.用户自定义变量
bash
name="Tom"
age=25
注意:等号两边不能有空格,否则会被解释成命令;变量名建议只使用字母、数字、下划线,不能以数字开头。
使用变量:
bash
echo "姓名是:$name"
echo "年龄是:$age"
补充:对于{变量}来说,的作用是引用变量的值,即用来访问变量的值,建议用{}包起变量名后再用取值,为了防止歧义。运算符还有多种作用,后续会统一介绍。
变量的基本操作:
操作 | 示例 | 说明 |
---|---|---|
赋值 | x=10 |
定义变量 |
取值 | echo $x |
读取变量 |
删除变量 | unset x |
删除变量 |
只读变量 | readonly x |
定义只读变量,不能再修改 |
示例:
bash
#!/bin/bash
myname="Alice"
readonly myname # 冻结变量
echo "Hello, $myname"
unset myname # 会报错:不能删除只读变量
shell脚本基本语法结构
1.特殊符号
$
$
符号用于 引用变量 、获取命令的输出 、指定参数 、执行进程替换 等多种用途。它是一个多用途的符号,非常常见且重要。
①引用变量的值
bash
my_var="Hello, World!"
echo $my_var
输出:Hello, World!
②获取命令的输出
bash
current_date=$(date)
echo "今天的日期是: $current_date"
输出:今天的日期是: Tue May 1 10:00:00 CST 2025
③获取位置参数
在 Shell 脚本中,$
用于引用 脚本的参数 。脚本在执行时可以接收外部传入的参数,这些参数通常通过 $1
, $2
, ..., $n
来引用,其中 1
表示第一个参数,2
表示第二个参数,以此类推。
bash
# 脚本执行时传入参数
# ./script.sh arg1 arg2 arg3
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "第三个参数: $3"
第一个参数: arg1
第二个参数: arg2
第三个参数: arg3
④获取所有位置参数
当你想要获取 所有参数 时,可以使用 $@
或 $*
。它们都表示所有的命令行参数,但有所区别:
-
$@
:将每个参数视为单独的字符串,适用于循环等操作; -
$*
:将所有参数视为一个整体的字符串。
bash
# 脚本执行时传入参数
# ./script.sh arg1 arg2 arg3
echo "所有参数 (\$@): $@"
echo "所有参数 (\$*): $*"
输出:
所有参数 ($@): arg1 arg2 arg3
所有参数 ($*): arg1 arg2 arg3
⑤获取脚本执行状态(退出状态)
$?
用于获取上一条命令的退出状态。每个命令在执行时都会返回一个退出状态(exit status),通常返回 0
表示成功,非 0
表示失败。
bash
mkdir new_dir
echo "上一条命令的退出状态: $?"
上一条命令的退出状态: 0
⑥获取后台进程PID
$!用于记录最近一次后台进程的PID
&
①后台运行命令,不阻塞当前shell
bash
#在后台运行command
command &
sleep 10 & # 后台休眠10秒,立即返回提示符,返回格式:[作业号] PID(如 [1] 12345)
②在子shell中并发执行多条命令
bash
{ command1 & command2 & }
{ sleep 3; echo "A"; } & { sleep 1; echo "B"; } &
输出(可能顺序):
B # 先完成的任务先输出
A
③重定向中的特殊用途
bash
command > file.txt 2>&1 #将stderr重定向到stdout输出的位置,即file.txt中
2>&1 表示将文件描述符 2(stderr)指向描述符 1(stdout)的当前位置。
此时执行command产生的stdout和stderr都被重定向写入到了file.txt中
④逻辑操作符与运算操作符
bash
[[ -f file.txt ]] & echo "检查文件" # 异步检查文件是否存在
echo $(( 5 & 3 )) # 输出 1(二进制 101 & 011 = 001)
注释#
Shell 中注释以 #
开头,整行或行尾注释,Shell 执行时会忽略这些内容。
bash
# 这是一个注释
name="Alice" # 设置用户名变量
换行\
每条命令默认使用换行分隔:
bash
echo "第一行"
echo "第二行"
输出结果:
第一行
第二行
也可以使用 \
实现 命令换行(续行:该行未结束,下一行是当前行的继续):
bash
echo "这是一条非常 \
长的命令"
输出结果:
这是一条非常 长的命令
转义\
当我们需要在字符串中输出一些特殊字符(例如空格、引号、美元符号等)时,可以使用 \
来转义它们。
bash
echo "He said, \"Hello!\""
输出:
bash
He said, "Hello!"
分号;
使用分号可以在同一行中执行多个命令:
bash
echo "hello"; echo "world"
等价于:
bash
echo "hello"
echo "world"
&&
||
&&
:前一个命令成功(返回值为 0)时才会执行后一个命令
||
:前一个命令失败(返回值非 0)时才会执行后一个命令
bash
# 如果命令成功,则执行第二条命令
mkdir new_dir && cd new_dir
# 如果命令失败,则执行第二条命令
mkdir new_dir || echo "目录创建失败"
2.输入输出
echo:输出内容到终端
bash
echo "Hello, World!"
echo -n "不换行输出"
echo -e "换行符:\n下一行"
参数 作用
-n 不换行
-e 启用转义字符(如 \n、\t)
read:从用户输入读取数据
bash
read name
#用户再命令行中输入abc
echo "你好,$name" #打印 你好,abc
带提示信息:
read -p "请输入你的名字:" name
echo "你好,$name"
多个变量:
read first last
echo "First: $first, Last: $last"
3.运算符
算术运算符
|-----|---|--------------|
| +
| 加 | $((3 + 2))
|
|-----|---|--------------|
| -
| 减 | $((5 - 1))
|
|-----|---|--------------|
| *
| 乘 | $((2 * 3))
|
|-----|---|---------------|
| /
| 除 | $((10 / 2))
|
|-----|----|--------------|
| %
| 取余 | $((7 % 4))
|
比较运算符(整数)
用于 test
或 [ ]
中
|-------|----|---------------------|
| -eq
| 相等 | [ "$a" -eq "$b" ]
|
|-------|----|---------------------|
| -ne
| 不等 | [ "$a" -ne "$b" ]
|
|-------|----|---------------------|
| -gt
| 大于 | [ "$a" -gt "$b" ]
|
|-------|----|---------------------|
| -lt
| 小于 | [ "$a" -lt "$b" ]
|
|-------|------|---------------------|
| -ge
| 大于等于 | [ "$a" -ge "$b" ]
|
|-------|------|---------------------|
| -le
| 小于等于 | [ "$a" -le "$b" ]
|
字符串比较运算符
|------------|-------|-------------------|
| =
或 ==
| 字符串相等 | [ "$a" = "$b" ]
|
|------|-------|--------------------|
| !=
| 字符串不等 | [ "$a" != "$b" ]
|
|------|----------|---------------|
| -z
| 字符串长度为 0 | [ -z "$a" ]
|
|------|-----------|---------------|
| -n
| 字符串长度不为 0 | [ -n "$a" ]
|
逻辑运算符
|-----|---|---------------------|
| !
| 非 | [ ! -f file.txt ]
|
|------|---|----------------------------|
| -a
| 与 | [ -f a.txt -a -r a.txt ]
|
|------|---|----------------------------------|
| -o
| 或 | [ "$a" -lt 0 -o "$b" -gt 100 ]
|
4. 条件判断结构
**[ ]:**条件测试命令test的简写
bash
[ 条件表达式 ]
注意:"[" 与 "条件" 与 "]"都要用空格分开
**[[ ]]:**加强版的[ ]
支持字符串比较
bash
name="admin"
# 传统写法
[ "$name" = "admin" ] && echo "匹配"
# Bash 写法
[[ $name == "admin" ]] && echo "匹配"
支持通配符
bash
name="Alice"
[[ $name == A* ]]&& echo "名字以 A 开头"
[[ $name == *ce ]] && echo "名字以 ce 结尾"
支持正则匹配
bash
email="[email protected]"
=~:使用正则表达式进行匹配
不需要加引号(加了反而当普通字符串处理)
[[ $email =~ ^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$ ]] && echo "这是合法邮箱"
支持多条件判断(支持|| &&)
bash
age=25
if [[ $age -gt 18 && $age -lt 65 ]]; then
echo "在工作年龄范围内"
fi
更好的鲁棒性:变量为空不报错
bash
name=""
# 传统写法可能出错
# [ $name = "admin" ] # 报错:一元运算符预期
# Bash 写法不会出错
[[ $name == "admin" ]] # 安全 ✅
if-then
bash
if [ condition ]; then
commands
fi
if-else
bash
if [ "$age" -ge 18 ]; then
echo "成年人"
else
echo "未成年人"
fi
if-elif-else
bash
if [ "$score" -ge 90 ]; then
echo "优秀"
elif [ "$score" -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi
test
命令
bash
test $a -gt 5 && echo "a > 5"
#等价于
[ $a -gt 5 ] && echo "a > 5"
shell脚本流程控制
1.if-else、elif语句
bash
if 条件; then
命令
elif 另一个条件; then
命令
else
命令
fi
bash
#!/bin/bash
read -p "请输入一个数字:" num
if [[ $num -gt 100 ]]; then
echo "大于100"
elif [[ $num -eq 100 ]]; then
echo "等于100"
else
echo "小于100"
fi
2 case 语句
bash
case $变量 in
模式1)
命令
;;
模式2)
命令
;;
*)
默认命令
;;
esac
bash
#!/bin/bash
read -p "请输入操作(start/stop/restart):" action
case $action in
start)
echo "服务已启动"
;;
stop)
echo "服务已停止"
;;
restart)
echo "服务已重启"
;;
*)
echo "未知命令"
;;
esac
3 for 循环
语法格式 1:遍历列表
bash
for 变量 in 列表; do
命令
done
bash
for name in Alice Bob Charlie; do
echo "你好,$name"
done
语法格式 2:数值循环(C 风格)
bash
for ((i=1; i<=5; i++)); do
echo "第 $i 次循环"
done
4 while 循环
bash
while 条件; do
命令
done
bash
count=1
while [[ $count -le 3 ]]; do
echo "循环第 $count 次"
((count++))
done
也可用于读取文件逐行:
bash
while read line; do
echo "$line"
done < 文件名.txt
5 break 与 continue
bash
for ((i=1; i<=5; i++)); do
if [[ $i -eq 3 ]]; then
continue # 跳过第3次
fi
if [[ $i -gt 4 ]]; then
break # 提前结束
fi
echo "第 $i 次"
done
shell脚本中的函数
1.函数的定义与调用
①函数的定义方式
bash
# 方式一:推荐使用
function 函数名() {
命令列表
}
# 方式二:兼容写法
函数名() {
命令列表
}
②函数的调用方式
调用方式非常简单,直接写函数名即可:
bash
say_hello() {
echo "Hello, Shell!"
}
say_hello # 调用无参函数
say_hello() {
echo "Hello $0!"
}
say_hello bob # 调用有参函数
注意:shell脚本是逐行运行,不会像其它语言一样先编译,所以必须在函数调用前声明函数
③函数体中的局部变量与全局变量
bash
foo() {
local name="Alice" # 局部变量,仅在函数内有效
echo "Hi, $name"
}
name="Bob"
foo
echo "外部变量 name = $name" # 不受 foo 中的修改影响
2.函数中的参数与返回值
①参数
Shell 函数接收参数的方式和脚本一样,用 $1
、$2
、$3
表示第 1、2、3 个参数:
bash
greet() {
echo "你好,$1!你今天感觉怎么样?"
}
greet "小明"
输出:
bash
你好,小明!你今天感觉怎么样?
②返回值
函数返回值方式一:用 echo
bash
get_name() {
echo "Shell脚本"
}
name=$(get_name)
echo "返回值是:$name"
函数返回值方式二:用 return
返回状态码(0~255)
bash
is_even() {
if (( $1 % 2 == 0 )); then
return 0
else
return 1
fi
}
is_even 8
if [[ $? -eq 0 ]]; then
echo "是偶数"
else
echo "是奇数"
fi
shell脚本进阶
1 数组与字符串处理
①数组(仅bash支持)
定义数组
bash
arr=(apple banana cherry)
访问数组元素
bash
echo ${arr[0]}
获取所有元素
bash
echo ${arr[@]}
获取数组长度
bash
echo ${#arr[@]}
获取数组中某一元素的长度
bash
echo ${#arr[0]}
遍历数组
bash
for item in "${arr[@]}"; do
echo "$item"
done
修改元素
bash
arr[1]="orange"
②字符串处理
字符串长度
bash
str="Hello Shell"
echo ${#str} #输出11
子串提取
bash
echo ${str:6:5} #从下标6开始向后截取5个长度的元素
查找并替换
bash
echo ${str/Shell/Bash} #将str中的Shell替换为Bash,输出Hello Bash
字符串拼接
bash
a="hello"
b="world"
c="$a $b"
echo $c
2 .输入输出重定向
①输出重定向
bash
echo "写入文件" > file.txt # 将"写入文件"覆盖写入到文件file.txt
echo "追加内容" >> file.txt # 将"追加内容"追加写入到文件file.txt
②输出重定向
bash
while read line; do #每次读取file.txt中一行数据,重定向输出到控制台
echo "$line"
done < file.txt
③多行输入
bash
cat <<EOF > file.txt #将多行数据一次性写入到file.txt中
这是多行内容
可以写很多行
EOF
④标准错误重定向
bash
#执行ls一个不存在的文件会产生一个标准错误,
#可以通过 2> error.log 将这个标准错误重定向到error.log文件中
#2指的就是stderr的文件描述符
ls not_exist_file 2> error.log
⑤将命令执行结果同时重定向给stdout和stderror
bash
#将command的运行结果stdout重定向到out.log中
#将command运行产生的标准错误重定向到error.log中
command > output.log 2> error.log
#将command的运行结果的标准输出和标准错误均重定向到all.log文件中
command > all.log 2>&1 #&1指的是获取文件描述符1即stdout的当前位置,即all.log
等价于
command > all.log 2> all.log
#将ping这条指令的标准输出stdout和标准错误均重定向到空设备(黑洞设备),丢弃所有正常输出
ping -c 1 www.baidu.com > /dev/null 2>&1
3.错误处理与调试
①退出码检查 $?
bash
cp file.txt /some/path/
if [ $? -ne 0 ]; then
echo "拷贝失败!"
fi
注意:$?存储的是最近一条指令执行的退出状态码
②set -e
:遇到错误立即退出脚本
bash
#!/bin/bash
set -e
cp file.txt /path/ # 若失败,则退出脚本
echo "这行不会执行"
③set -x
:打印执行过程(调试用)
自动打印 实际执行的每一行命令及其参数(展开变量后的真实命令)
bash
set -x
echo "当前用户: $USER"
#控制台输出
+ echo '当前用户: alice' # 这是set -x的调试输出
当前用户: alice # 这是echo的输出
4.使用 trap 捕捉信号
trap
是 Shell 脚本中用于 捕获并处理信号 的关键命令,它能让脚本在收到特定系统信号时执行自定义操作(比如清理资源、打印日志等),而不是直接终止。
在 Linux/Unix 系统中,信号是进程间通信的一种方式,用于通知进程发生了某些事件。常见信号:
-
INT
(SIGINT
,Ctrl+C
触发) -
TERM
(SIGTERM
,默认的kill
命令发送的信号) -
KILL
(SIGKILL
,强制终止,不可捕获) -
HUP
(SIGHUP
,终端挂断) -
EXIT
(非标准信号,脚本退出时触发)
trap
的基本语法
bash
trap '执行的命令' 信号列表
单信号:trap 'echo 收到信号' INT
多信号:trap 'echo 退出' INT TERM EXIT
忽略信号:trap '' INT(空字符串表示忽略)
恢复默认:trap - INT(取消自定义处理)
示例一:优雅处理终端
bash
#!/bin/bash
trap 'echo "捕获中断,正在退出..."; exit' INT
while true; do
echo "运行中..."
sleep 1
done
示例二:脚本退出时清理资源
bash
#!/bin/bash
tempfile="/tmp/mytemp.$$" # 临时文件
trap 'rm -f "$tempfile"; echo "清理完成"' EXIT
touch "$tempfile"
echo "脚本运行中..."
sleep 10
示例三:捕捉后执行函数
bash
#!/bin/bash
cleanup() {
echo "正在清理..."
rm -f *.tmp
exit
}
trap cleanup INT TERM
# 模拟任务
touch {1..3}.tmp
sleep 60
shell脚本实战
示例一:批量重命名文件
在一个目录中,所有图片文件命名格式杂乱无章,比如:IMG001.jpg
、img_02.JPG
、photo3.jpeg
,你希望将所有图片统一重命名为 image_001.jpg
、image_002.jpg
... 的格式,便于管理。
bash
#!/bin/bash
count=1
# 遍历所有支持的图片格式(包括大小写变体)
for file in *.jpg *.jpeg *.JPG *.JPEG; do
# 跳过无效匹配(当没有文件时会返回原始模式如"*.jpg")
[ -f "$file" ] || continue
# 生成新文件名(强制使用.jpg扩展名):
# %03d 表示3位数字(001, 002...)
new_name=$(printf "image_%03d.jpg" "$count")
# 执行重命名(原扩展名无论是什么,都改为.jpg)
mv -- "$file" "$new_name"
# 计数器增加
((count++))
done
示例二:Git 仓库自动部署系统
企业服务端项目部署,要求:
-
监听某个 Git 仓库的更新;
-
自动拉取
main
分支最新代码; -
构建项目并备份旧版;
bash
#!/bin/bash
# 基础配置
REPO_DIR="/var/www/myapp" # 监控的Git仓库目录
SERVICE_NAME="myapp" # 服务名称(需与systemd单元名一致)
LOG_FILE="/var/log/git_deploy.log" # 日志文件路径
# 进入Git仓库目录
cd "$REPO_DIR" || {
echo "$(date) - ERROR: 无法进入项目目录" >> "$LOG_FILE"
exit 1
}
# 获取当前和远程的main分支差异
git fetch origin main >> "$LOG_FILE" 2>&1
LOCAL_COMMIT=$(git rev-parse main)
REMOTE_COMMIT=$(git rev-parse origin/main)
# 如果没有更新则退出
[ "$LOCAL_COMMIT" = "$REMOTE_COMMIT" ] && exit 0
# 记录部署开始
echo "$(date) - 检测到新提交: $REMOTE_COMMIT" >> "$LOG_FILE"
# 备份当前可执行文件(假设编译产物在build目录)
[ -f build/myapp ] && cp build/myapp build/myapp.bak
# 拉取最新代码
if ! git reset --hard origin/main >> "$LOG_FILE" 2>&1; then
echo "$(date) - ERROR: Git拉取失败" >> "$LOG_FILE"
exit 2
fi
# 构建项目(假设使用CMake)
{
mkdir -p build
cd build || exit 1
cmake .. && make -j$(nproc)
} >> "$LOG_FILE" 2>&1
if [ $? -ne 0 ]; then
echo "$(date) - ERROR: 构建失败" >> "$LOG_FILE"
# 尝试恢复备份
[ -f build/myapp.bak ] && mv build/myapp.bak build/myapp
exit 3
fi
示例三:部署前自动检查依赖并构建 CMake + Make 项目
你维护一个使用 CMake
和 make
的 C++ 项目,部署前需要执行以下步骤:
-
检查是否安装了
g++
和cmake
; -
若
build/
目录不存在,则创建并进入; -
执行
cmake ..
和make
构建; -
所有输出统一写入日志文件
deploy.log
。
bash
#!/bin/bash
# 定义日志文件路径(当前目录下的deploy.log)
LOG_FILE="./deploy.log"
# 定义构建目录路径(当前目录下的build文件夹)
BUILD_DIR="./build"
# 在日志文件中记录部署开始时间(tee命令同时输出到终端和日志文件)
echo "----- DEPLOY STARTED: $(date) -----" >> "$LOG_FILE"
# 1. 检查必要的编译工具是否安装
# 检查cmake是否可用(command -v会返回命令路径,&>/dev/null丢弃所有输出)
if ! command -v cmake &>/dev/null; then
# 如果cmake不存在,输出错误信息(tee -a表示追加到日志文件)
echo "[ERROR] cmake is not installed!" | tee -a "$LOG_FILE"
exit 1 # 退出状态码1表示常规错误
fi
# 检查g++是否可用
if ! command -v g++ &>/dev/null; then
echo "[ERROR] g++ is not installed!" | tee -a "$LOG_FILE"
exit 1
fi
# 2. 准备构建目录
# 检查构建目录是否存在(-d测试目录是否存在)
if [ ! -d "$BUILD_DIR" ]; then
echo "[INFO] Creating build directory..." | tee -a "$LOG_FILE"
mkdir "$BUILD_DIR" # 创建构建目录
fi
# 进入构建目录,如果失败则报错(|| 表示前一个命令失败时执行后续命令)
# { } 命令组用于组合多个命令,必须用分号结尾
cd "$BUILD_DIR" || {
echo "[ERROR] Cannot enter build directory!" | tee -a "$LOG_FILE"
exit 1
}
# 3. 运行CMake配置项目
echo "[INFO] Running cmake..." | tee -a "$LOG_FILE"
# 执行cmake并将标准输出和错误输出都重定向到日志文件(2>&1)
cmake .. >> "$LOG_FILE" 2>&1
# 检查上一条命令的退出状态($?)
if [ $? -ne 0 ]; then # -ne表示不等于0(非成功状态)
echo "[ERROR] cmake failed!" | tee -a "$LOG_FILE"
exit 2 # 使用不同的退出码便于识别错误阶段
fi
# 4. 使用Make编译项目
echo "[INFO] Building project with make..." | tee -a "$LOG_FILE"
# -j$(nproc) 使用所有CPU核心并行编译,nproc获取CPU数量
make -j$(nproc) >> "$LOG_FILE" 2>&1
if [ $? -eq 0 ]; then # -eq表示等于0(成功状态)
echo "[SUCCESS] Build completed successfully!" | tee -a "$LOG_FILE"
else
echo "[ERROR] Build failed!" | tee -a "$LOG_FILE"
exit 3 # 不同的退出码表示不同阶段的错误
fi
# 在日志文件中记录部署结束时间
echo "----- DEPLOY ENDED: $(date) -----" >> "$LOG_FILE"