Skip to content

Shell 脚本编程

本章将介绍 Shell 脚本编程,包括脚本基础、变量、流程控制、函数、输入输出以及实战案例,帮助你编写自动化脚本。

脚本基础

第一个脚本

bash
#!/bin/bash
# 这是第一个 Shell 脚本
# 文件名:hello.sh

# 输出欢迎信息
echo "Hello, World!"
echo "当前用户:$USER"
echo "当前目录:$(pwd)"

# 运行脚本的方式:
# 1. 添加执行权限后运行
chmod +x hello.sh
./hello.sh

# 2. 使用解释器运行
bash hello.sh

# 3. 使用 source 执行(在当前 Shell 中执行)
source hello.sh
. hello.sh

脚本结构

bash
#!/bin/bash
# =====================================
# 脚本名称:script_name.sh
# 功能描述:脚本功能说明
# 作    者:作者名
# 创建日期:2024-01-15
# 修改日期:2024-01-16
# =====================================

# 设置严格模式
set -e          # 命令失败时退出
set -u          # 使用未定义变量时退出
set -o pipefail  # 管道中任一命令失败时退出

# 脚本配置
SCRIPT_NAME=$(basename "$0")
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)

# 帮助信息
usage() {
    echo "用法: $SCRIPT_NAME [选项]"
    echo "选项:"
    echo "  -h, --help     显示帮助信息"
    echo "  -v, --version  显示版本信息"
}

# 主程序
main() {
    echo "脚本开始执行"
    # 在这里编写主要逻辑
    echo "脚本执行完成"
}

# 调用主函数
main "$@"

Shebang

bash
#!/bin/bash        # 使用 bash 解释器
#!/bin/sh          # 使用 sh 解释器(POSIX 兼容)
#!/usr/bin/env bash  # 使用 env 查找 bash(更可移植)
#!/usr/bin/python3   # Python 脚本
#!/usr/bin/env node  # Node.js 脚本

# 查看可用的 Shell
cat /etc/shells

# 查看默认 Shell
echo $SHELL

变量

变量定义和使用

bash
#!/bin/bash

# 变量定义(等号两边不能有空格)
name="张三"
age=25
score=95.5

# 使用变量
echo "姓名:$name"
echo "年龄:$age"
echo "成绩:$score"

# 使用 ${} 明确变量边界
echo "姓名是${name},年龄是${age}岁"

# 变量赋值
current_date=$(date +%Y-%m-%d)
current_time=`date +%H:%M:%S`  # 旧式写法

# 只读变量
readonly PI=3.14159
# PI=3.14  # 错误:只读变量不能修改

# 删除变量
unset score
echo "成绩:$score"  # 输出空

# 变量默认值
echo "未定义变量:${undefined_var:-默认值}"
echo "未定义变量:${undefined_var:=默认值并赋值}"

# 变量存在时返回值
defined_var="已定义"
echo "变量存在:${defined_var:+存在时返回此值}"

数据类型

bash
#!/bin/bash

# 字符串
str1='单引号字符串,变量$HOME不会被替换'
str2="双引号字符串,变量$HOME会被替换"
str3="字符串可以
换行"

# 字符串操作
str="Hello World"
echo "字符串长度:${#str}"
echo "子字符串:${str:0:5}"      # 从位置0开始,取5个字符
echo "子字符串:${str:6}"        # 从位置6开始到结尾
echo "删除匹配:${str#Hello }"   # 删除开头匹配
echo "删除匹配:${str%World}"    # 删除结尾匹配
echo "替换:${str/World/Linux}"  # 替换第一个匹配
echo "替换全部:${str//o/O}"     # 替换所有匹配

# 数组
arr=(one two three four five)

echo "第一个元素:${arr[0]}"
echo "所有元素:${arr[@]}"
echo "所有元素:${arr[*]}"
echo "数组长度:${#arr[@]}"
echo "元素长度:${#arr[0]}"

# 遍历数组
for item in "${arr[@]}"; do
    echo "元素:$item"
done

# 关联数组(需要 bash 4.0+)
declare -A user
user[name]="张三"
user[age]=25
user[city]="北京"

echo "姓名:${user[name]}"
echo "所有键:${!user[@]}"
echo "所有值:${user[@]}"

