高级特性
本章介绍 Shell 脚本的高级特性,包括输入输出重定向、进程替换、子 Shell、信号处理、并行执行等。掌握这些特性可以让你编写更强大、更高效的脚本。
输入输出重定向
Shell 使用文件描述符(File Descriptor)来管理输入输出。每个进程有三个标准文件描述符:
| 文件描述符 | 名称 | 默认设备 |
|---|---|---|
| 0 | 标准输入(stdin) | 键盘 |
| 1 | 标准输出(stdout) | 屏幕 |
| 2 | 标准错误(stderr) | 屏幕 |
输出重定向
覆盖写入 >
# 将标准输出重定向到文件
ls > output.txt
# 将标准错误重定向到文件
ls /nonexistent 2> error.txt
# 将标准输出和标准错误分别重定向
ls /tmp /nonexistent > output.txt 2> error.txt
# 将标准输出和标准错误重定向到同一文件
ls /tmp /nonexistent > all.txt 2>&1
ls /tmp /nonexistent &> all.txt # Bash 简写
追加写入 >>
# 追加标准输出
echo "Line 1" >> log.txt
echo "Line 2" >> log.txt
# 追加标准错误
command 2>> error.log
# 追加标准输出和标准错误
command >> all.log 2>&1
command &>> all.log
输入重定向
# 从文件读取输入
wc -l < file.txt
# Here Document(多行输入)
cat << EOF
Line 1
Line 2
Line 3
EOF
# Here String(单行输入)
grep "pattern" <<< "search in this string"
# 使用变量
name="World"
cat <<< "Hello, $name"
Here Document 详解
Here Document 允许在脚本中嵌入多行文本。
# 基本语法
command << DELIMITER
text
DELIMITER
# 示例:创建配置文件
cat > config.ini << EOF
[database]
host = localhost
port = 3306
user = root
password = secret
EOF
# 禁用变量替换(使用单引号)
cat << 'EOF'
$HOME 不会被替换
$(pwd) 也不会执行
EOF
# 去除前导制表符(使用 <<-)
cat <<- EOF
这行的制表符会被去除
这行也是
EOF
# 使用变量
file="data.txt"
cat << EOF
Processing file: $file
Current directory: $(pwd)
EOF
文件描述符操作
# 打开文件描述符
exec 3> output.txt # 打开文件描述符 3 用于写入
echo "Hello" >&3
exec 3>&- # 关闭文件描述符 3
# 读取文件描述符
exec 3< input.txt
read -u 3 line
echo $line
exec 3<&-
# 复制文件描述符
exec 3>&1 # 将 fd 3 指向 stdout
exec > output.txt # 将 stdout 重定向到文件
echo "To file"
exec >&3 # 恢复 stdout
exec 3>&- # 关闭 fd 3
# 同时重定向输入和输出
exec 3< input.txt 4> output.txt
read -u 3 line
echo "Processed: $line" >&4
exec 3<&- 4>&-
管道
管道将一个命令的标准输出连接到另一个命令的标准输入。
基本用法
# 简单管道
ls | grep ".txt"
# 多级管道
cat file.txt | grep "error" | sort | uniq -c
# 管道与 tee(同时输出到文件和屏幕)
ls | tee output.txt | grep ".txt"
管道中的退出状态
默认情况下,管道的退出状态是最后一个命令的状态。
# 获取管道中所有命令的退出状态
false | true | false
echo "${PIPESTATUS[@]}" # 输出:1 0 1
# 设置 pipefail 选项
set -o pipefail
false | true | false
echo $? # 输出:1(最后一个非零状态)
命名管道(FIFO)
命名管道是一种特殊文件,允许不相关的进程进行通信。
# 创建命名管道
mkfifo mypipe
# 在一个终端写入
echo "Hello" > mypipe
# 在另一个终端读取
cat < mypipe
# 删除命名管道
rm mypipe
进程替换
进程替换允许将命令的输出或输入作为文件名使用。
输出替换 >(...)
# 将输出同时发送到多个命令
echo "Hello" | tee >(grep -o "H" > h.txt) >(grep -o "e" > e.txt)
# 比较两个命令的输出
diff <(ls dir1) <(ls dir2)
# 使用进程替换作为文件参数
tar -cf >(gzip > archive.tar.gz) /path/to/files
输入替换 <(...)
# 比较两个排序后的文件
diff <(sort file1.txt) <(sort file2.txt)
# 合并多个文件并排序
sort -m <(sort file1.txt) <(sort file2.txt) <(sort file3.txt)
# 使用进程替换读取多个输入
join <(sort file1.txt) <(sort file2.txt)
子 Shell
子 Shell 是当前 Shell 的子进程,它继承了父 Shell 的环境,但修改不会影响父 Shell。
创建子 Shell
# 使用括号创建子 Shell
(cd /tmp; ls)
# 当前目录不会改变
# 子 Shell 中的变量修改不影响父 Shell
x=1
(x=2; echo "In subshell: $x")
echo "In parent: $x"
# 输出:
# In subshell: 2
# In parent: 1
子 Shell 的用途
# 在独立环境中执行命令
(
cd /tmp
rm -rf temp_*
mkdir temp_work
# ... 其他操作 ...
)
# 不影响当前目录和环境
# 并行执行任务
(
echo "Task 1 starting"
sleep 2
echo "Task 1 done"
) &
(
echo "Task 2 starting"
sleep 1
echo "Task 2 done"
) &
wait
echo "All tasks done"
后台任务
基本用法
# 在后台运行命令
sleep 100 &
# 查看后台任务
jobs
# 将后台任务调到前台
fg %1
# 将暂停的任务放到后台
bg %1
nohup
nohup 命令让任务在退出终端后继续运行。
# 使用 nohup 运行命令
nohup ./long_running_script.sh &
# 输出默认保存到 nohup.out
nohup ./script.sh > output.log 2>&1 &
disown
disown 将任务从当前 Shell 的任务列表中移除,使其在 Shell 退出后继续运行。
# 启动后台任务
sleep 100 &
# [1] 12345
# 从任务列表移除
disown %1
# 或
disown 12345
信号处理
信号是进程间通信的一种机制,Shell 脚本可以使用 trap 命令捕获和处理信号。
常见信号
| 信号 | 编号 | 含义 |
|---|---|---|
| SIGINT | 2 | Ctrl+C 中断 |
| SIGTERM | 15 | 终止请求 |
| SIGKILL | 9 | 强制终止(不可捕获) |
| SIGHUP | 1 | 挂起(终端关闭) |
| SIGQUIT | 3 | Ctrl+\ 退出 |
| SIGUSR1 | 10 | 用户自定义信号 1 |
| SIGUSR2 | 12 | 用户自定义信号 2 |
trap 命令
# 基本语法
trap 'commands' signals
# 捕获 SIGINT(Ctrl+C)
trap 'echo "Interrupted"; exit 1' INT
# 捕获多个信号
trap 'echo "Signal received"; cleanup; exit' INT TERM
# 忽略信号
trap '' INT # 忽略 Ctrl+C
# 恢复默认行为
trap - INT
清理脚本
#!/bin/bash
temp_file="/tmp/script_$$"
cleanup() {
echo "Cleaning up..."
rm -f "$temp_file"
exit
}
# 捕获退出信号
trap cleanup EXIT INT TERM
# 创建临时文件
touch "$temp_file"
# 主逻辑
echo "Working..."
sleep 10
# 正常退出时也会执行 cleanup
调试信号
#!/bin/bash
# 打印调试信息
trap 'echo "Line $LINENO: $BASH_COMMAND"' DEBUG
# 主脚本
x=1
y=2
echo $((x + y))
并行执行
Shell 提供了多种方式实现并行执行。
使用 & 和 wait
#!/bin/bash
process_file() {
local file=$1
echo "Processing $file"
sleep 1
echo "Done with $file"
}
# 并行处理
for file in *.txt; do
process_file "$file" &
done
# 等待所有后台任务完成
wait
echo "All files processed"
限制并发数
#!/bin/bash
max_jobs=4
job_count=0
process_file() {
local file=$1
echo "Processing $file"
sleep 2
}
for file in *.txt; do
process_file "$file" &
((job_count++))
if ((job_count >= max_jobs)); then
wait -n # 等待任意一个后台任务完成
((job_count--))
fi
done
wait
echo "All done"
使用 xargs 并行
# 使用 -P 选项指定并行数
find . -name "*.txt" | xargs -P 4 -I {} process_file {}
# 示例:并行压缩
find . -name "*.log" | xargs -P 4 -I {} gzip {}
使用 GNU parallel
# 安装
# apt install parallel
# 基本用法
parallel echo ::: a b c d
# 处理文件
parallel gzip ::: *.txt
# 从标准输入读取
find . -name "*.txt" | parallel gzip
# 指定并行数
parallel -j 4 process_file {} ::: *.txt
进程管理
查看进程
# 查看当前 Shell 的 PID
echo $$
# 查看进程信息
ps aux | grep process_name
# 查看进程树
pstree
# 动态监控
top
htop
终止进程
# 发送信号
kill PID
kill -9 PID # SIGKILL
kill -TERM PID # SIGTERM
# 按名称终止
killall process_name
pkill process_name
# 终止匹配条件的进程
pkill -f "pattern"
进程优先级
# 以较低优先级运行
nice -n 10 ./script.sh
# 修改运行中进程的优先级
renice -n 5 -p PID
# 以更高优先级运行(需要 root)
sudo nice -n -5 ./script.sh
Here Document 和 Here String
Here Document 进阶
# 使用变量和命令替换
cat << EOF
Current user: $USER
Current directory: $(pwd)
Date: $(date)
EOF
# 禁用替换
cat << 'EOF'
$USER will not be expanded
$(pwd) will not execute
EOF
# 使用函数
generate_config() {
cat << EOF
[server]
host = $1
port = $2
EOF
}
generate_config "localhost" 8080
# 嵌套 Here Document
cat << OUTER
This is outer
$(cat << INNER
This is inner
INNER
)
Back to outer
OUTER
Here String
# 基本用法
grep "pattern" <<< "search string"
# 使用变量
file="test.txt"
cat <<< "Creating $file"
# 传递多行
awk '{print NR, $0}' <<< "line1
line2
line3"
exec 命令
exec 命令用于替换当前 Shell 或操作文件描述符。
替换当前 Shell
# 用新命令替换当前 Shell
exec bash # 启动新的 Bash
exec zsh # 启动 Zsh
# 执行脚本后退出
exec ./script.sh
# 脚本执行完毕后 Shell 会退出
操作文件描述符
# 重定向所有输出
exec > output.log 2>&1
echo "This goes to the file"
ls /nonexistent # 错误也到文件
# 恢复标准输出
exec 1>&2 # 将 stdout 重定向到 stderr
# 打开文件用于读写
exec 3<> file.txt
read -u 3 line
echo "Modified: $line" >&3
exec 3>&-
实用高级脚本示例
带进度条的脚本
#!/bin/bash
show_progress() {
local current=$1
local total=$2
local width=50
local percent=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r["
printf "%${filled}s" | tr ' ' '='
printf "%${empty}s" | tr ' ' ' '
printf "] %3d%% (%d/%d)" $percent $current $total
}
total=100
for i in $(seq 1 $total); do
show_progress $i $total
sleep 0.05
done
echo
带超时的命令
#!/bin/bash
run_with_timeout() {
local timeout=$1
shift
local cmd=("$@")
"${cmd[@]}" &
local pid=$!
(
sleep $timeout
if kill -0 $pid 2>/dev/null; then
kill $pid 2>/dev/null
fi
) &
local watchdog=$!
wait $pid
local status=$?
kill $watchdog 2>/dev/null
return $status
}
# 使用:5 秒超时
run_with_timeout 5 sleep 10 && echo "Success" || echo "Timeout or failed"
并行下载脚本
#!/bin/bash
download_file() {
local url=$1
local output=$(basename "$url")
echo "Downloading $output..."
curl -s -o "$output" "$url" && echo "Done: $output" || echo "Failed: $output"
}
urls=(
"https://example.com/file1.txt"
"https://example.com/file2.txt"
"https://example.com/file3.txt"
)
max_parallel=2
running=0
for url in "${urls[@]}"; do
download_file "$url" &
((running++))
if ((running >= max_parallel)); then
wait -n
((running--))
fi
done
wait
echo "All downloads completed"
日志轮转脚本
#!/bin/bash
log_file="/var/log/app.log"
max_size=10485760 # 10MB
max_backups=5
rotate_log() {
local file=$1
local max=$2
# 检查文件大小
if [[ ! -f "$file" ]]; then
return
fi
local size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
if (( size > max_size )); then
# 删除最旧的备份
if [[ -f "${file}.${max_backups}" ]]; then
rm "${file}.${max_backups}"
fi
# 轮转备份
for ((i=max_backups-1; i>=1; i--)); do
if [[ -f "${file}.${i}" ]]; then
mv "${file}.${i}" "${file}.$((i+1))"
fi
done
# 轮转当前日志
mv "$file" "${file}.1"
touch "$file"
echo "Log rotated at $(date)" >> "$file"
fi
}
rotate_log "$log_file" "$max_size"
小结
本章介绍了 Shell 脚本的高级特性:
- 输入输出重定向:文件描述符、Here Document、Here String
- 管道:连接命令的输出和输入
- 进程替换:将命令输出作为文件使用
- 子 Shell:在独立环境中执行命令
- 后台任务:nohup、disown
- 信号处理:trap 命令捕获和处理信号
- 并行执行:&、wait、xargs、GNU parallel
- 进程管理:查看、终止、优先级
- exec 命令:替换 Shell 和操作文件描述符
掌握这些高级特性可以让你编写更强大、更高效的 Shell 脚本。下一章将介绍调试和测试技巧。