avatar

青青子衿的拾枝杂谈

A text-focused Halo theme

  • 首页
  • linux基础
  • Linux系统
  • Linux高级
  • nginx
  • k8s
  • 网络
Home Shell脚本与版本控制
文章

Shell脚本与版本控制

Posted recently Updated recently
By 青青子衿
110~142 min read

Shell 脚本编程与版本控制实战指南

环境假设:CentOS 7/8 或 RHEL 系列 Linux


目录

  • 第一部分:Shell 脚本编程
    • 1. Shell 脚本介绍
    • 2. 脚本基础知识及变量
    • 3. 常用命令速查
    • 4. test 命令与判断语法
    • 5. 循环语法
    • 6. case 分支语法与函数
    • 7. 变量替换与数组
    • 8. 正则表达式
    • 9. sed 流编辑器
    • 10. awk 的使用
    • 11. SSH 远程操作与终端控制
  • 第二部分:版本控制与 CI/CD
    • 1. SVN 版本控制
    • 2. Git 基础
    • 3. GitHub 使用
    • 4. Git 工作流程
    • 5. Issues 与 Pull Request
    • 6. GitHub Pages
    • 7. Jenkins 持续集成

第一部分:Shell 脚本编程

1. Shell 脚本介绍

什么是 Shell?

类比:如果把操作系统想象成一家餐厅,那么 Shell 就是你和服务员之间的"对话窗口"。你告诉服务员你想吃什么(输入命令),服务员把需求传给后厨(系统内核),然后把做好的菜端给你(返回结果)。

Shell 是一个命令行解释器,它接收用户输入的命令,解析后交给系统内核去执行。Linux 中常见的 Shell 有:

Shell 类型路径说明
bash/bin/bash最常用,功能丰富,大多数 Linux 默认 Shell
sh/bin/shbash 的简化版,兼容性好
zsh/bin/zshmacOS 默认,补全功能强
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

对比项SVNGit
架构集中式(依赖中央服务器)分布式(每人都有完整仓库)
版本号递增整数 (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 或者有个好想法,但暂时不会改代码。

使用流程:

  1. 在项目页面点击 Issues → New Issue
  2. 填写标题和描述(描述越详细越好,最好附上截图和复现步骤)
  3. 项目维护者会看到通知,与你讨论
  4. 问题解决后关闭 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 创建后,项目维护者会:

  1. 查看你的代码修改
  2. 在特定行添加评论
  3. 要求你修改(你再 push 新提交即可自动更新 PR)
  4. 确认没问题后 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?

术语全称含义
CIContinuous Integration(持续集成)开发者频繁地将代码合并到主干,自动编译和测试
CDContinuous Delivery(持续交付)在 CI 基础上,自动将代码部署到测试/生产环境
CDContinuous 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打标签

linux服务
Linux系统 linux基础
License:  CC BY 4.0
Share

Further Reading

Jul 4, 2026

监控虚拟化与集群

监控系统、虚拟化与集群存储 -- 运维实战指南 第一部分:监控系统 -- 给服务器做"体检" 1.1 为什么需要监控系统? 生活类比:想象你是一个学校的校医,负责1000名学生的健康。如果每天都要一个个量体温、测心率,你肯定累趴了。最好的办法是:每个学生戴一个智能手环,实时把健康数据发到你办公室的大

Jul 4, 2026

Docker与Kubernetes

Docker容器与Kubernetes集群 -- 从零开始的实战指南 目录 第一部分:Docker容器技术 容器概念:为什么我们需要容器? Docker介绍与安装 Docker客户端操作

Jul 4, 2026

Shell脚本与版本控制

Shell 脚本编程与版本控制实战指南 环境假设:CentOS 7/8 或 RHEL 系列 Linux 目录 第一部分:Shell 脚本编程 1. Shell 脚本介绍 2. 脚本基础知识及变量

OLDER

Python编程

NEWER

Linux基础与服务

Recently Updated

  • 监控虚拟化与集群
  • Docker与Kubernetes
  • Nginx与Tomcat
  • 网络协议与安全
  • 数据库与自动化运维

Trending Tags

安全 Linux系统 nginx 日志管理 Linux服务 网络 Linux高级 pxe 中间件 python

Contents

©2026 青青子衿的拾枝杂谈 . Some rights reserved.

Using the Halo theme Chirpy