# 遍历关联数组
for key in "${!user[@]}"; do
    echo "$key: ${user[$key]}"
done

特殊变量

bash
#!/bin/bash

# 特殊变量
echo "脚本名称:$0"
echo "第一个参数:$1"
echo "第二个参数:$2"
echo "参数个数:$#"
echo "所有参数:$@"
echo "所有参数:$*"
echo "上一命令退出状态:$?"
echo "当前进程 PID:$$"
echo "后台进程 PID:$!"

# $@ 和 $* 的区别
echo "--- 使用 \$@ ---"
for arg in "$@"; do
    echo "参数:$arg"
done

echo "--- 使用 \$* ---"
for arg in "$*"; do
    echo "参数:$arg"
done

# 参数处理
while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)
            echo "显示帮助信息"
            shift
            ;;
        -v|--version)
            echo "版本 1.0.0"
            shift
            ;;
        -*)
            echo "未知选项:$1"
            exit 1
            ;;
        *)
            echo "参数:$1"
            shift
            ;;
    esac
done

环境变量

bash
#!/bin/bash

# 查看环境变量
echo "HOME: $HOME"
echo "USER: $USER"
echo "PATH: $PATH"
echo "PWD: $PWD"
echo "SHELL: $SHELL"

# 设置环境变量
export MY_VAR="自定义环境变量"

# 在脚本中设置的环境变量只在当前进程及其子进程有效

# 常用环境变量
echo "主机名:$HOSTNAME"
echo "历史命令数:$HISTSIZE"
echo "语言设置:$LANG"
echo "终端类型:$TERM"

# 读取用户输入
read -p "请输入姓名:" name
read -s -p "请输入密码:" password
echo
echo "姓名:$name"
echo "密码长度:${#password}"

# 带超时的输入
read -t 5 -p "5秒内输入:" input || echo "超时"

流程控制

条件判断

bash
#!/bin/bash

# if 语句
age=18

if [[ $age -ge 18 ]]; then
    echo "成年人"
elif [[ $age -ge 12 ]]; then
    echo "青少年"
else
    echo "儿童"
fi

# 数值比较
# -eq: 等于
# -ne: 不等于
# -gt: 大于
# -lt: 小于
# -ge: 大于等于
# -le: 小于等于

if [[ $age -eq 18 ]]; then
    echo "正好18岁"
fi

# 字符串比较
str1="hello"
str2="world"

if [[ $str1 == $str2 ]]; then
    echo "字符串相等"
elif [[ $str1 != $str2 ]]; then
    echo "字符串不相等"
fi

# 字符串判空
if [[ -z "$str1" ]]; then
    echo "字符串为空"
fi

if [[ -n "$str1" ]]; then
    echo "字符串非空"
fi

# 文件测试
file="/etc/passwd"

if [[ -e $file ]]; then echo "文件存在"; fi
if [[ -f $file ]]; then echo "是普通文件"; fi
if [[ -d $file ]]; then echo "是目录"; fi
if [[ -r $file ]]; then echo "文件可读"; fi
if [[ -w $file ]]; then echo "文件可写"; fi
if [[ -x $file ]]; then echo "文件可执行"; fi
if [[ -s $file ]]; then echo "文件非空"; fi
if [[ -L $file ]]; then echo "是符号链接"; fi

# 逻辑运算
if [[ $age -ge 18 && $age -le 60 ]]; then
    echo "工作年龄"
fi

if [[ $age -lt 18 || $age -gt 60 ]]; then
    echo "非工作年龄"
fi

# 正则匹配
email="user@example.com"
if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "有效的邮箱地址"
fi

case 语句

bash
#!/bin/bash

# case 语句
read -p "请输入选项 (start/stop/restart/status): " action

case $action in
    start)
        echo "启动服务"
        ;;
    stop)
        echo "停止服务"
        ;;
    restart)
        echo "重启服务"
        ;;
    status)
        echo "查看状态"
        ;;
    *)
        echo "未知选项:$action"
        exit 1
        ;;
esac

# 模式匹配
read -p "请输入文件名:filename

case $filename in
    *.txt)
        echo "文本文件"
        ;;
    *.sh)
        echo "Shell 脚本"
        ;;
    *.py)
        echo "Python 脚本"
        ;;
    *.jpg|*.png|*.gif)
        echo "图片文件"
        ;;
    *)
        echo "未知文件类型"
        ;;
