Shell高级编程

Shell高级编程技巧可以让我们编写更强大、更灵活的脚本。本章将介绍高级变量操作、高级函数编程、进程替换、信号处理等高级主题,帮助您成为Shell编程专家。


一、高级变量

1.1 变量替换

1.1.1 基本替换

变量替换语法

语法 说明 示例
${var} 变量值 ${name}
${#var} 变量长度 ${#name}
${var:-word} 变量为空时返回word ${name:-"default"}
${var:=word} 变量为空时赋值word ${name:="default"}
${var:+word} 变量非空时返回word ${name:+"exists"}
${var:?message} 变量为空时报错 ${name:?"name is empty"}

示例

#!/bin/bash

# 变量长度
name="东巴文"
echo "变量长度: ${#name}"  # 输出: 3

# 默认值
unset var
echo "${var:-default}"     # 输出: default
echo "$var"                # 输出: (空)

# 赋默认值
unset var
echo "${var:=default}"     # 输出: default
echo "$var"                # 输出: default

# 变量存在时返回值
var="value"
echo "${var:+exists}"      # 输出: exists

# 变量为空时报错
unset var
echo "${var:?变量未设置}"  # 输出: bash: var: 变量未设置

东巴文理解:变量替换可以简化条件判断,提高代码可读性。

1.1.2 字符串操作

字符串操作语法

语法 说明 示例
${var#pattern} 删除开头最短匹配 ${path#*/}
${var##pattern} 删除开头最长匹配 ${path##*/}
${var%pattern} 删除结尾最短匹配 ${path%/*}
${var%%pattern} 删除结尾最长匹配 ${path%%/*}
${var:offset} 从offset开始截取 ${str:3}
${var:offset:length} 从offset截取length个字符 ${str:3:5}
${var/pattern/replace} 替换第一个匹配 ${str/old/new}
${var//pattern/replace} 替换所有匹配 ${str//old/new}
${var/#pattern/replace} 替换开头匹配 ${str/#old/new}
${var/%pattern/replace} 替换结尾匹配 ${str/%old/new}

示例

#!/bin/bash

path="/usr/local/bin/python"

# 删除开头最短匹配
echo "${path#*/}"        # 输出: usr/local/bin/python

# 删除开头最长匹配
echo "${path##*/}"       # 输出: python

# 删除结尾最短匹配
echo "${path%/*}"        # 输出: /usr/local/bin

# 删除结尾最长匹配
echo "${path%%/*}"       # 输出: (空)

# 字符串截取
str="Hello World"
echo "${str:6}"          # 输出: World
echo "${str:0:5}"        # 输出: Hello

# 字符串替换
str="hello world world"
echo "${str/world/WORLD}"   # 输出: hello WORLD world
echo "${str//world/WORLD}"  # 输出: hello WORLD WORLD
echo "${str/#hello/HELLO}"  # 输出: HELLO world world
echo "${str/%world/WORLD}"  # 输出: hello world WORLD

# 大小写转换
str="Hello World"
echo "${str,,}"          # 输出: hello world (转小写)
echo "${str^^}"          # 输出: HELLO WORLD (转大写)
echo "${str,}"           # 输出: hello World (首字母小写)
echo "${str^}"           # 输出: Hello World (首字母大写)

1.2 间接引用

1.2.1 变量间接引用

使用${!var}语法

#!/bin/bash

# 间接引用
name="value"
ref="name"
echo "${!ref}"           # 输出: value

# 等价于
eval echo \$$ref         # 输出: value

# 动态变量名
for i in 1 2 3; do
    var_$i="value_$i"
done

echo "${var_1}"          # 输出: value_1
echo "${var_2}"          # 输出: value_2
echo "${var_3}"          # 输出: value_3

# 使用间接引用访问动态变量
for i in 1 2 3; do
    ref="var_$i"
    echo "${!ref}"
done

1.2.2 数组间接引用

#!/bin/bash

# 数组间接引用
arr=("one" "two" "three")
ref="arr[@]"
echo "${!ref}"           # 输出: one two three

# 动态数组
for i in 1 2 3; do
    declare -a "arr_$i=(a b c)"
done

# 访问动态数组
ref="arr_1[@]"
echo "${!ref}"           # 输出: a b c

# 使用eval
eval echo \${arr_2[@]}   # 输出: a b c

东巴文提示:间接引用可以实现动态变量名,但要注意安全性。

1.3 变量属性

1.3.1 declare命令

declare选项

选项 说明
-a 声明数组
-A 声明关联数组
-i 声明整数
-r 声明只读变量
-x 声明环境变量
-u 转换为大写
-l 转换为小写
-n 声明名称引用

示例

#!/bin/bash

# 声明整数
declare -i num=10
num="abc"               # 自动转换为0
echo "$num"             # 输出: 0

# 声明只读变量
declare -r PI=3.14159
PI=3.14                 # 报错: bash: PI: 只读变量

# 声明数组
declare -a arr=(1 2 3)
echo "${arr[@]}"        # 输出: 1 2 3

# 声明关联数组
declare -A user
user[name]="东巴文"
user[age]=25
echo "${user[name]}"    # 输出: 东巴文

# 转换大小写
declare -u upper="hello"
echo "$upper"           # 输出: HELLO

declare -l lower="WORLD"
echo "$lower"           # 输出: world

# 名称引用
declare -n ref=user
echo "${ref[name]}"     # 输出: 东巴文

1.3.2 变量作用域

#!/bin/bash

# 全局变量
global_var="全局变量"

function test_scope() {
    # 局部变量
    local local_var="局部变量"
    
    echo "函数内访问全局变量: $global_var"
    echo "函数内访问局部变量: $local_var"
    
    # 修改全局变量
    global_var="修改后的全局变量"
}

test_scope

echo "函数外访问全局变量: $global_var"
echo "函数外访问局部变量: $local_var"  # 输出: (空)

二、高级函数

2.1 函数库

2.1.1 创建函数库

函数库文件

#!/bin/bash
#===============================================================================
# 文件名称:utils.lib
# 文件功能:通用函数库
# 作者:东巴文
#===============================================================================

# 颜色定义
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color

# 打印函数
function print_info() {
    echo -e "${BLUE}[INFO]${NC} $1"
}

function print_success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

function print_warning() {
    echo -e "${YELLOW}[WARNING]${NC} $1"
}

function print_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# 日志函数
function log() {
    local level="$1"
    local message="$2"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" >> /var/log/script.log
}

# 验证函数
function is_root() {
    [[ $EUID -eq 0 ]]
}

function is_installed() {
    command -v "$1" &> /dev/null
}

function is_file() {
    [[ -f "$1" ]]
}

function is_dir() {
    [[ -d "$1" ]]
}

# 字符串函数
function trim() {
    local var="$1"
    var="${var#"${var%%[![:space:]]*}"}"
    var="${var%"${var##*[![:space:]]}"}"
    echo "$var"
}

function to_lower() {
    echo "${1,,}"
}

function to_upper() {
    echo "${1^^}"
}

# 数组函数
function array_contains() {
    local needle="$1"
    shift
    local item
    for item in "$@"; do
        [[ "$item" == "$needle" ]] && return 0
    done
    return 1
}

function array_unique() {
    local -A seen
    local item
    for item in "$@"; do
        [[ -z "${seen[$item]}" ]] && echo "$item"
        seen[$item]=1
    done
}

# 数学函数
function max() {
    local max=$1
    shift
    for num in "$@"; do
        ((num > max)) && max=$num
    done
    echo "$max"
}

function min() {
    local min=$1
    shift
    for num in "$@"; do
        ((num < min)) && min=$num
    done
    echo "$min"
}

function sum() {
    local total=0
    for num in "$@"; do
        ((total += num))
    done
    echo "$total"
}

# 网络函数
function get_ip() {
    hostname -I | awk '{print $1}'
}

function is_port_open() {
    local host="$1"
    local port="$2"
    timeout 1 bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null
}

# 文件函数
function backup_file() {
    local file="$1"
    local backup="${file}.bak.$(date +%Y%m%d_%H%M%S)"
    cp "$file" "$backup" && echo "$backup"
}

function get_file_size() {
    local file="$1"
    stat -c %s "$file" 2>/dev/null || stat -f %z "$file" 2>/dev/null
}

function get_file_md5() {
    local file="$1"
    md5sum "$file" 2>/dev/null | awk '{print $1}' || md5 "$file" 2>/dev/null | awk '{print $4}'
}

2.1.2 使用函数库

引入函数库

#!/bin/bash
#===============================================================================
# 脚本名称:use_library.sh
# 脚本功能:使用函数库示例
# 作者:东巴文
#===============================================================================

# 引入函数库
LIB_PATH="/path/to/utils.lib"

if [[ ! -f "$LIB_PATH" ]]; then
    echo "错误: 函数库不存在"
    exit 1
fi

# source函数库
source "$LIB_PATH"

# 使用函数库中的函数
print_info "开始执行脚本"

# 检查是否为root用户
if is_root; then
    print_warning "请勿使用root用户运行此脚本"
    exit 1
fi

# 检查命令是否安装
if ! is_installed "git"; then
    print_error "git未安装"
    exit 1
fi

# 使用字符串函数
name="  东巴文  "
trimmed=$(trim "$name")
print_info "去除空格: '$trimmed'"

# 使用数组函数
arr=(1 2 3 2 1)
unique=$(array_unique "${arr[@]}")
print_info "去重: $unique"

# 使用数学函数
numbers=(10 5 8 3 9)
print_info "最大值: $(max "${numbers[@]}")"
print_info "最小值: $(min "${numbers[@]}")"
print_info "总和: $(sum "${numbers[@]}")"

# 使用网络函数
ip=$(get_ip)
print_info "本机IP: $ip"

# 使用文件函数
file="/etc/passwd"
if is_file "$file"; then
    size=$(get_file_size "$file")
    print_info "文件大小: $size 字节"
    
    md5=$(get_file_md5 "$file")
    print_info "文件MD5: $md5"
fi

print_success "脚本执行完成"

东巴文提示:函数库可以提高代码复用性,减少重复代码。

2.2 递归函数

2.2.1 递归基础

递归示例

#!/bin/bash

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

# 测试
echo "5! = $(factorial 5)"   # 输出: 120
echo "10! = $(factorial 10)" # 输出: 3628800

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

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

# 汉诺塔
function hanoi() {
    local n=$1
    local from=$2
    local to=$3
    local aux=$4
    
    if ((n == 1)); then
        echo "移动盘子 1 从 $from$to"
    else
        hanoi $((n - 1)) "$from" "$aux" "$to"
        echo "移动盘子 $n$from$to"
        hanoi $((n - 1)) "$aux" "$to" "$from"
    fi
}

# 测试
hanoi 3 "A" "C" "B"

2.2.2 递归遍历目录

#!/bin/bash

# 递归遍历目录
function traverse_dir() {
    local dir="$1"
    local indent="${2:-0}"
    local prefix=$(printf "%${indent}s" "")
    
    for item in "$dir"/*; do
        if [[ -d "$item" ]]; then
            echo "${prefix}📁 $(basename "$item")/"
            traverse_dir "$item" $((indent + 2))
        elif [[ -f "$item" ]]; then
            echo "${prefix}📄 $(basename "$item")"
        fi
    done
}

# 测试
traverse_dir "/etc" 0

# 递归查找文件
function find_files() {
    local dir="$1"
    local pattern="$2"
    
    for item in "$dir"/*; do
        if [[ -d "$item" ]]; then
            find_files "$item" "$pattern"
        elif [[ -f "$item" ]]; then
            if [[ "$(basename "$item")" == *"$pattern"* ]]; then
                echo "$item"
            fi
        fi
    done
}

# 测试
find_files "/etc" ".conf"

# 递归计算目录大小
function dir_size() {
    local dir="$1"
    local total=0
    
    for item in "$dir"/*; do
        if [[ -d "$item" ]]; then
            total=$((total + $(dir_size "$item")))
        elif [[ -f "$item" ]]; then
            total=$((total + $(stat -c %s "$item")))
        fi
    done
    
    echo "$total"
}

# 测试
size=$(dir_size "/var/log")
echo "目录大小: $((size / 1024)) KB"

2.3 回调函数

2.3.1 回调函数实现

#!/bin/bash

# 使用回调函数处理数组
function process_array() {
    local callback="$1"
    shift
    local arr=("$@")
    
    for item in "${arr[@]}"; do
        $callback "$item"
    done
}

# 回调函数
function print_item() {
    echo "处理: $1"
}

function double_item() {
    echo "$(( $1 * 2 ))"
}

function square_item() {
    echo "$(( $1 * $1 ))"
}

# 测试
arr=(1 2 3 4 5)

echo "打印元素:"
process_array print_item "${arr[@]}"

echo -e "\n元素翻倍:"
process_array double_item "${arr[@]}"

echo -e "\n元素平方:"
process_array square_item "${arr[@]}"

# 使用回调函数过滤数组
function filter_array() {
    local callback="$1"
    shift
    local arr=("$@")
    local result=()
    
    for item in "${arr[@]}"; do
        if $callback "$item"; then
            result+=("$item")
        fi
    done
    
    echo "${result[@]}"
}

# 过滤函数
function is_even() {
    local num=$1
    (( num % 2 == 0 ))
}

function is_positive() {
    local num=$1
    (( num > 0 ))
}

# 测试
arr=(-2 -1 0 1 2 3 4 5)

echo "偶数: $(filter_array is_even "${arr[@]}")"
echo "正数: $(filter_array is_positive "${arr[@]}")"

2.3.2 事件驱动

#!/bin/bash

# 事件系统
declare -A event_handlers

# 注册事件处理器
function on() {
    local event="$1"
    local handler="$2"
    event_handlers["$event"]="$handler"
}

# 触发事件
function emit() {
    local event="$1"
    shift
    local args=("$@")
    
    if [[ -n "${event_handlers[$event]}" ]]; then
        ${event_handlers[$event]} "${args[@]}"
    fi
}

# 事件处理器
function on_start() {
    echo "脚本开始执行"
}

function on_complete() {
    echo "脚本执行完成"
}

function on_error() {
    echo "脚本执行出错: $1"
}

# 注册事件
on "start" "on_start"
on "complete" "on_complete"
on "error" "on_error"

# 触发事件
emit "start"

# 执行任务
if ! ls /nonexistent &> /dev/null; then
    emit "error" "目录不存在"
fi

emit "complete"

东巴文理解:回调函数和事件驱动可以实现灵活的程序设计。


三、进程替换

3.1 进程替换语法

3.1.1 基本语法

进程替换语法

语法 说明
<(command) 进程输出替换为文件
>(command) 进程输入替换为文件

示例

#!/bin/bash

# 进程输出替换
# 将命令输出作为文件传递给另一个命令
diff <(ls /dir1) <(ls /dir2)

# 比较两个命令的输出
diff <(echo "a b c") <(echo "a b d")

# 将进程输出作为输入
cat <(echo "Hello") <(echo "World")

# 进程输入替换
# 将命令输入重定向到进程
echo "Hello World" > >(tee output.txt)

# 使用进程替换处理数据
sort <(grep "error" /var/log/syslog) | uniq -c

东巴文理解:进程替换将命令的输出/输入当作文件来处理,非常灵活。

3.1.2 与管道的区别

#!/bin/bash

# 使用管道
ls /dir1 | sort > sorted1.txt
ls /dir2 | sort > sorted2.txt
diff sorted1.txt sorted2.txt

# 使用进程替换(更简洁)
diff <(ls /dir1 | sort) <(ls /dir2 | sort)

# 管道只能单向传递数据
# 进程替换可以多向传递数据

# 示例:同时处理多个数据源
join <(sort file1.txt) <(sort file2.txt)

# 示例:比较远程文件
diff <(ssh user@host1 cat /etc/passwd) <(ssh user@host2 cat /etc/passwd)

3.2 实战应用

3.2.1 数据比较

#!/bin/bash

# 比较两个目录的文件列表
echo "比较目录差异:"
diff -y <(ls -1 /dir1 | sort) <(ls -1 /dir2 | sort)

# 比较两个配置文件(去除注释和空行)
echo -e "\n比较配置文件:"
diff <(grep -v '^#' /etc/apache2/apache2.conf | grep -v '^$') \
     <(grep -v '^#' /etc/apache2/apache2.conf.bak | grep -v '^$')

# 比较两个进程的环境变量
echo -e "\n比较进程环境变量:"
diff <(cat /proc/$PID1/environ | tr '\0' '\n' | sort) \
     <(cat /proc/$PID2/environ | tr '\0' '\n' | sort)

# 比较两个数据库表
echo -e "\n比较数据库表:"
diff <(mysql -e "SELECT * FROM table1" db1 | sort) \
     <(mysql -e "SELECT * FROM table2" db2 | sort)

3.2.2 数据合并

#!/bin/bash

# 合并多个数据源
cat <(echo "=== 文件1 ===") \
    <(cat file1.txt) \
    <(echo -e "\n=== 文件2 ===") \
    <(cat file2.txt) \
    > merged.txt

# 使用join合并数据
join -1 1 -2 1 \
     <(sort file1.txt) \
     <(sort file2.txt) \
     > joined.txt

# 使用paste合并数据
paste <(cut -d: -f1,3 /etc/passwd) \
      <(cut -d: -f1,7 /etc/passwd) \
      > combined.txt

# 合并多个日志文件并按时间排序
sort -k4 \
     <(cat /var/log/nginx/access.log) \
     <(cat /var/log/nginx/access.log.1) \
     > merged_logs.txt

3.2.3 数据处理

#!/bin/bash

# 处理多个数据源
awk 'NR==FNR{a[$1]=$2; next} {print $1, a[$1], $2}' \
    <(cat file1.txt) \
    <(cat file2.txt)

# 使用进程替换进行复杂处理
while read -r line; do
    # 处理每一行
    echo "处理: $line"
done < <(grep "error" /var/log/syslog | sort | uniq)

# 并行处理多个文件
for file in *.txt; do
    process_file < <(cat "$file") > "${file}.processed" &
done
wait

# 使用进程替换进行数据验证
comm -23 \
     <(sort required_packages.txt) \
     <(dpkg -l | awk '{print $2}' | sort) \
     > missing_packages.txt

四、信号处理

4.1 信号基础

4.1.1 常用信号

常用信号列表

信号 编号 说明 默认行为
SIGHUP 1 挂起 终止进程
SIGINT 2 中断(Ctrl+C) 终止进程
SIGQUIT 3 退出(Ctrl+\) 终止进程并转储
SIGKILL 9 强制终止 终止进程
SIGTERM 15 正常终止 终止进程
SIGCONT 18 继续 恢复进程
SIGSTOP 19 停止 停止进程
SIGTSTP 20 终端停止(Ctrl+Z) 停止进程

查看信号列表

# 查看所有信号
kill -l

# 查看信号编号
kill -l SIGTERM  # 输出: 15
kill -l 15       # 输出: TERM

发送信号

# 发送信号给进程
kill -SIGTERM 1234
kill -15 1234
kill 1234  # 默认发送SIGTERM

# 强制终止进程
kill -SIGKILL 1234
kill -9 1234

# 发送信号给进程组
kill -SIGTERM -1234  # 负数表示进程组

# 使用pkill发送信号
pkill -SIGTERM nginx
pkill -f "nginx.*worker"

# 使用killall发送信号
killall -SIGTERM nginx

东巴文理解:信号是进程间通信的一种方式,可以控制进程的行为。

4.2 trap命令

4.2.1 基本用法

trap语法

trap '命令' 信号列表

示例

#!/bin/bash

# 捕获SIGINT信号(Ctrl+C)
trap 'echo "捕获到SIGINT信号"; exit 1' SIGINT

# 捕获多个信号
trap 'echo "捕获到信号"; exit 1' SIGINT SIGTERM SIGHUP

# 忽略信号
trap '' SIGINT  # 忽略Ctrl+C

# 恢复信号默认行为
trap - SIGINT

# 捕获EXIT信号(脚本退出时执行)
trap 'echo "脚本退出"; rm -f /tmp/tempfile' EXIT

# 创建临时文件
temp_file=$(mktemp)
echo "临时文件: $temp_file"

# 脚本结束时自动删除临时文件
trap 'rm -f "$temp_file"' EXIT

# 使用函数处理信号
function cleanup() {
    echo "清理资源..."
    rm -f /tmp/tempfile
    exit 1
}

trap cleanup SIGINT SIGTERM

4.2.2 实战案例

守护脚本

#!/bin/bash
#===============================================================================
# 脚本名称:daemon.sh
# 脚本功能:守护进程脚本
# 作者:东巴文
#===============================================================================

PID_FILE="/var/run/daemon.pid"
LOG_FILE="/var/log/daemon.log"

# 清理函数
function cleanup() {
    echo "$(date): 守护进程停止" >> "$LOG_FILE"
    rm -f "$PID_FILE"
    exit 0
}

# 捕获信号
trap cleanup SIGTERM SIGINT SIGHUP

# 检查是否已经在运行
if [[ -f "$PID_FILE" ]]; then
    pid=$(cat "$PID_FILE")
    if kill -0 "$pid" 2>/dev/null; then
        echo "守护进程已经在运行 (PID: $pid)"
        exit 1
    else
        rm -f "$PID_FILE"
    fi
fi

# 写入PID
echo $$ > "$PID_FILE"
echo "$(date): 守护进程启动 (PID: $$)" >> "$LOG_FILE"

# 主循环
while true; do
    # 执行任务
    echo "$(date): 执行任务..." >> "$LOG_FILE"
    
    # 检查某个服务是否运行
    if ! systemctl is-active --quiet nginx; then
        echo "$(date): nginx未运行,尝试启动" >> "$LOG_FILE"
        systemctl start nginx
    fi
    
    # 等待
    sleep 60
done

优雅退出脚本

#!/bin/bash
#===============================================================================
# 脚本名称:graceful_exit.sh
# 脚本功能:优雅退出脚本
# 作者:东巴文
#===============================================================================

# 临时文件数组
declare -a temp_files

# 清理函数
function cleanup() {
    echo -e "\n正在清理资源..."
    
    # 删除临时文件
    for file in "${temp_files[@]}"; do
        if [[ -f "$file" ]]; then
            echo "删除临时文件: $file"
            rm -f "$file"
        fi
    done
    
    # 关闭数据库连接
    if [[ -n "$db_conn" ]]; then
        echo "关闭数据库连接"
        # 关闭连接的代码
    fi
    
    # 停止后台进程
    if [[ -n "$bg_pid" ]]; then
        echo "停止后台进程: $bg_pid"
        kill "$bg_pid" 2>/dev/null
    fi
    
    echo "清理完成"
    exit 1
}

# 捕获信号
trap cleanup SIGINT SIGTERM SIGHUP

# 创建临时文件
temp_file1=$(mktemp)
temp_files+=("$temp_file1")
echo "创建临时文件: $temp_file1"

temp_file2=$(mktemp)
temp_files+=("$temp_file2")
echo "创建临时文件: $temp_file2"

# 启动后台进程
sleep 1000 &
bg_pid=$!
echo "启动后台进程: $bg_pid"

# 主循环
echo "脚本运行中,按Ctrl+C退出"
while true; do
    echo "处理中..."
    sleep 2
done

4.3 信号应用

4.3.1 进程控制

#!/bin/bash
#===============================================================================
# 脚本名称:process_control.sh
# 脚本功能:进程控制脚本
# 作者:东巴文
#===============================================================================

# 启动进程
function start_process() {
    local name="$1"
    local command="$2"
    
    # 检查是否已经在运行
    if pgrep -f "$name" &>/dev/null; then
        echo "进程 $name 已经在运行"
        return 1
    fi
    
    # 启动进程
    nohup $command >/dev/null 2>&1 &
    local pid=$!
    
    # 等待进程启动
    sleep 1
    
    if kill -0 "$pid" 2>/dev/null; then
        echo "进程 $name 启动成功 (PID: $pid)"
        return 0
    else
        echo "进程 $name 启动失败"
        return 1
    fi
}

# 停止进程
function stop_process() {
    local name="$1"
    local timeout="${2:-10}"
    
    # 获取进程PID
    local pid=$(pgrep -f "$name")
    
    if [[ -z "$pid" ]]; then
        echo "进程 $name 未运行"
        return 1
    fi
    
    # 发送SIGTERM信号
    kill -TERM "$pid"
    
    # 等待进程退出
    local count=0
    while kill -0 "$pid" 2>/dev/null; do
        sleep 1
        ((count++))
        
        if ((count >= timeout)); then
            echo "进程 $name 未响应,强制终止"
            kill -KILL "$pid"
            break
        fi
    done
    
    echo "进程 $name 已停止"
    return 0
}

# 重启进程
function restart_process() {
    local name="$1"
    local command="$2"
    
    stop_process "$name"
    sleep 2
    start_process "$name" "$command"
}

# 查看进程状态
function status_process() {
    local name="$1"
    
    if pgrep -f "$name" &>/dev/null; then
        echo "进程 $name 正在运行"
        pgrep -af "$name"
        return 0
    else
        echo "进程 $name 未运行"
        return 1
    fi
}

# 主菜单
case "$1" in
    start)
        start_process "$2" "$3"
        ;;
    stop)
        stop_process "$2"
        ;;
    restart)
        restart_process "$2" "$3"
        ;;
    status)
        status_process "$2"
        ;;
    *)
        echo "用法: $0 {start|stop|restart|status} 进程名 [命令]"
        exit 1
        ;;
esac

4.3.2 超时控制

#!/bin/bash
#===============================================================================
# 脚本名称:timeout_control.sh
# 脚本功能:超时控制脚本
# 作者:东巴文
#===============================================================================

# 超时执行命令
function timeout_command() {
    local timeout="$1"
    shift
    local command=("$@")
    
    # 执行命令
    "${command[@]}" &
    local pid=$!
    
    # 等待命令完成或超时
    local count=0
    while kill -0 "$pid" 2>/dev/null; do
        sleep 1
        ((count++))
        
        if ((count >= timeout)); then
            echo "命令超时,终止进程"
            kill -TERM "$pid"
            sleep 1
            
            # 如果进程还在运行,强制终止
            if kill -0 "$pid" 2>/dev/null; then
                kill -KILL "$pid"
            fi
            
            return 124  # 超时退出码
        fi
    done
    
    # 获取命令退出码
    wait "$pid"
    return $?
}

# 测试
echo "测试1: 正常命令"
timeout_command 5 sleep 2
echo "退出码: $?"

echo -e "\n测试2: 超时命令"
timeout_command 3 sleep 10
echo "退出码: $?"

# 使用timeout命令(更简单)
echo -e "\n测试3: 使用timeout命令"
timeout 3 sleep 10
echo "退出码: $?"

五、性能优化

5.1 脚本优化

5.1.1 减少外部命令

避免不必要的管道

#!/bin/bash

# 不推荐:使用多个管道
result=$(cat file.txt | grep "pattern" | awk '{print $1}' | sort | uniq)

# 推荐:使用单个命令
result=$(awk '/pattern/ {print $1}' file.txt | sort -u)

# 不推荐:使用cat
result=$(cat file.txt | grep "pattern")

# 推荐:直接读取文件
result=$(grep "pattern" file.txt)

# 不推荐:使用多个命令
lines=$(wc -l < file.txt)
words=$(wc -w < file.txt)
chars=$(wc -c < file.txt)

# 推荐:使用单个命令
read lines words chars <<< $(wc file.txt)

5.1.2 使用内置功能

使用Shell内置功能

#!/bin/bash

# 不推荐:使用外部命令
basename=$(basename "$path")
dirname=$(dirname "$path")

# 推荐:使用参数扩展
basename="${path##*/}"
dirname="${path%/*}"

# 不推荐:使用expr
result=$(expr $a + $b)

# 推荐:使用算术扩展
result=$((a + b))

# 不推荐:使用sed
result=$(echo "$str" | sed 's/old/new/')

# 推荐:使用参数扩展
result="${str/old/new}"

# 不推荐:使用tr
result=$(echo "$str" | tr '[:lower:]' '[:upper:]')

# 推荐:使用参数扩展
result="${str^^}"

# 不推荐:使用外部命令
length=$(echo -n "$str" | wc -c)

# 推荐:使用参数扩展
length=${#str}

东巴文提示:使用Shell内置功能可以避免创建子进程,提高性能。

5.2 并行处理

5.2.1 后台任务

#!/bin/bash

# 串行处理(慢)
for file in *.txt; do
    process_file "$file"
done

# 并行处理(快)
for file in *.txt; do
    process_file "$file" &
done
wait

# 限制并发数
max_jobs=4
count=0

for file in *.txt; do
    process_file "$file" &
    
    ((count++))
    if ((count >= max_jobs)); then
        wait -n  # 等待任意一个后台任务完成
        ((count--))
    fi
done
wait

# 使用xargs并行处理
find . -name "*.txt" -print0 | xargs -0 -P 4 -I {} process_file "{}"

# 使用GNU parallel
find . -name "*.txt" | parallel -j 4 process_file {}

5.2.2 进程池

#!/bin/bash
#===============================================================================
# 脚本名称:process_pool.sh
# 脚本功能:进程池实现
# 作者:东巴文
#===============================================================================

# 进程池大小
POOL_SIZE=4

# 任务队列
declare -a task_queue

# 添加任务
function add_task() {
    task_queue+=("$1")
}

# 执行任务
function execute_task() {
    local task="$1"
    echo "执行任务: $task"
    # 模拟耗时操作
    sleep 2
    echo "完成任务: $task"
}

# 进程池
function process_pool() {
    local running=0
    
    for task in "${task_queue[@]}"; do
        # 等待空闲槽位
        while ((running >= POOL_SIZE)); do
            wait -n
            ((running--))
        done
        
        # 执行任务
        execute_task "$task" &
        ((running++))
    done
    
    # 等待所有任务完成
    wait
}

# 添加任务
for i in {1..10}; do
    add_task "任务$i"
done

# 执行
echo "开始处理任务..."
start_time=$(date +%s)

process_pool

end_time=$(date +%s)
echo "所有任务完成,耗时: $((end_time - start_time)) 秒"

5.3 内存优化

5.3.1 减少内存占用

#!/bin/bash

# 不推荐:读取整个文件到内存
content=$(cat large_file.txt)

# 推荐:逐行处理
while IFS= read -r line; do
    process_line "$line"
done < large_file.txt

# 不推荐:使用数组存储大量数据
declare -a large_array
while IFS= read -r line; do
    large_array+=("$line")
done < large_file.txt

# 推荐:使用文件或数据库存储
while IFS= read -r line; do
    echo "$line" >> temp_file.txt
done < large_file.txt

# 不推荐:使用大量变量
var1="value1"
var2="value2"
# ... 100个变量

# 推荐:使用数组
declare -a vars=("value1" "value2" ...)

5.3.2 及时释放资源

#!/bin/bash

# 及时关闭文件描述符
exec 3< file.txt
while IFS= read -r -u 3 line; do
    process_line "$line"
done
exec 3<&-  # 关闭文件描述符

# 及时删除临时文件
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT

# 使用临时文件
echo "data" > "$temp_file"
process_file "$temp_file"

# 及时清空大变量
large_data=$(cat large_file.txt)
process_data "$large_data"
unset large_data  # 释放内存

# 使用函数限制变量作用域
function process_chunk() {
    local data="$1"
    # 处理数据
    # 函数结束后,局部变量自动释放
}

process_chunk "$chunk"

六、调试技巧

6.1 调试方法

6.1.1 调试选项

Shell调试选项

选项 说明
-n 只读取脚本,不执行(检查语法)
-v 显示读取的每一行
-x 显示执行的每一行
-e 命令出错时立即退出
-u 使用未定义变量时报错
-o pipefail 管道中任何命令出错都返回错误

示例

#!/bin/bash

# 检查语法
bash -n script.sh

# 显示执行的每一行
bash -x script.sh

# 显示读取的每一行
bash -v script.sh

# 组合使用
bash -xv script.sh

# 在脚本中启用调试
#!/bin/bash
set -x  # 启用调试
# ... 代码 ...
set +x  # 关闭调试

# 严格模式
#!/bin/bash
set -euo pipefail

6.1.2 调试函数

#!/bin/bash

# 调试函数
function debug() {
    [[ $DEBUG -eq 1 ]] && echo "[DEBUG] $*" >&2
}

# 使用调试函数
DEBUG=1

function process_file() {
    local file="$1"
    debug "处理文件: $file"
    
    if [[ ! -f "$file" ]]; then
        debug "文件不存在: $file"
        return 1
    fi
    
    debug "文件大小: $(stat -c %s "$file")"
    # ... 处理文件 ...
}

process_file "/etc/passwd"

# 跟踪函数调用
function trace() {
    echo "[TRACE] ${FUNCNAME[1]} called with: $*" >&2
}

function my_function() {
    trace "$@"
    # ... 函数代码 ...
}

6.2 错误处理

6.2.1 错误检测

#!/bin/bash

# 严格模式
set -euo pipefail

# 错误处理函数
function error_handler() {
    local line_no=$1
    local error_code=$?
    
    echo "错误发生在第 $line_no 行"
    echo "错误代码: $error_code"
    
    # 清理资源
    cleanup
    
    exit $error_code
}

# 设置错误处理
trap 'error_handler $LINENO' ERR

# 清理函数
function cleanup() {
    echo "清理资源..."
    rm -f /tmp/tempfile
}

# 测试
echo "开始执行"
false  # 这会触发错误
echo "这行不会执行"

6.2.2 日志记录

#!/bin/bash
#===============================================================================
# 脚本名称:logging.sh
# 脚本功能:日志记录脚本
# 作者:东巴文
#===============================================================================

# 日志文件
LOG_FILE="/var/log/script.log"

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

function log_info() {
    log "INFO" "$@"
}

function log_warning() {
    log "WARNING" "$@"
}

function log_error() {
    log "ERROR" "$@"
}

function log_debug() {
    [[ $DEBUG -eq 1 ]] && log "DEBUG" "$@"
}

# 使用日志
log_info "脚本开始执行"
log_debug "调试信息"
log_warning "警告信息"
log_error "错误信息"

# 带错误处理的日志
function safe_command() {
    local description="$1"
    shift
    local command=("$@")
    
    log_info "执行: $description"
    
    if "${command[@]}" 2>&1 | tee -a "$LOG_FILE"; then
        log_info "成功: $description"
        return 0
    else
        log_error "失败: $description"
        return 1
    fi
}

# 使用
safe_command "创建目录" mkdir -p /path/to/dir
safe_command "复制文件" cp source.txt dest.txt

七、本章小结

7.1 核心要点

✅ 掌握高级变量操作 ✅ 学会函数库和递归 ✅ 理解进程替换技术 ✅ 掌握信号处理机制 ✅ 学会性能优化技巧 ✅ 掌握调试方法

7.2 验证清单

完成本章学习后,请确认您能够:

  • 使用高级变量操作
  • 创建和使用函数库
  • 使用进程替换处理数据
  • 捕获和处理信号
  • 优化脚本性能
  • 调试Shell脚本

东巴文(db-w.cn) - 让Linux学习更简单