跳到主要内容

高级特性

本章介绍 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 命令捕获和处理信号。

常见信号

信号编号含义
SIGINT2Ctrl+C 中断
SIGTERM15终止请求
SIGKILL9强制终止(不可捕获)
SIGHUP1挂起(终端关闭)
SIGQUIT3Ctrl+\ 退出
SIGUSR110用户自定义信号 1
SIGUSR212用户自定义信号 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 脚本。下一章将介绍调试和测试技巧。