esac

# 使用 | 分隔多个模式
read -p "请输入字符:" char

case $char in
    [a-z]|[A-Z])
        echo "字母"
        ;;
    [0-9])
        echo "数字"
        ;;
    *)
        echo "其他字符"
        ;;
esac

循环语句

bash
#!/bin/bash

# for 循环 - 列表形式
for i in 1 2 3 4 5; do
    echo "数字:$i"
done

# for 循环 - 范围形式
for i in {1..5}; do
    echo "数字:$i"
done

# for 循环 - 步长
for i in {1..10..2}; do
    echo "奇数:$i"
done

# for 循环 - C 风格
for ((i=1; i<=5; i++)); do
    echo "计数:$i"
done

# for 循环 - 遍历文件
for file in /etc/*.conf; do
    echo "配置文件:$file"
done

# for 循环 - 遍历命令输出
for user in $(cut -d: -f1 /etc/passwd); do
    echo "用户:$user"
done

# while 循环
count=1
while [[ $count -le 5 ]]; do
    echo "计数:$count"
    ((count++))
done

# while 循环 - 读取文件
while IFS= read -r line; do
    echo "行:$line"
done < /etc/hosts

# while 循环 - 无限循环
while true; do
    echo "按 Ctrl+C 退出"
    sleep 1
done

# until 循环(条件为假时执行)
count=1
until [[ $count -gt 5 ]]; do
    echo "计数:$count"
    ((count++))
done

# 循环控制
for i in {1..10}; do
    if [[ $i -eq 3 ]]; then
        continue    # 跳过当前迭代
    fi
    if [[ $i -eq 7 ]]; then
        break       # 退出循环
    fi
    echo "数字:$i"
done

# 嵌套循环
for i in {1..3}; do
    for j in {1..3}; do
        echo "($i, $j)"
    done
done

函数

函数定义

bash
#!/bin/bash

# 函数定义方式一
function greet() {
    echo "Hello, $1!"
}

# 函数定义方式二
say_hello() {
    echo "Hello, World!"
}

# 调用函数
greet "张三"
say_hello

# 带返回值的函数
add() {
    local result=$(($1 + $2))
    echo $result
}

sum=$(add 10 20)
echo "和:$sum"

# 使用 return 返回状态码
check_file() {
    if [[ -f $1 ]]; then
        return 0    # 成功
    else
        return 1    # 失败
    fi
}

check_file /etc/passwd
if [[ $? -eq 0 ]]; then
    echo "文件存在"
fi

# 局部变量
func() {
    local local_var="局部变量"
    global_var="全局变量"
    echo "函数内:$local_var"
}

func
echo "函数外:$global_var"
# echo "函数外:$local_var"  # 错误:局部变量不可访问

函数参数

bash
#!/bin/bash

# 函数参数
show_args() {
    echo "函数名:$0"
    echo "参数个数:$#"
    echo "所有参数:$@"
    echo "第一个参数:$1"
    echo "第二个参数:$2"
    
    # 遍历参数
    for arg in "$@"; do
        echo "参数:$arg"
    done
}

show_args "a" "b" "c"

# 参数默认值
greet() {
    local name=${1:-"访客"}
    echo "你好,$name!"
}

greet        # 输出:你好,访客!
greet "张三"  # 输出:你好,张三!

# 可变参数
sum() {
    local total=0
    for num in "$@"; do
        ((total += num))
    done
    echo $total
}

result=$(sum 1 2 3 4 5)
echo "总和:$result"

递归函数

bash
#!/bin/bash

# 阶乘
factorial() {
    local n=$1
    if [[ $n -le 1 ]]; then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}

result=$(factorial 5)
echo "5! = $result"

# 斐波那契数列
fibonacci() {
    local n=$1
    if [[ $n -le 1 ]]; then
        echo $n
    else
        local a=$(fibonacci $((n - 1)))
        local b=$(fibonacci $((n - 2)))
        echo $((a + b))
    fi
}

for i in {0..10}; do
    result=$(fibonacci $i)
    echo "F($i) = $result"
done

输入输出

echo 和 printf

bash
#!/bin/bash

# echo 输出
echo "普通输出"
echo -n "不换行输出"
echo "继续输出"
echo -e "转义字符:\t制表符\n换行"

# printf 格式化输出
printf "姓名:%s,年龄:%d\n" "张三" 25
printf "浮点数:%.2f\n" 3.14159
printf "宽度:|%10s|\n" "hello"
printf "左对齐:|%-10s|\n" "hello"

# printf 格式说明符
# %s - 字符串
# %d - 整数
# %f - 浮点数
# %x - 十六进制
# %o - 八进制
# %c - 单个字符

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'    # 重置颜色

echo -e "${RED}红色文字${NC}"
echo -e "${GREEN}绿色文字${NC}"
echo -e "${YELLOW}黄色文字${NC}"

printf "${RED}错误:${NC}操作失败\n"
printf "${GREEN}成功:${NC}操作完成\n"

read 输入

bash
#!/bin/bash

# 基本输入
read -p "请输入姓名:" name
echo "你好,$name!"

# 静默输入(密码)
read -s -p "请输入密码:" password
echo
echo "密码长度:${#password}"

# 限制输入长度
read -n 1 -p "按任意键继续..." key
echo
echo "你按下了:$key"

# 带超时
read -t 10 -p "10秒内输入:" input || echo "超时"

# 读取到数组
read -a arr -p "输入多个单词(空格分隔):"
echo "第一个:${arr[0]}"
echo "所有:${arr[@]}"

# 读取整行
while IFS= read -r line; do
    echo "行:$line"
done <<< "第一行
第二行
第三行"

# 从文件读取
while IFS=: read -r user pass uid gid rest; do
    echo "用户:$user,UID:$uid"
done < /etc/passwd

文件描述符

bash
#!/bin/bash

# 标准文件描述符
# 0 - stdin(标准输入)
# 1 - stdout(标准输出)
# 2 - stderr(标准错误)

# 重定向
echo "标准输出" > output.txt
echo "追加内容" >> output.txt

# 重定向错误
ls /notexist 2> error.txt

# 同时重定向输出和错误
ls / /notexist > all.txt 2>&1
ls / /notexist &> all.txt    # 简写

# 丢弃输出
ls /notexist 2>/dev/null

# 自定义文件描述符
exec 3> custom.txt
echo "写入文件描述符3" >&3
exec 3>&-    # 关闭

# 从文件读取
exec 3< /etc/hosts
while read -u 3 line; do
    echo "读取:$line"
done
exec 3<&-    # 关闭

# 同时读写
exec 3<> file.txt
echo "写入" >&3
exec 3<&-

实战案例

系统备份脚本

bash
#!/bin/bash
# =====================================
# 脚本名称:backup.sh
# 功能描述:系统备份脚本
# =====================================

set -e

# 配置
BACKUP_DIR="/backup"
SOURCE_DIRS=("/etc" "/home" "/var/www")
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="${BACKUP_DIR}/backup_${DATE}.log"

# 创建备份目录
mkdir -p "$BACKUP_DIR"

# 日志函数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 备份函数
backup() {
    local source=$1
    local dest="${BACKUP_DIR}/$(basename $source)_${DATE}.tar.gz"
    
    log "开始备份:$source"
    
    if tar -czf "$dest" "$source" 2>> "$LOG_FILE"; then
        log "备份成功:$dest"
        log "文件大小:$(du -h "$dest" | cut -f1)"
    else
        log "备份失败:$source"
        return 1
    fi
}

# 清理旧备份
cleanup() {
    log "清理30天前的备份..."
    find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete
    log "清理完成"
}

# 主程序
main() {
    log "========== 备份开始 =========="
    
    for dir in "${SOURCE_DIRS[@]}"; do
        if [[ -d "$dir" ]]; then
            backup "$dir"
        else
            log "目录不存在:$dir"
        fi
    done
    
    cleanup
    
    log "========== 备份完成 =========="
}

main "$@"

服务监控脚本

bash
#!/bin/bash
# =====================================
# 脚本名称:monitor.sh
# 功能描述:服务监控脚本
# =====================================

# 配置
SERVICES=("nginx" "mysql" "redis")
ALERT_EMAIL="admin@example.com"
LOG_FILE="/var/log/monitor.log"

# 日志函数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 检查服务状态
check_service() {
    local service=$1
    
    if systemctl is-active --quiet "$service"; then
        log "$service 运行正常"
        return 0
    else
        log "$服务已停止,尝试重启..."
        systemctl restart "$service"
        
        sleep 3
        
        if systemctl is-active --quiet "$service"; then
            log "$service 重启成功"
            return 0
        else
            log "$service 重启失败"
            send_alert "$service"
            return 1
        fi
    fi
}

# 发送告警
send_alert() {
    local service=$1
    local message="$service 服务异常,请检查!"
    
    # 发送邮件(需要配置邮件服务)
    echo "$message" | mail -s "服务告警:$service" "$ALERT_EMAIL"
    
    log "已发送告警邮件"
}

# 检查磁盘空间
check_disk() {
    local threshold=90
    local usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
    
    if [[ $usage -gt $threshold ]]; then
        log "磁盘使用率超过 $threshold%:当前 $usage%"
        # 发送告警
    fi
}

# 检查内存
check_memory() {
    local threshold=90
    local usage=$(free | awk '/Mem/ {printf "%.0f", $3/$2 * 100}')
    
    if [[ $usage -gt $threshold ]]; then
        log "内存使用率超过 $threshold%:当前 $usage%"
        # 发送告警
    fi
}

# 主程序
main() {
    log "========== 监控开始 =========="
    
    # 检查服务
    for service in "${SERVICES[@]}"; do
        check_service "$service"
    done
    
    # 检查系统资源
    check_disk
    check_memory
    
    log "========== 监控完成 =========="
}

main "$@"

日志分析脚本

bash
#!/bin/bash
# =====================================
# 脚本名称:log_analyze.sh
# 功能描述:日志分析脚本
# =====================================

# 配置
LOG_FILE="/var/log/nginx/access.log"
REPORT_FILE="/tmp/log_report.txt"

# 分析 IP 访问量
analyze_ip() {
    echo "=== IP 访问量 TOP 10 ==="
    awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
}

# 分析请求路径
analyze_path() {
    echo "=== 请求路径 TOP 10 ==="
    awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
}

# 分析状态码
analyze_status() {
    echo "=== 状态码统计 ==="
    awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn
}

# 分析访问时间
analyze_time() {
    echo "=== 每小时访问量 ==="
    awk '{print substr($4, 14, 2)}' "$LOG_FILE" | sort | uniq -c
}

# 分析 User-Agent
analyze_ua() {
    echo "=== 浏览器统计 ==="
    awk -F'"' '{print $6}' "$LOG_FILE" | grep -oE '(Chrome|Firefox|Safari|Edge|MSIE)' | sort | uniq -c | sort -rn
}

# 生成报告
generate_report() {
    {
        echo "日志分析报告"
        echo "生成时间:$(date)"
        echo "日志文件:$LOG_FILE"
        echo ""
        
        analyze_ip
        echo ""
        
        analyze_path
        echo ""
        
        analyze_status
        echo ""
        
        analyze_time
        echo ""
        
        analyze_ua
        
    } > "$REPORT_FILE"
    
    echo "报告已生成:$REPORT_FILE"
}

# 主程序
main() {
    if [[ ! -f "$LOG_FILE" ]]; then
        echo "日志文件不存在:$LOG_FILE"
        exit 1
    fi
    
    generate_report
}

main "$@"

小结

本章介绍了 Shell 脚本编程:

内容说明
脚本基础Shebang、脚本结构、执行方式
变量定义、使用、数据类型、特殊变量
流程控制if、case、for、while、until
函数定义、参数、返回值、递归
输入输出echo、printf、read、重定向

最佳实践

bash
# 1. 使用严格模式
set -euo pipefail

# 2. 变量使用双引号
echo "$variable"

# 3. 使用 [[ ]] 进行条件判断
if [[ $var == "value" ]]; then

# 4. 函数使用 local 变量
func() {
    local var="local"
}

# 5. 添加错误处理
command || exit 1

# 6. 使用有意义的变量名
log_file="/var/log/app.log"

# 7. 添加注释和帮助信息

下一步

下一章我们将学习 服务管理,了解 systemd 和服务配置。