Shell 脚本编程与版本控制实战指南
环境假设:CentOS 7/8 或 RHEL 系列 Linux
目录
第一部分:Shell 脚本编程
1. Shell 脚本介绍
什么是 Shell?
类比:如果把操作系统想象成一家餐厅,那么 Shell 就是你和服务员之间的"对话窗口"。你告诉服务员你想吃什么(输入命令),服务员把需求传给后厨(系统内核),然后把做好的菜端给你(返回结果)。
Shell 是一个命令行解释器,它接收用户输入的命令,解析后交给系统内核去执行。Linux 中常见的 Shell 有:
| Shell 类型 | 路径 | 说明 |
|---|---|---|
| bash | /bin/bash | 最常用,功能丰富,大多数 Linux 默认 Shell |
| sh | /bin/sh | bash 的简化版,兼容性好 |
| zsh | /bin/zsh | macOS 默认,补全功能强 |
| nologin | /sbin/nologin | 禁止登录的伪 Shell,用于系统账户 |
sh 和 bash 的区别:sh 是 bash 的一个子集。bash 提供了更多高级特性(如数组、[[ ]] 双括号判断等)。日常工作中我们几乎都用 bash。
什么是 Shell 脚本?
Shell 脚本(Shell Script)就是把一系列命令写在一个文本文件里,让系统一次性按顺序执行。类似于 Windows 下的批处理文件(.bat),但功能强大得多。
类比:如果每条命令是一道数学题的解题步骤,那么 Shell 脚本就是一份完整的"解题过程"——你写好步骤,机器自动执行,不用再一步一步手动输入。
第一个 Shell 脚本
问题场景:每次做系统巡检都要依次执行 date、free -m、df -Th,很烦。能不能一个脚本搞定?
#!/bin/bash
# check.sh - 系统巡检脚本
echo "=== 系统巡检报告 ==="
echo "当前时间:"
date
echo ""
echo "内存使用情况:"
free -m
echo ""
echo "磁盘使用情况:"
df -Th
执行方式:
chmod +x check.sh
./check.sh
输出示例:
=== 系统巡检报告 ===
当前时间:
2024年 03月 15日 星期五 14:30:22 CST
内存使用情况:
total used free shared buff/cache available
Mem: 3932 1205 1832 56 894 2440
磁盘使用情况:
Filesystem Type Size Used Avail Use% Mounted on
/dev/sda2 xfs 50G 12G 39G 24% /
想一想:为什么脚本第一行要写 #!/bin/bash?
这一行叫做 shebang,它告诉系统"用哪个程序来执行这个脚本"。就像信封上写收件人地址一样,
#!/bin/bash告诉操作系统"请用 bash 来运行我"。
2. 脚本基础知识及变量
脚本执行方式
| 执行方式 | 命令 | 特点 |
|---|---|---|
| 作为程序执行 | ./script.sh | 需要 chmod +x 权限,产生子进程 |
| 用 bash 执行 | bash script.sh | 不需要执行权限,产生子进程 |
| 在当前 Shell 执行 | source script.sh 或 . script.sh | 不产生子进程,变量修改影响当前环境 |
思考题:脚本里执行了
cd /tmp,用./script.sh执行后,当前目录变了吗?用source script.sh呢?
答案:./script.sh不会影响当前 Shell(子进程里 cd,父进程不变);source script.sh会改变当前目录(在同一进程中执行)。
变量体系
四类变量
1. 环境变量——系统预设好的"全局配置"
# 查看当前所有环境变量
env
# 常用环境变量
echo $PATH # 命令搜索路径
echo $HOME # 当前用户家目录
echo $USER # 当前用户名
echo $HOSTNAME # 主机名
echo $PWD # 当前工作目录
echo $UID # 当前用户ID
echo $PS1 # 命令提示符格式
2. 预定义变量——和进程相关的特殊变量
| 变量 | 含义 | 类比 |
|---|---|---|
$0 | 当前脚本名称 | 相当于"这份卷子的标题" |
$$ | 当前进程 PID | 相当于"这份卷子的编号" |
$# | 位置参数个数 | 相当于"卷子后面附了几道题" |
$* / $@ | 所有位置参数 | 相当于"所有题目的内容" |
$? | 上一条命令的返回值 | 0 = 成功,非 0 = 失败 |
3. 位置变量——脚本后面跟的参数 $1 $2 ... $9
# calc.sh
#!/bin/bash
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "运算结果: $(($1 + $2))"
$ ./calc.sh 10 20
第一个参数: 10
第二个参数: 20
运算结果: 30
4. 自定义变量——用户自己定义的变量
# 定义变量(注意:等号两边不能有空格!)
name="张三"
age=18
# 引用变量用 $
echo "姓名: $name, 年龄: $age"
# 变量运算
a=10
b=3
echo $((a + b)) # 13 算术运算推荐 $(( ))
echo $[a * b] # 30 $[ ] 也可以
echo $(expr $a + $b) # 13 expr 方式(注意运算符两边要有空格)
echo $(expr $a \* $b) # 30 expr 中 * 要转义
命令替换(将命令输出赋给变量)
# 推荐用 $(),可嵌套
today=$(date +%F)
echo "今天是: $today"
# 嵌套示例(反引号 `` 无法嵌套,所以推荐 $())
file=$(ls $(date +%F))
变量的作用域
export MY_VAR="hello" # 环境变量:子进程可见
LOCAL_VAR="world" # 局部变量:仅当前 Shell 可见
类比:
export就像广播通知——全校(所有子进程)都能听到;不加export就像私下告诉同桌——只有当前 Shell 知道。
配置文件加载顺序
用户登录时:
/etc/profile → ~/.bash_profile → ~/.bashrc → /etc/bashrc
打开新终端时:
~/.bashrc → /etc/bashrc
.bash_profile:登录级配置(su - user触发).bashrc:Shell 级配置(su user或新开终端触发)
引号的区别
name="robin"
echo "hello $name" # hello robin (双引号内变量会被替换)
echo 'hello $name' # hello $name (单引号内变量不会被替换,所见即所得)
echo hello\ world # hello world (反斜杠转义空格)
思考题:要输出一句 I have $100,应该怎么写 echo 命令?
echo 'I have $100' # 方法一:用单引号
echo "I have \$100" # 方法二:用反斜杠转义 $
算术运算汇总
a=10; b=3
echo $((a + b)) # 13 —— 推荐
echo $[a + b] # 13
expr $a + $b # 13 —— 注意空格
let c=a+b; echo $c # 13
echo "scale=2;$a/$b" | bc # 3.33 —— bc 支持小数
3. 常用命令速查
echo 与 printf
# echo 常用选项
echo -n "不换行输出"
echo -e "支持转义\t制表符\n换行"
# 带颜色输出
echo -e "\033[31m红色文字\033[0m"
echo -e "\033[32m绿色文字\033[0m"
echo -e "\033[43;37m黄底白字\033[0m"
# printf 格式化输出(类似 C 语言的 printf)
printf "%-15s %-10d %s\n" "张三" 95 "优秀"
printf "%-15s %-10d %s\n" "李四" 78 "良好"
# 输出:
# 张三 95 优秀
# 李四 78 良好
read 命令
# 基本读取
read -p "请输入你的名字: " username
echo "你好, $username"
# 设置超时
read -p "请输入密码(5秒超时): " -t 5 password
# 隐藏输入(密码场景)
stty -echo
read -p "密码: " pass
stty echo
echo ""
管道与重定向
类比:管道
|就像工厂里的流水线——上一道工序的产品(输出)直接进入下一道工序(输入)。
# 管道:前一个命令的输出 = 后一个命令的输入
ps aux | grep nginx
cat /etc/passwd | sort -t: -k3 -n | tail -5
# 重定向
ls > output.txt # 标准输出覆盖写入文件
ls >> output.txt # 标准输出追加写入文件
ls 2> error.txt # 错误输出写入文件
ls &> all.txt # 正确+错误都写入文件
ls > out.txt 2>&1 # 等同于 &>
# 输入重定向
wc -l < /etc/passwd # 读取文件内容作为输入
# Here Document(在脚本中嵌入多行输入)
cat > /tmp/config.txt << EOF
server_name=web01
port=8080
log_level=info
EOF
# Here String
cat <<< "单行字符串输入"
注意:
echo 123 | read a之后$a是空的!因为管道会产生子进程,read在子进程中赋值不影响父 Shell。
文本处理命令速查
# cut - 截取字段
cut -d: -f1,3 /etc/passwd # 取第1、3列
cut -c 1-5 /etc/passwd # 取每行第1-5个字符
# sort - 排序
sort -n file.txt # 按数值排序
sort -t: -k3 -n /etc/passwd # 按第3列数值排序
sort -u file.txt # 去重
# uniq - 去重(仅处理连续重复行,通常先 sort)
sort file.txt | uniq -c # 统计重复次数
sort file.txt | uniq -d # 只显示重复行
# wc - 统计
wc -l /etc/passwd # 行数
wc -w file.txt # 单词数
wc -c file.txt # 字节数
# tr - 字符替换/删除
echo "hello" | tr 'a-z' 'A-Z' # HELLO(大小写转换)
echo "hellooo" | tr -s 'o' # helo(压缩重复字符)
# xargs - 将标准输入转为命令行参数
find / -name "*.log" | xargs rm -f
echo "a b c" | xargs mkdir # 创建 a b c 三个目录
4. test 命令与判断语法
test 命令概述
test 用于条件判断,等价于 [ ]。返回值为 0 表示真,非 0 表示假。
类比:test 命令就像数学中的"命题判断"——
[ 5 -gt 3 ]就像问"5 大于 3 吗?",答案为真(返回 0)。
文件判断
[ -f /etc/passwd ] # 文件存在且是普通文件
[ -d /etc ] # 文件存在且是目录
[ -e /etc/hosts ] # 文件存在(不关心类型)
[ -r file.txt ] # 文件可读
[ -w file.txt ] # 文件可写
[ -x script.sh ] # 文件可执行
[ -h file ] # 是否为符号链接
[ -s file ] # 文件存在且大小非零
[ file1 -ef file2 ] # 两个文件是否为硬链接(相同 inode)
整数比较
[ $a -eq $b ] # 等于 (equal)
[ $a -ne $b ] # 不等于 (not equal)
[ $a -gt $b ] # 大于 (greater than)
[ $a -ge $b ] # 大于等于
[ $a -lt $b ] # 小于 (less than)
[ $a -le $b ] # 小于等于
记忆口诀:
eq(等)、ne(不等)、gt(大于)、lt(小于)、ge(大于等于)、le(小于等于)——都是英文缩写。
字符串比较
[ "$str1" = "$str2" ] # 两个字符串相等
[ "$str1" != "$str2" ] # 不相等
[ -n "$str" ] # 字符串长度非零(有内容)
[ -z "$str" ] # 字符串长度为零(空字符串)
陷阱提醒:字符串比较时务必加引号!
[ $var = "hello" ]如果$var为空,会变成[ = "hello" ],直接报错。正确写法是[ "$var" = "hello" ]。
[[ ]] 双括号(bash 扩展)
[[ "$name" == z* ]] # 支持模式匹配(通配符)
[[ "$name" =~ ^[a-z]+[0-9]$ ]] # 支持正则表达式
[[ $a -gt 3 && $b -lt 10 ]] # 支持 && 和 || 逻辑运算
if 语句
#!/bin/bash
# 判断用户是否存在
read -p "输入用户名: " username
if id "$username" &> /dev/null
then
echo "用户 $username 存在"
grep "$username" /etc/passwd | cut -d: -f1,3,4,6,7
elif [ -z "$username" ]
then
echo "你什么都没输入!"
else
echo "用户 $username 不存在"
fi
实战脚本:猜数字游戏
#!/bin/bash
target=$RANDOM
# 限制范围在 1-100
target=$((target % 100 + 1))
for i in $(seq 1 5)
do
read -p "请输入数字(1-100),你还剩$((6-i))次机会: " num
if [ "$num" -eq "$target" ]; then
echo "恭喜你,猜对了!"
exit 0
elif [ "$num" -gt "$target" ]; then
echo "太大了!"
else
echo "太小了!"
fi
done
echo "5次机会用完了,正确答案是: $target"
思考题:如果要判断一个目录是否为空目录,应该怎么做?
提示:[ -d "$dir" ] && [ $(ls -A "$dir" | wc -l) -eq 0 ]
5. 循环语法
for 循环
# 列表遍历
for fruit in apple banana orange
do
echo "水果: $fruit"
done
# 从文件读取
for user in $(cat userlist.txt)
do
echo "处理用户: $user"
done
# C 语言风格
for ((i=1; i<=10; i++))
do
echo "第 $i 次"
done
# 实用场景:批量创建用户
for i in $(seq 1 10)
do
useradd "student$i"
echo "123456" | passwd --stdin "student$i" &> /dev/null
echo "已创建: student$i"
done
while 循环
# 条件为真时循环
count=0
while [ $count -lt 5 ]
do
echo "count = $count"
count=$((count + 1))
done
# 死循环(守护进程常用)
while true
do
# 检查服务状态
if ! systemctl is-active --quiet nginx; then
systemctl start nginx
echo "$(date): nginx 重启" >> /var/log/nginx-watch.log
fi
sleep 10
done
until 循环
until 和 while 相反——条件为假时循环。
x=1
until [ $x -gt 10 ]
do
echo $x
x=$((x + 1))
done
# 输出 1 到 10
break 与 continue
# break:跳出整个循环
for i in $(seq 1 100)
do
if [ $i -eq 5 ]; then
echo "遇到5,退出!"
break
fi
echo $i
done
# 输出 1 2 3 4
# continue:跳过本次,继续下一次
for i in $(seq 1 20)
do
if [ $((i % 7)) -eq 0 ] || echo $i | grep -q '7'; then
echo "跳过: $i"
continue
fi
echo "数字: $i"
done
# 7、14、17 等被跳过
实用示例:累加求和
#!/bin/bash
# 计算 1-100 的和(想想高斯怎么算的?)
sum=0
for i in $(seq 1 100)
do
sum=$((sum + i))
done
echo "1到100的和: $sum" # 5050
# 更高效的方式:用等差数列公式
# sum = n*(n+1)/2
n=100
echo "高斯公式: $((n * (n + 1) / 2))" # 5050
思考题:如何计算 1 到 100 中所有偶数的和?
提示:seq 2 2 100或for ((i=2; i<=100; i+=2))
6. case 分支语法与函数
case 语法
类比:case 就像自动售货机上的按钮——你按"A1"出来可乐,按"B2"出来雪碧,按其他按钮提示"无此商品"。
#!/bin/bash
# 模拟服务控制脚本
case $1 in
start)
echo "启动服务..."
systemctl start nginx
;;
stop)
echo "停止服务..."
systemctl stop nginx
;;
restart|reload)
echo "重启服务..."
systemctl restart nginx
;;
status)
systemctl status nginx
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
;;
esac
函数
#!/bin/bash
# 定义函数
sum() {
local result=$(($1 + $2))
echo $result
}
# 调用函数
total=$(sum 10 20)
echo "结果是: $total" # 结果是: 30
# 带返回状态的函数
check_user() {
if id "$1" &> /dev/null; then
return 0 # 成功
else
return 1 # 失败
fi
}
check_user "root"
if [ $? -eq 0 ]; then
echo "用户存在"
fi
函数变量作用域:
func() {
local a=100 # 局部变量,函数外不可见
b=200 # 全局变量,函数外可见
echo "函数内: a=$a, b=$b"
}
func
echo "函数外: b=$b" # 可以访问
echo "函数外: a=$a" # 空(不可访问)
思考题:
:(){ :|:& };:这个命令做了什么?(警告:千万不要在真实服务器上执行!)
答案:这是一个"fork 炸弹"——函数名叫:,它递归地调用自己并放到后台,迅速耗尽系统资源。
7. 变量替换与数组
参数扩展(Parameter Expansion)
# 默认值替换
unset name
echo "${name:-默认用户}" # 输出:默认用户(name为空时使用默认值,name本身不变)
# 默认值并赋值
echo "${name:=默认用户}" # 输出:默认用户(同时给name赋值)
echo $name # 默认用户
# 已设置时替换
name="张三"
echo "${name:+已设置}" # 输出:已设置(name有值时返回指定值)
# 未设置时报错
unset critical_var
# echo "${critical_var:?变量未设置!}" # 会输出错误信息
字符串操作
a="Hello, World! Hello, Linux!"
# 字符串长度
echo ${#a} # 28
# 子串截取(从第几个字符开始,取几个)
echo ${a:7} # World! Hello, Linux!
echo ${a:7:5} # World
# 最短前缀删除 #
echo ${a#Hello} # , World! Hello, Linux!
# 最长前缀删除 ##
echo ${a##Hello} # , Linux!
# 最短后缀删除 %
echo ${a%Linux!} # Hello, World! Hello,
# 最长后缀删除 %%
echo ${a%%Hello*} # (空,因为从头开始匹配)
# 第一次匹配替换
echo ${a/Hello/Hi} # Hi, World! Hello, Linux!
# 全局替换
echo ${a//Hello/Hi} # Hi, World! Hi, Linux!
数组
# 定义数组
fruits=("apple" "banana" "cherry" "date")
# 取值
echo ${fruits[0]} # apple
echo ${fruits[2]} # cherry
# 取所有值
echo ${fruits[@]} # apple banana cherry date
echo ${fruits[*]} # apple banana cherry date
# 数组长度
echo ${#fruits[@]} # 4(元素个数)
echo ${#fruits[1]} # 6(banana 的字符长度)
# 数组切片
echo ${fruits[@]:1:2} # banana cherry(从索引1开始取2个)
# 遍历数组
for fruit in ${fruits[@]}; do
echo "水果: $fruit"
done
# 添加元素
fruits+=("elderberry")
echo ${#fruits[@]} # 5
# 删除元素
unset fruits[2] # 删除 cherry
关联数组(类似字典/Map)
declare -A scores
scores=([张三]=95 [李四]=88 [王五]=72)
echo ${scores[张三]} # 95
# 遍历
for name in ${!scores[@]}; do
echo "$name: ${scores[$name]}"
done
# 输出:
# 张三: 95
# 李四: 88
# 王五: 72
思考题:用关联数组实现一个"石头剪刀布"游戏,怎么设计?
提示:declare -A win; win=([石头]=剪刀 [剪刀]=布 [布]=石头)
8. 正则表达式
类比:正则表达式就像数学中的"通项公式"——用一个公式描述一类规律,而不是列举所有情况。比如
a_n = 2n描述了所有偶数,正则中的[0-9]+描述了所有正整数。
基础正则表达式(BRE)元字符
| 元字符 | 含义 | 示例 |
|---|---|---|
. | 匹配任意单个字符(除换行符) | a.c 匹配 abc, adc, a2c |
* | 前导字符出现 0 次或多次 | ab*c 匹配 ac, abc, abbc |
^ | 匹配行首 | ^root 匹配以 root 开头的行 |
$ | 匹配行尾 | bash$ 匹配以 bash 结尾的行 |
[...] | 匹配括号内任一字符 | [aeiou] 匹配元音字母 |
[^...] | 否定匹配 | [^0-9] 匹配非数字 |
\ | 转义字符 | \. 匹配真正的点号 |
扩展正则表达式(ERE,egrep / grep -E)
| 元字符 | 含义 | 示例 |
|---|---|---|
+ | 前导字符出现 1 次或多次 | ab+ 匹配 ab, abb,不匹配 a |
? | 前导字符出现 0 次或 1 次 | colou?r 匹配 color 和 colour |
| | 或 | cat|dog 匹配 cat 或 dog |
() | 分组 | compan(y|ies) |
{n,m} | 匹配 n 到 m 次 | a{3,5} 匹配 aaa, aaaa, aaaaa |
{n} | 精确匹配 n 次 | [0-9]{4} 匹配4位数字 |
常用字符类
[0-9] # 数字
[a-z] # 小写字母
[A-Z] # 大写字母
[a-zA-Z] # 所有字母
[^0-9] # 非数字
grep 实战
# 基本搜索
grep "root" /etc/passwd # 包含 root 的行
grep -i "error" /var/log/messages # 忽略大小写
grep -n "root" /etc/passwd # 显示行号
grep -c "root" /etc/passwd # 统计匹配行数
grep -v "nologin" /etc/passwd # 取反(排除)
grep -r "config" /etc/ # 递归搜索目录
grep -rl "config" /etc/ # 只显示文件名
# 上下文显示
grep -A 2 "error" log.txt # 匹配行及后面2行
grep -B 1 "error" log.txt # 匹配行及前面1行
grep -C 2 "error" log.txt # 前后各2行
# 扩展正则
grep -E "^(root|admin)" /etc/passwd # 以root或admin开头
grep -xE "[a-zA-Z]{16}" /usr/share/dict/words # 刚好16个字母的单词
# 实战:提取时间范围的日志
egrep "(08:[4-5][0-9]|09:[0-1][0-9]|09:20)" /var/log/messages
# 实战:匹配手机号(1开头,第二位3-9,共11位)
grep -xE '1[3-9][0-9]{9}' phone.txt
# 匹配 IP 地址
grep -xE '([0-9]{1,3}\.){3}[0-9]{1,3}' ip.txt
想一想:grep "a.*c" file.txt 和 grep "a.c" file.txt 有什么区别?
a.c匹配恰好3个字符(a + 任意1个 + c),如 abc, a2c
a.*c匹配 a 开头 c 结尾、中间任意长度,如 ac, abc, a123456c(*有贪婪性,会尽可能多地匹配)
9. sed 流编辑器
类比:如果 grep 是"从一堆作业本中挑出符合条件的本子",那么 sed 就是"自动批改机器"——它可以查找、替换、删除、插入文本内容,而且不用打开文件手动编辑。
sed 基本语法
sed '命令' 文件 # 结果显示在屏幕上,原文件不变
sed -i '命令' 文件 # 直接修改原文件(慎用!)
sed -n '命令' 文件 # 只输出匹配/处理过的行(不自动打印)
sed -r '命令' 文件 # 使用扩展正则(省去很多反斜杠)
sed -e '命令1' -e '命令2' # 执行多条命令
sed -f script.sed 文件 # 从脚本文件读取命令
替换命令 s
# 基本替换(只替换每行第一个匹配)
sed 's/old/new/' file.txt
# 全局替换(替换每行所有匹配)
sed 's/old/new/g' file.txt
# 替换第2次出现的匹配
sed 's/old/new/2' file.txt
# 使用不同分隔符(避免冲突)
sed 's@/usr/local@/opt@g' file.txt # 用 @ 作分隔符
sed 's;/usr/local;/opt;g' file.txt # 用 ; 作分隔符
# & 引用匹配内容
sed 's/linux/& redhat/' file.txt # linux → linux redhat
sed 's/[0-9]\+/【&】/g' file.txt # 数字加方括号
# \(\) 分组与回调
echo "first:second" | sed 's/\(.*\):\(.*\)/\2:\1/'
# 输出:second:first (交换冒号两边的内容)
# 交换前两个单词
echo "hello world linux" | sed -r 's/([a-Z]+)([^a-Z]+)([a-Z]+)/\3\2\1/'
# 输出:world hello linux
删除命令 d
sed '3d' file.txt # 删除第3行
sed '1,5d' file.txt # 删除第1到5行
sed '$d' file.txt # 删除最后一行
sed '/^$/d' file.txt # 删除空行
sed '/^ *$/d' file.txt # 删除空白行(含空格)
sed '/pattern/d' file.txt # 删除匹配行
sed '1,/^$/d' file.txt # 从第1行删到第一个空行
sed '5,$d' file.txt # 从第5行删到文件末尾
插入、追加、更改
sed '/pattern/a 追加的文本' file.txt # 在匹配行后追加
sed '/pattern/i 插入的文本' file.txt # 在匹配行前插入
sed '/pattern/c 替换整行文本' file.txt # 替换整行
sed '3a 新行内容' file.txt # 在第3行后追加
地址范围(定址)
sed '5s/old/new/' file.txt # 只处理第5行
sed '2,8s/old/new/' file.txt # 处理第2到8行
sed '5,$s/old/new/' file.txt # 从第5行到末尾
sed '/start/,/end/s/old/new/' file.txt # 从匹配 start 到匹配 end 的行
sed '3!d' file.txt # 只保留第3行(! 取反)
打印与行号
sed -n '5,10p' file.txt # 只打印第5到10行
sed '=' file.txt # 显示行号
sed -n '/error/{=;p}' file.txt # 显示匹配行的行号和内容
高级用法:多行操作
# 每两行合并(用冒号连接)
sed 'N;s/\n/:/' file.txt
# 删除连续空行(只保留一个)
sed '/^$/{N;/^\n$/D}' file.txt
# 反转文件(类似 tac)
sed '1!G;h;$!d' file.txt
# 每行后加空行
sed 'G' file.txt
# 退出(加速大文件处理)
sed -n '1,100p' bigfile.txt # 慢:处理整个文件
sed '100q' bigfile.txt # 快:到第100行就退出
实战练习
# 1. 将日期格式 mm/dd/yy 转换为 mm:dd:yy
echo "03/15/24" | sed 's@/@:@g'
# 2. 删除每行第一个字符
sed -r 's/^.//' file.txt
# 3. 删除每行最后一个字符
sed -r 's/.$//' file.txt
# 4. 在包含 apple 的行前加三个星号
sed '/^apple/s/^/*** /' file.txt
# 5. 提取 IP 地址
ifconfig ens33 | sed -n 's/.*inet \([^ ]*\).*/\1/p'
# 6. 只保留文件前10行
sed '10q' file.txt > newfile.txt
思考题:
sed 's/ab*/x/' file.txt中,ab*匹配的是什么?
答案:匹配a后面跟着 0 个或多个b。所以a、ab、abb、abbb都会被匹配并替换为x。
10. awk 的使用
类比:如果说 sed 是"文本的外科医生"(精准修改),那么 awk 就是"文本的会计师"——它擅长把文本拆成一个个"字段"(列),然后进行统计、计算、生成报表。awk 本质上是一门完整的编程语言。
awk 基本语法
awk '模式 { 动作 }' 文件
- 模式:匹配条件(正则、比较、逻辑运算)
- 动作:匹配后执行的操作
字段与记录
# 默认以空格/制表符为分隔符
echo "张三 95 88 76" | awk '{print $1, $3}'
# 输出:张三 88
# -F 指定分隔符
awk -F: '{print $1, $3}' /etc/passwd # 打印用户名和UID
awk -F: '{print $1 "\t" $3}' /etc/passwd # 用制表符分隔输出
# $0 表示整行
awk '{print $0}' file.txt # 等同于 cat
# NF 表示当前行的字段数
awk -F: '{print $1, "字段数:", NF}' /etc/passwd
# $NF 表示最后一个字段
awk -F: '{print $NF}' /etc/passwd # 打印每行最后一列
BEGIN 和 END
# BEGIN:处理文件之前执行(常用于打印表头)
# END:处理完所有行后执行(常用于打印汇总)
awk -F: '
BEGIN { print "用户名\t\tUID\t\tShell" }
{ print $1 "\t\t" $3 "\t\t" $7 }
END { print "总计: " NR " 个用户" }
' /etc/passwd
输出示例:
用户名 UID Shell
root 0 /bin/bash
bin 1 /sbin/nologin
...
总计: 42 个用户
模式匹配
# 正则匹配
awk -F: '/root/' /etc/passwd # 包含 root 的行
awk -F: '$1 ~ /root/' /etc/passwd # 第1字段匹配 root
awk -F: '$1 !~ /nologin/' /etc/passwd # 第1字段不包含 nologin
# 比较运算
awk -F: '$3 >= 1000 {print $1, $3}' /etc/passwd # UID >= 1000 的用户
awk -F: '$3 == 0 {print $1}' /etc/passwd # UID 为 0 的用户
# 逻辑运算
awk -F: '$3>=30 && $3<=40 {print $1,$3}' /etc/passwd
awk -F: '$1~/root/ || NR>40 {print}' /etc/passwd
# NR 行号匹配
awk -F: 'NR>=5 && NR<=10 {print NR, $1}' /etc/passwd # 第5-10行
awk -F: 'NR%2==1 {print}' /etc/passwd # 奇数行
内置变量
| 变量 | 说明 |
|---|---|
$n | 第 n 个字段 |
$0 | 整行记录 |
NF | 当前记录的字段数 |
NR | 当前记录号(累计行号) |
FNR | 当前文件的行号(多文件时重置) |
FS | 输入字段分隔符(等同 -F) |
OFS | 输出字段分隔符(默认空格) |
RS | 输入记录分隔符(默认换行) |
ORS | 输出记录分隔符(默认换行) |
FILENAME | 当前文件名 |
ENVIRON | 环境变量数组 |
# 设置输出分隔符
awk 'BEGIN{FS=":"; OFS="-"} {print $1, $3}' /etc/passwd | head -3
# 输出:root-0
# bin-1
# daemon-2
流程控制
# if-else
awk -F: '{
if ($3 == 0)
print $1, $3, "管理员"
else if ($3 < 1000)
print $1, $3, "系统用户"
else
print $1, $3, "普通用户"
}' /etc/passwd
# for 循环
awk 'BEGIN {
for (i=1; i<=9; i++) {
for (j=1; j<=i; j++)
printf "%d*%d=%-4d", j, i, j*i
print ""
}
}'
# 输出九九乘法表
# while 循环
awk -F: '{
i = 1
while (i <= NF) {
printf "字段%d: %s\n", i, $i
i++
}
}' /etc/passwd | head -7
数组与统计
# 统计每个 IP 的访问次数(Apache 日志)
awk '{ip[$1]++} END {for (i in ip) print i, ip[i]}' /var/log/httpd/access_log | sort -k2 -nr
# 统计 UID 总和
awk -F: '{sum += $3} END {print "UID总和:", sum}' /etc/passwd
# 计算进程内存使用
ps aux | awk 'NR>1 {vsz+=$5; rss+=$6} END {
printf "VSZ总计: %.1fM\n", vsz/1024
printf "RSS总计: %.1fM\n", rss/1024
}'
内置函数
# 字符串函数
awk 'BEGIN {
s = "Hello World"
print length(s) # 11
print substr(s, 7, 5) # World
print index(s, "World") # 7
gsub("World", "Linux", s) # 全局替换
print s # Hello Linux
}'
# split 函数:按分隔符拆分
awk 'BEGIN {
n = split("2024-03-15", date, "-")
print "年:" date[1], "月:" date[2], "日:" date[3]
}'
# 数学函数
awk 'BEGIN {
print sqrt(144) # 12
print int(3.7) # 3
srand()
print int(100 * rand()) # 随机数
}'
# system 函数:执行系统命令
awk 'BEGIN { system("date") }'
awk 引用 Shell 变量
search="root"
awk -v var="$search" -F: '$1 == var {print $0}' /etc/passwd
思考题:用 awk 如何提取
ifconfig输出中的 IP 地址?
答案:ifconfig ens33 | awk '/netmask/{print $2}'
11. SSH 远程操作与终端控制
SSH 密钥分发
问题:每次 SSH 到远程服务器都要输密码,管理 50 台服务器时非常痛苦。
解决方案:使用 SSH 密钥认证,实现免密登录。
# 1. 生成密钥对(一路回车即可)
ssh-keygen -t rsa -b 2048
# 生成 ~/.ssh/id_rsa(私钥)和 ~/.ssh/id_rsa.pub(公钥)
# 2. 将公钥复制到远程服务器
ssh-copy-id -i ~/.ssh/id_rsa.pub user@192.168.1.100
# 3. 测试免密登录
ssh user@192.168.1.100 # 不再需要密码
批量分发密钥(expect 自动化)
#!/bin/bash
# 批量向多台服务器分发 SSH 公钥
for ip in 192.168.1.{100..110}
do
/usr/bin/expect <<EOF
set timeout 10
spawn ssh-copy-id -i root@$ip
expect {
"*yes/no" { send "yes\r"; exp_continue }
"*password:" { send "your_password\r" }
}
expect eof
EOF
echo "$ip 密钥分发完成"
done
expect 关键字说明
| 关键字 | 说明 |
|---|---|
spawn | 启动一个子进程执行命令 |
expect | 等待特定输出出现 |
send | 发送输入(末尾要加 \r) |
interact | 执行完毕后保持交互状态 |
expect eof | 等待进程结束 |
exp_continue | 继续匹配下一个 expect |
tput 终端控制
#!/bin/bash
# 屏幕中央倒计时
col=$(tput cols)
line=$(tput lines)
center_col=$((col / 2))
center_line=$((line / 2))
tput sc # 保存光标位置
for i in $(seq 10 -1 1)
do
tput cup $center_line $center_col
echo -e "\033[31m$i\033[0m"
sleep 1
done
tput cup $center_line $center_col
echo "发射!"
tput rc # 恢复光标位置
第二部分:版本控制与 CI/CD
整体类比:
- SVN 就像"带历史版本的共享网盘"——所有人共享同一份文件,每次保存都会生成新版本
- Git 就像"文档的时间机器"——每次提交都是一个快照,你可以随时回到任何一个时间点
- GitHub 就像"代码版的社交网络"——你在上面展示项目、互相协作、贡献代码
- Jenkins 就像"自动化的流水线工人"——代码一更新,它自动编译、测试、部署
1. SVN 版本控制
什么是版本控制?
想象你在写一篇论文,保存了无数个版本:论文_v1.doc、论文_v2_修改.doc、论文_v3_最终版.doc、论文_v4_最终版_真的最终版.doc...
版本控制系统就是帮你自动管理这些版本的工具——它会记录每一次修改,谁改的,什么时候改的,改了什么。
SVN 服务器搭建
# 1. 安装
yum -y install subversion
# 2. 创建版本库目录
mkdir -p /var/svn/svnrepos
# 3. 创建版本库
svnadmin create /var/svn/svnrepos/myproject
# 4. 配置权限(编辑 /var/svn/svnrepos/myproject/conf/ 下的文件)
svnserve.conf 配置:
[general]
anon-access = read # 匿名用户只读
auth-access = write # 认证用户可写
password-db = passwd # 密码文件
# 注意:authz-db = authz 这行建议注释掉,避免认证失败
passwd 文件(添加用户):
zhangsan = 123456
lisi = 654321
authz 文件(设置权限):
[/]
zhangsan = rw
lisi = rw
# 5. 启动 SVN 服务(默认端口 3690)
svnserve -d -r /var/svn/svnrepos
# 6. 验证
netstat -ln | grep 3690
SVN 常用操作
# 检出(下载)代码到本地
svn checkout svn://192.168.1.100/myproject
# 简写:svn co
# 添加文件到版本库
svn add filename.txt
# 提交修改
svn commit -m "添加了新功能" filename.txt
# 简写:svn ci
# 更新到最新版本
svn update
# 简写:svn up
# 回退到指定版本
svn update -r 2
# 查看状态
svn status -v
# 简写:svn st
# 查看日志
svn log
# 查看差异
svn diff filename.txt # 工作副本与版本库的差异
svn diff -r 2:3 # 两个版本之间的差异
# 删除文件
svn delete filename.txt
svn commit -m "删除了不需要的文件"
# 创建目录
svn mkdir newdir
svn commit -m "创建新目录"
# 查看仓库信息
svn info
想一想:SVN 的版本号是什么形式的?
答案:SVN 使用全局递增的整数作为版本号(1, 2, 3...),每次提交版本号 +1,所有文件共享同一个版本号。这和 Git 的"哈希值"方式完全不同。
2. Git 基础
Git vs SVN
| 对比项 | SVN | Git |
|---|---|---|
| 架构 | 集中式(依赖中央服务器) | 分布式(每人都有完整仓库) |
| 版本号 | 递增整数 (1, 2, 3) | 哈希值 (a1b2c3d) |
| 离线操作 | 不支持 | 完全支持 |
| 分支 | 重量级(复制目录) | 轻量级(只是指针移动) |
| 速度 | 较慢(网络依赖) | 很快(本地操作) |
Git 安装与配置
# Linux 安装
yum install git # CentOS
apt install git # Ubuntu
# 配置身份(必须!每次提交都会用到)
git config --global user.name "张三"
git config --global user.email "zhangsan@example.com"
# 查看配置
git config --list
Git 三个工作区域
工作区(Working Directory)
↓ git add
暂存区(Staging Area / Index)
↓ git commit
版本库(Repository / .git)
类比:想象你在准备一封信——
- 工作区:你的书桌,正在编辑的文档
- 暂存区:信封里已经装好、准备寄出的信
- 版本库:邮局的存档,已经正式寄出并记录了
基本操作
# 创建仓库
mkdir myproject && cd myproject
git init
# 输出:Initialized empty Git repository in /path/to/myproject/.git/
# 查看状态(最常用的命令!)
git status
# 添加文件到暂存区
git add file.txt # 添加单个文件
git add . # 添加所有修改
# 提交到版本库
git commit -m "初始化项目"
# 查看提交历史
git log # 详细日志
git log --oneline # 简洁模式
git log --oneline --graph # 图形化显示分支
# 查看差异
git diff # 工作区 vs 暂存区
git diff --cached # 暂存区 vs 版本库
git diff HEAD # 工作区 vs 最新提交
# 修改文件后
vim file.txt
git add file.txt
git commit -m "修改了配置文件"
# 删除文件
git rm file.txt
git commit -m "删除了废弃文件"
# 查看某次提交的内容
git show a1b2c3d
撤销操作
# 撤销工作区的修改(回到暂存区状态)
git checkout -- file.txt
# 撤销暂存(从暂存区移回工作区)
git reset HEAD file.txt
# 回退到某个版本(保留工作区修改)
git reset --soft HEAD~1
# 回退到某个版本(丢弃所有修改!慎用!)
git reset --hard HEAD~1
思考题:
git reset --hard为什么危险?
因为它会同时重置暂存区和工作区,你未提交的修改会全部丢失,而且很难找回。
3. GitHub 使用
什么是 GitHub?
GitHub 是全球最大的代码托管平台,基于 Git 构建。你可以把它理解为"代码版的社交网络"——开发者在上面展示项目、互相协作、Review 代码。
连接 GitHub
# 方式一:HTTPS(需要用户名密码或 Token)
git clone https://github.com/username/project.git
# 方式二:SSH(推荐,配置一次后免密)
# 先添加 SSH Key 到 GitHub 账户
cat ~/.ssh/id_rsa.pub
# 复制输出内容,到 GitHub → Settings → SSH Keys 添加
git clone git@github.com:username/project.git
远程仓库操作
# 查看远程仓库
git remote -v
# 添加远程仓库
git remote add origin https://github.com/username/project.git
# 推送到远程
git push origin master # 推送到 master 分支
git push -u origin master # 首次推送并建立追踪关系
# 拉取远程更新
git pull origin master
# 克隆远程仓库
git clone https://github.com/username/project.git
免密推送配置
# 配置凭证存储
git config --global credential.helper store
# 第一次 push 时输入用户名密码,之后自动记住
# 或使用 SSH(更安全)
ssh-keygen -t rsa -C "your_email@example.com"
# 将公钥添加到 GitHub
4. Git 工作流程
分支(Branch)
类比:分支就像平行宇宙——你在"主宇宙"(master)正常工作,同时可以创建一个"平行宇宙"(feature 分支)去尝试新想法,如果成功了就"合并"回来,失败了也不影响主宇宙。
# 查看所有分支
git branch
# 创建分支
git branch feature-login
# 切换分支
git checkout feature-login
# 或者(创建并切换)
git checkout -b feature-login
# 合并分支(先切换到目标分支)
git checkout master
git merge feature-login
# 删除已合并的分支
git branch -d feature-login
分支管理策略
master (主分支,永远保持可发布状态)
│
├── develop (开发分支,集成所有功能)
│ │
│ ├── feature-login (功能分支)
│ ├── feature-payment (功能分支)
│ └── feature-search (功能分支)
│
└── hotfix-bug123 (紧急修复分支,直接从 master 创建)
合并冲突解决
什么时候会产生冲突? 当两个人修改了同一文件的同一行时,Git 不知道该听谁的,就会报冲突。
# 模拟冲突
# 在分支 A 修改 file.txt 第3行为 "Hello A"
# 在分支 B 修改 file.txt 第3行为 "Hello B"
# 合并时:
git merge feature-B
# Auto-merging file.txt
# CONFLICT (content): Merge conflict in file.txt
# Automatic merge failed; fix conflicts and then commit the result.
冲突文件内容会变成这样:
第一行没有修改
第二行没有修改
<<<<<<< HEAD
Hello A
=======
Hello B
>>>>>>> feature-B
第四行没有修改
手动解决冲突:
# 编辑文件,保留你想要的内容
vim file.txt
# 删除 <<<<<<< ======= >>>>>>> 标记,保留正确内容
# 标记为已解决
git add file.txt
git commit -m "解决合并冲突"
Git Flow 完整工作流示例
# 1. 从 develop 创建功能分支
git checkout develop
git checkout -b feature/user-profile
# 2. 开发功能,多次提交
git add .
git commit -m "添加用户头像上传"
git commit -m "完善个人资料页面"
# 3. 推送功能分支到远程
git push -u origin feature/user-profile
# 4. 在 GitHub 上创建 Pull Request,请同事 Review
# 5. Review 通过后,合并到 develop
git checkout develop
git merge feature/user-profile
# 6. 发布到 master
git checkout master
git merge develop
git tag v1.2.0 # 打标签
思考题:为什么不直接在 master 上开发,而要搞这么多分支?
答案:保护主分支的稳定性。master 永远是可以发布的状态,所有新功能在分支上开发和测试,确认没问题才合并。这就像考试时先在草稿纸上算,确认正确后再写到答题卡上。
5. Issues 与 Pull Request
Issues:问题追踪
场景:你使用了某开源项目,发现一个 Bug 或者有个好想法,但暂时不会改代码。
使用流程:
- 在项目页面点击 Issues → New Issue
- 填写标题和描述(描述越详细越好,最好附上截图和复现步骤)
- 项目维护者会看到通知,与你讨论
- 问题解决后关闭 Issue
好的 Issue 应该包含:
## Bug 描述
用户点击"保存"按钮后页面无响应
## 复现步骤
1. 登录系统
2. 进入"个人设置"页面
3. 修改邮箱
4. 点击"保存"
## 期望行为
保存成功并显示"修改成功"提示
## 实际行为
页面无任何反应,控制台报错 xxx
## 环境
浏览器:Chrome 120
操作系统:Windows 11
Pull Request(PR):代码贡献
场景:你修复了 Bug 或添加了功能,想把代码贡献给原项目。
完整流程:
# 1. Fork 原项目到自己的 GitHub(点 Fork 按钮)
# 2. Clone 自己的 Fork
git clone https://github.com/你的用户名/原项目.git
# 3. 创建功能分支
git checkout -b fix-login-bug
# 4. 修改代码并提交
vim src/login.js
git add src/login.js
git commit -m "修复登录验证逻辑"
# 5. 推送到你的 Fork
git push origin fix-login-bug
# 6. 在 GitHub 上创建 Pull Request
# 进入你的 Fork 页面 → Compare & pull request → 填写描述 → Create
Code Review(代码审查)
PR 创建后,项目维护者会:
- 查看你的代码修改
- 在特定行添加评论
- 要求你修改(你再 push 新提交即可自动更新 PR)
- 确认没问题后 Merge
想一想:为什么大公司都要求代码必须经过 Code Review 才能合并?
答案:(1) 发现潜在 Bug;(2) 知识共享,团队成员互相学习;(3) 保证代码质量和风格一致;(4) 形成文档记录。
6. GitHub Pages
什么是 GitHub Pages?
GitHub Pages 是 GitHub 提供的免费静态网站托管服务。你只需要把 HTML/CSS/JS 文件推送到特定仓库,GitHub 就会自动帮你发布成一个网站。
类比:就像学校公告栏——你把海报(HTML 文件)贴上去,所有人都能看到,不需要自己搭建服务器。
个人站点
# 1. 创建仓库,仓库名必须是:用户名.github.io
# 例如:zhangsan.github.io
# 2. 在仓库中创建 index.html
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>我的个人主页</title></head>
<body>
<h1>欢迎来到我的主页</h1>
<p>我是一名高中数学老师</p>
</body>
</html>
EOF
# 3. 提交并推送
git add index.html
git commit -m "创建个人主页"
git push origin master
# 4. 访问 https://zhangsan.github.io
项目站点
# 1. 进入项目仓库 → Settings → Pages
# 2. Source 选择 master branch
# 3. 点击 "choose a theme" 选择主题
# 4. 自动生成 README 和页面
# 5. 访问 https://用户名.github.io/仓库名
适用场景:个人博客、项目文档展示
7. Jenkins 持续集成
什么是 CI/CD?
| 术语 | 全称 | 含义 |
|---|---|---|
| CI | Continuous Integration(持续集成) | 开发者频繁地将代码合并到主干,自动编译和测试 |
| CD | Continuous Delivery(持续交付) | 在 CI 基础上,自动将代码部署到测试/生产环境 |
| CD | Continuous Deployment(持续部署) | 全自动:提交代码 → 编译 → 测试 → 部署 |
类比:传统方式就像"期末考试"——攒了一大堆代码最后一起测试,问题扎堆;CI/CD 就像"随堂测验"——每次提交一小部分,立刻验证,有问题马上发现。
构建工具演进
Make → Ant → Maven → Gradle → Jenkins Pipeline
(编译) (改进) (依赖管理)(更灵活) (全流程自动化)
Maven 是 Java 项目最常用的构建工具:
- 自动下载依赖包(不用再手动找 jar 包)
- 统一项目结构(src/main/java, src/test/java)
- 通过
pom.xml管理项目配置
Jenkins 安装与配置
# 1. 安装 JDK
yum install -y java-11-openjdk
# 2. 安装 Maven
wget https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.9.6/binaries/apache-maven-3.9.6-bin.tar.gz
tar -xzf apache-maven-3.9.6-bin.tar.gz -C /usr/local/
ln -s /usr/local/apache-maven-3.9.6 /usr/local/maven
# 配置环境变量
cat >> /etc/profile << 'EOF'
export MAVEN_HOME=/usr/local/maven
export PATH=$MAVEN_HOME/bin:$PATH
EOF
source /etc/profile
# 3. 安装 Jenkins
# 添加 Jenkins 仓库
wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat/jenkins.repo
rpm --import https://pkg.jenkins.io/redhat/jenkins.io-2023.key
yum install -y jenkins
# 4. 启动 Jenkins
systemctl start jenkins
systemctl enable jenkins
# 5. 访问 http://服务器IP:8080
# 首次登录密码在:/var/lib/jenkins/secrets/initialAdminPassword
Jenkins 工作流程
开发者 git push → GitHub 通知 Jenkins → Jenkins 执行:
1. 拉取最新代码(git pull)
2. Maven 编译(mvn clean package)
3. 运行单元测试(mvn test)
4. 生成构建报告
5. 部署到 Tomcat/Nginx
6. 发送通知(成功/失败)
Jenkins Pipeline(流水线)
类比:Pipeline 就像工厂的流水线——原材料(代码)从一端进入,经过一道道工序(编译→测试→打包→部署),最终产出成品(可运行的软件)。
Jenkinsfile 示例:
pipeline {
agent any
environment {
MAVEN_HOME = '/usr/local/maven'
}
stages {
stage('拉取代码') {
steps {
git branch: 'master',
url: 'https://github.com/username/project.git'
}
}
stage('编译构建') {
steps {
sh '/usr/local/maven/bin/mvn clean package -DskipTests'
}
}
stage('单元测试') {
steps {
sh '/usr/local/maven/bin/mvn test'
}
}
stage('部署') {
steps {
sh '''
cp target/*.war /usr/local/tomcat/webapps/
/usr/local/tomcat/bin/shutdown.sh
sleep 3
/usr/local/tomcat/bin/startup.sh
'''
}
}
}
post {
success {
echo '构建成功!'
// 可以发送邮件/钉钉/企业微信通知
}
failure {
echo '构建失败!请检查代码。'
}
}
}
配置定时构建
在 Jenkins Job 配置中设置 Build Triggers:
# 每天凌晨2点构建
H 2 * * *
# 每小时构建一次
H * * * *
# 工作日每天上午9点和下午6点构建
H 9,18 * * 1-5
完整 CI/CD 实战流程
# 1. 开发者修改代码
vim src/main/webapp/index.jsp
# 添加一行 <h2>新功能上线!</h2>
# 2. 提交到 Git
git add index.jsp
git commit -m "添加新功能提示"
git push origin master
# 3. Jenkins 自动触发(或手动触发)
# → 拉取代码
# → Maven 编译打包
# → 运行测试
# → 部署到 Tomcat
# → 发送通知
# 4. 测试人员打开浏览器验证
# http://server:8080/project/
思考题:如果没有 CI/CD,一个10人团队开发项目会遇到什么问题?
答案:(1) 手动编译容易出错;(2) 测试滞后,Bug 积累;(3) 部署流程不统一,"在我电脑上能跑";(4) 发布时间不可预测。CI/CD 解决了所有这些痛点。
综合实战:从零搭建一个完整的开发工作流
把前面学到的所有知识串起来:
1. Shell 脚本 → 自动化日常运维任务
2. SVN/Git → 管理代码版本
3. GitHub → 代码托管与团队协作
4. Jenkins → 自动化构建与部署
一键部署脚本示例
#!/bin/bash
# deploy.sh - 自动部署脚本
# 变量定义
PROJECT_DIR="/opt/webapp"
BACKUP_DIR="/opt/backup"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/deploy.log"
# 函数:日志记录
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 函数:备份当前版本
backup() {
log "开始备份..."
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/webapp_${DATE}.tar.gz" "$PROJECT_DIR"
log "备份完成: webapp_${DATE}.tar.gz"
}
# 函数:拉取最新代码
pull_code() {
log "拉取最新代码..."
cd "$PROJECT_DIR" || exit 1
git pull origin master
if [ $? -ne 0 ]; then
log "代码拉取失败!"
exit 1
fi
log "代码拉取成功"
}
# 函数:构建项目
build() {
log "开始构建..."
cd "$PROJECT_DIR"
mvn clean package -DskipTests
if [ $? -ne 0 ]; then
log "构建失败!"
exit 1
fi
log "构建成功"
}
# 函数:重启服务
restart_service() {
log "重启服务..."
systemctl restart tomcat
sleep 5
if systemctl is-active --quiet tomcat; then
log "服务重启成功"
else
log "服务重启失败!正在回滚..."
rollback
fi
}
# 函数:回滚
rollback() {
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/webapp_*.tar.gz | head -1)
if [ -n "$LATEST_BACKUP" ]; then
tar -xzf "$LATEST_BACKUP" -C /
systemctl restart tomcat
log "已回滚到: $LATEST_BACKUP"
fi
}
# 主流程
case $1 in
deploy)
backup
pull_code
build
restart_service
;;
rollback)
rollback
;;
*)
echo "用法: $0 {deploy|rollback}"
exit 1
;;
esac
附录:命令速查表
Shell 常用快捷键
| 快捷键 | 功能 |
|---|---|
Ctrl+A | 光标跳到行首 |
Ctrl+E | 光标跳到行尾 |
Ctrl+U | 删除光标前所有内容 |
Ctrl+K | 删除光标后所有内容 |
Ctrl+W | 删除前一个单词 |
Ctrl+L | 清屏 |
Ctrl+R | 搜索历史命令 |
Tab | 自动补全 |
!! | 执行上一条命令 |
!n | 执行历史第 n 条命令 |
Git 常用命令速查
| 命令 | 说明 |
|---|---|
git init | 初始化仓库 |
git clone | 克隆远程仓库 |
git status | 查看状态 |
git add | 添加到暂存区 |
git commit -m "msg" | 提交到版本库 |
git log --oneline | 查看历史 |
git diff | 查看差异 |
git branch | 管理分支 |
git checkout -b name | 创建并切换分支 |
git merge branch | 合并分支 |
git push | 推送到远程 |
git pull | 拉取远程更新 |
git stash | 暂存当前修改 |
git stash pop | 恢复暂存 |
git tag v1.0 | 打标签 |