在Linux系统管理和自动化脚本编写中,循环和函数是Shell编程的两大核心构件。循环让我们能够高效处理重复性任务,而函数则将复杂逻辑封装为可重用的代码块。实际工作中,我经常看到新手编写的脚本冗长且难以维护,主要原因就是没有充分利用这两种结构。
Shell脚本中的循环主要有三种形式:for循环、while循环和until循环。每种循环都有其特定的适用场景:
函数则可以将一段功能代码封装起来,通过传递参数实现不同场景的复用。合理使用函数能让脚本结构更清晰,也更易于调试和维护。
最基本的for循环格式如下:
bash复制for 变量 in 列表
do
命令序列
done
这里的"列表"可以是显式列举的值,也可以是命令替换、文件名扩展等生成的序列。例如处理当前目录所有.txt文件:
bash复制for file in *.txt
do
echo "Processing $file..."
wc -l $file
done
bash还支持类似C语言的for循环语法,这在需要精确控制循环变量时特别有用:
bash复制for ((i=0; i<10; i++))
do
echo "Iteration $i"
done
这种形式在处理数值范围时比传统的seq命令更直观,性能也更好。我在处理大量数据分片时经常使用这种结构。
在循环中,我们可以使用break和continue控制流程:
一个实用的例子是查找文件:
bash复制for file in *
do
if [[ ! -f $file ]]; then
continue # 跳过非普通文件
fi
if grep -q "target_pattern" "$file"; then
echo "Found in $file"
break # 找到后立即退出
fi
done
提示:在嵌套循环中,break和continue默认只影响当前层循环。如果需要跳出多层循环,可以使用break n语法,其中n表示要跳出的层数。
while循环在条件为真时持续执行,特别适合处理不确定次数的迭代。例如读取文件内容:
bash复制while IFS= read -r line
do
echo "Processing line: $line"
# 复杂处理逻辑...
done < input.txt
这里IFS=和-r参数是为了保留行首尾空格和反斜杠转义,是处理文本文件时的最佳实践。
until循环与while逻辑相反,在条件为假时执行。典型的应用场景是等待某个条件满足:
bash复制until ping -c1 example.com &>/dev/null
do
echo "Waiting for network..."
sleep 5
done
echo "Network is up!"
这种结构在系统启动脚本和服务健康检查中非常常见。
循环体内部也可以使用重定向,例如将整个循环输出重定向到文件:
bash复制while read -r host
do
ping -c1 "$host"
done < hostlist.txt > ping_results.log 2>&1
更高级的用法是使用exec在循环内动态改变重定向目标,这在生成复杂报告时特别有用。
Shell函数的两种定义方式:
bash复制# 方式一:传统风格
function_name() {
# 函数体
}
# 方式二:类似其他语言的风格
function function_name {
# 函数体
}
函数调用直接使用函数名即可,参数通过位置变量传递:
bash复制greet() {
echo "Hello, $1!"
}
greet "World" # 输出:Hello, World!
Shell函数参数通过$1, $2...$n访问,$#表示参数个数,$*和$@表示所有参数。返回值方面:
一个实用的参数处理示例:
bash复制process_files() {
local verbose=0
local pattern="*.txt"
# 参数解析
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) verbose=1 ;;
-p|--pattern) pattern="$2"; shift ;;
*) break ;;
esac
shift
done
# 剩余参数作为文件处理
for file in $pattern; do
[[ $verbose -eq 1 ]] && echo "Processing $file"
# 实际处理逻辑...
done
}
默认情况下,Shell变量都是全局的。为了避免污染全局命名空间,函数内部变量应该使用local声明:
bash复制count_files() {
local dir="$1"
local count=0
for file in "$dir"/*; do
[[ -f "$file" ]] && ((count++))
done
echo "$count"
}
如果不使用local,count变量在函数调用后仍然存在,可能导致意外的副作用。
将循环封装在函数中可以创建强大的处理单元。例如批量重命名函数:
bash复制batch_rename() {
local prefix="$1"
local suffix="$2"
local i=1
for file in *; do
if [[ -f "$file" ]]; then
mv "$file" "${prefix}${i}${suffix}"
((i++))
fi
done
}
将重复逻辑提取为函数后,在循环中调用可以大幅提升代码可读性:
bash复制process_item() {
local item="$1"
# 复杂处理逻辑...
echo "Processed $item"
}
for item in "${items[@]}"; do
process_item "$item" &
done
wait # 等待所有后台进程完成
这个例子还展示了并行处理的技巧,通过&将函数调用放入后台,可以加速批量处理。
Shell函数也支持递归调用,虽然不如其他语言高效,但在处理目录树等递归结构时很有用:
bash复制traverse_dir() {
local dir="$1"
local indent="$2"
echo "${indent}Directory: $dir"
for item in "$dir"/*; do
if [[ -d "$item" ]]; then
traverse_dir "$item" "$indent "
elif [[ -f "$item" ]]; then
echo "${indent} File: ${item##*/}"
fi
done
}
Shell循环在处理大量数据时可能成为性能瓶颈。一些优化技巧:
例如,统计文件行数的低效与高效写法对比:
bash复制# 低效写法
total=0
for file in *.log; do
lines=$(wc -l < "$file")
total=$((total + lines))
done
# 高效写法
total=$(wc -l *.log | tail -n1 | awk '{print $1}')
调试Shell函数时可以使用以下技术:
一个实用的调试函数模板:
bash复制debug_function() {
set -x # 开启调试
trap 'echo "Error at line $LINENO"; set +x' ERR
# 函数逻辑...
set +x # 关闭调试
trap - ERR
}
健壮的脚本需要妥善处理错误:
例如:
bash复制cleanup() {
echo "Cleaning up temporary files..."
rm -f "$temp_file"
}
main() {
local temp_file=$(mktemp)
trap cleanup EXIT
# 主逻辑...
return 0
}
set -euo pipefail
main "$@"
结合循环和函数处理日志的典型例子:
bash复制#!/bin/bash
analyze_log() {
local log_file="$1"
local threshold="${2:-10}" # 默认阈值10
echo "Analyzing $log_file for errors..."
local error_count=0
while IFS= read -r line; do
if [[ "$line" =~ ERROR|FAIL ]]; then
echo "$line"
((error_count++))
fi
done < "$log_file"
if [[ $error_count -gt $threshold ]]; then
echo "WARNING: High error count ($error_count) in $log_file"
return 1
fi
return 0
}
process_all_logs() {
local log_dir="$1"
local has_errors=0
for log in "$log_dir"/*.log; do
if ! analyze_log "$log"; then
has_errors=1
fi
done
return $has_errors
}
# 主程序
log_directory="/var/log/myapp"
if process_all_logs "$log_directory"; then
echo "All logs processed successfully"
else
echo "Found problematic logs" >&2
exit 1
fi
自动化系统检查脚本示例:
bash复制#!/bin/bash
check_disk() {
local threshold="$1"
local usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ $usage -gt $threshold ]]; then
echo "Disk usage is ${usage}% (threshold: ${threshold}%)"
return 1
fi
return 0
}
check_memory() {
local threshold="$1"
local free_kb=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
local total_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
local percent_used=$((100 - (free_kb * 100) / total_kb))
if [[ $percent_used -gt $threshold ]]; then
echo "Memory usage is ${percent_used}% (threshold: ${threshold}%)"
return 1
fi
return 0
}
monitor_system() {
local disk_threshold=90
local memory_threshold=85
local checks_passed=0
local total_checks=2
check_disk $disk_threshold && ((checks_passed++))
check_memory $memory_threshold && ((checks_passed++))
echo "System check passed ${checks_passed}/${total_checks} tests"
return $((total_checks - checks_passed))
}
# 持续监控
while true; do
if monitor_system; then
echo "$(date): System OK"
else
echo "$(date): System issues detected" >&2
fi
sleep 300 # 5分钟间隔
done
结合循环和函数实现复杂部署逻辑:
bash复制#!/bin/bash
DEPLOY_TARGETS=("web01" "web02" "db01")
BACKUP_DIR="/backups/$(date +%Y%m%d)"
deploy_to_server() {
local server="$1"
echo "Deploying to ${server}..."
# 1. 备份现有配置
ssh "$server" "mkdir -p ${BACKUP_DIR}"
ssh "$server" "cp -a /etc/myapp ${BACKUP_DIR}/etc_myapp"
# 2. 同步新文件
rsync -avz --delete ./dist/ "${server}:/opt/myapp/"
# 3. 重启服务
if ! ssh "$server" "systemctl restart myapp"; then
echo "Failed to restart service on ${server}" >&2
return 1
fi
# 4. 验证部署
local status
status=$(ssh "$server" "curl -s http://localhost:8080/health")
if [[ "$status" != "OK" ]]; then
echo "Health check failed on ${server}: ${status}" >&2
return 1
fi
echo "Deployment to ${server} completed successfully"
return 0
}
rollback_server() {
local server="$1"
echo "Rolling back ${server}..."
ssh "$server" "rm -rf /opt/myapp/*"
ssh "$server" "cp -a ${BACKUP_DIR}/etc_myapp /etc/myapp"
ssh "$server" "systemctl restart myapp"
echo "Rollback on ${server} completed"
}
main() {
local failed_servers=()
for server in "${DEPLOY_TARGETS[@]}"; do
if ! deploy_to_server "$server"; then
failed_servers+=("$server")
fi
done
if [[ ${#failed_servers[@]} -gt 0 ]]; then
echo "Starting rollback on failed servers: ${failed_servers[*]}"
for server in "${failed_servers[@]}"; do
rollback_server "$server"
done
exit 1
fi
echo "Deployment to all servers completed successfully"
exit 0
}
main "$@"
使用GNU parallel或xargs实现并行处理:
bash复制# 使用xargs并行处理
find . -name "*.data" -print0 | xargs -0 -P4 -n1 process_file
# 使用GNU parallel更灵活
parallel -j4 process_file ::: *.data
# 在函数中使用并行
batch_process() {
local files=("$@")
local max_jobs=4
local running=0
for file in "${files[@]}"; do
if [[ $running -ge $max_jobs ]]; then
wait -n
((running--))
fi
process_file "$file" &
((running++))
done
wait # 等待所有后台任务完成
}
Shell允许运行时创建和修改函数,这在需要根据不同条件改变行为时很有用:
bash复制setup_processor() {
local mode="$1"
if [[ "$mode" == "strict" ]]; then
process_item() {
# 严格模式处理逻辑
[[ "$1" =~ ^[A-Z][a-z]+$ ]] || return 1
echo "Processing $1 with strict checks..."
}
else
process_item() {
# 宽松模式处理逻辑
echo "Processing $1 with minimal checks..."
}
fi
}
# 使用示例
setup_processor "strict"
process_item "Hello" # 会成功
process_item "hello" # 会失败
setup_processor "lenient"
process_item "hello" # 会成功
将常用函数组织成可重用的库文件:
~/lib/shell_utils.sh:
bash复制#!/bin/bash
# 日志记录函数
log() {
local level="$1"
local message="$2"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" >&2
}
# 安全文件操作
safe_move() {
local src="$1"
local dest="$2"
if [[ ! -e "$src" ]]; then
log "ERROR" "Source file $src does not exist"
return 1
fi
if [[ -e "$dest" ]]; then
log "WARNING" "Destination $dest already exists, creating backup"
mv "$dest" "${dest}.bak"
fi
mv "$src" "$dest"
}
# 数学计算
add() {
echo $(($1 + $2))
}
# 其他实用函数...
在脚本中引用:
bash复制#!/bin/bash
# 引入函数库
source ~/lib/shell_utils.sh
# 使用库函数
log "INFO" "Script started"
result=$(add 5 3)
log "DEBUG" "Calculation result: $result"
常见问题:循环结束后变量值不符合预期
bash复制# 问题代码
for i in {1..5}; do
data="value$i"
echo "$data"
done
echo "Final data: $data" # 输出value5,可能不是预期结果
解决方案:
修正后的代码:
bash复制# 方案1:明确初始化
final_data=""
for i in {1..5}; do
data="value$i"
echo "$data"
final_data="$data"
done
echo "Final data: $final_data" # 明确知道这是最后一个值
# 方案2:使用数组
data_list=()
for i in {1..5}; do
data_list+=("value$i")
done
echo "All values: ${data_list[@]}"
echo "First item: ${data_list[0]}"
常见问题:混淆return和echo的输出
bash复制# 问题代码
get_count() {
local items=("$@")
echo "${#items[@]}" # 输出到stdout
return 0
}
count=$(get_count a b c) # count获取了echo的输出
echo "Count is $count"
解决方案:
改进方案:
bash复制# 明确的状态和输出
process_items() {
local items=("$@")
# 处理过程...
if [[ ${#items[@]} -eq 0 ]]; then
echo "No items to process" >&2
return 1
fi
echo "${#items[@]}" # 输出处理结果
return 0 # 返回状态
}
# 调用时明确处理输出和状态
if ! result=$(process_items a b c); then
echo "Error processing items" >&2
exit 1
fi
echo "Processed $result items"
常见性能问题及解决方案:
频繁启动外部命令:
大文件逐行读取:
bash复制# 低效
while read line; do
echo "$line" | grep "pattern"
done < large_file
# 高效
grep "pattern" large_file
不必要的子shell:
bash复制# 低效:创建子shell
for i in $(seq 1 100); do
echo "$i"
done
# 高效:使用Shell内置
for i in {1..100}; do
echo "$i"
done
数组操作低效:
bash复制# 低效:频繁追加小数组
arr=()
for i in {1..1000}; do
arr+=("$i")
done
# 高效:一次性生成
mapfile -t arr < <(seq 1 1000)
在循环和函数中处理用户输入时必须验证:
bash复制process_input() {
local input="$1"
# 验证输入只包含字母数字
if [[ ! "$input" =~ ^[[:alnum:]]+$ ]]; then
echo "Invalid input: $input" >&2
return 1
fi
# 处理逻辑...
}
处理文件名时的注意事项:
bash复制safe_process() {
local file="$1"
# 检查文件是否存在且是普通文件
[[ -f "$file" ]] || { echo "Not a regular file: $file" >&2; return 1; }
# 解析规范路径(防止目录遍历攻击)
local resolved_file
resolved_file=$(realpath -e "$file") || return 1
# 检查文件是否在允许的目录下
[[ "$resolved_file" == /safe/directory/* ]] || { echo "Access denied for $resolved_file" >&2; return 1; }
# 实际处理...
}
函数应该以最小必要权限运行:
bash复制backup_database() {
local backup_file="$1"
# 切换到低权限用户执行
if [[ $(id -u) -eq 0 ]]; then
sudo -u backupuser -- "$0" "${FUNCNAME}" "$backup_file"
return $?
fi
# 实际备份逻辑...
pg_dump mydb > "$backup_file"
}
为Shell函数编写测试用例:
bash复制#!/bin/bash
# 被测函数
add() {
echo $(($1 + $2))
}
# 测试框架
run_test() {
local test_name="$1"
local actual="$2"
local expected="$3"
if [[ "$actual" == "$expected" ]]; then
echo "PASS: $test_name"
return 0
else
echo "FAIL: $test_name (expected $expected, got $actual)"
return 1
fi
}
# 测试用例
test_add_positive() {
local result=$(add 2 3)
run_test "${FUNCNAME[0]}" "$result" 5
}
test_add_negative() {
local result=$(add -1 -1)
run_test "${FUNCNAME[0]}" "$result" -2
}
# 运行所有测试
main() {
local failures=0
# 获取所有以test_开头的函数
for test_func in $(declare -F | awk '/test_/ {print $3}'); do
if ! $test_func; then
((failures++))
fi
done
if [[ $failures -eq 0 ]]; then
echo "All tests passed"
return 0
else
echo "$failures tests failed"
return 1
fi
}
main "$@"
测试循环逻辑的几种方法:
示例测试:
bash复制test_process_files() {
# 创建测试文件
temp_dir=$(mktemp -d)
touch "$temp_dir"/file{1..3}.txt
# 捕获输出
local output
output=$(process_files "$temp_dir")
# 验证处理了3个文件
local processed_count=$(grep -c "Processing" <<< "$output")
if [[ $processed_count -eq 3 ]]; then
echo "PASS: processed correct number of files"
else
echo "FAIL: expected to process 3 files, got $processed_count"
fi
# 清理
rm -rf "$temp_dir"
}
根据目标环境选择合适的解释器:
bash复制#!/usr/bin/env bash # 最通用的bash写法
#!/bin/sh # 只使用POSIX shell特性时
如果需要兼容非bash环境,避免使用bash特有特性:
bash复制# bash特有(避免)
array=(a b c)
echo "${array[@]^}" # 首字母大写
# POSIX兼容
for item in a b c; do
printf '%s\n' "$item" | awk '{print toupper(substr($0,1,1)) substr($0,2)}'
done
在脚本开始处检查必要工具:
bash复制check_dependencies() {
local missing=()
for cmd in awk sed grep; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Missing required commands: ${missing[*]}" >&2
return 1
fi
return 0
}
check_dependencies || exit 1
示例:
bash复制# 计算两个数的最大公约数
# 参数:
# $1: 第一个整数
# $2: 第二个整数
# 返回值:
# 输出最大公约数
gcd() {
local a=$1
local b=$2
# 欧几里得算法
while [[ $b -ne 0 ]]; do
local temp=$b
b=$((a % b))
a=$temp
done
echo $a
}
使用trap确保临时文件被清理:
bash复制work_with_tempfile() {
local tempfile
tempfile=$(mktemp) || return 1
# 设置退出时清理
trap 'rm -f "$tempfile"' EXIT
# 使用临时文件...
echo "Processing data" > "$tempfile"
# 可以手动提前清理
rm -f "$tempfile"
trap - EXIT # 取消trap
}
显式管理文件描述符避免泄漏:
bash复制process_with_fifo() {
local fifo
fifo=$(mktemp -u) # 只生成名称
mkfifo "$fifo" || return 1
# 打开文件描述符
exec 3<>"$fifo"
# 后台写入数据
echo "data" >&3 &
# 读取处理
while read -r line <&3; do
echo "Processing: $line"
done
# 清理
exec 3>&- # 关闭描述符
rm "$fifo"
}
测量函数或循环的执行时间:
bash复制time_function() {
local start end
start=$(date +%s.%N)
"$@" # 执行传入的命令
end=$(date +%s.%N)
# 计算并打印耗时
awk -v s="$start" -v e="$end" 'BEGIN {printf "Execution time: %.3f seconds\n", e - s}'
}
# 使用示例
time_function sleep 1
监控脚本内存使用情况:
bash复制monitor_memory() {
local pid=$1
local interval=${2:-1} # 默认1秒间隔
while ps -p "$pid" >/dev/null; do
ps -o rss= -p "$pid" | awk "{print \"Memory usage: \"\$1\" KB\"}"
sleep "$interval"
done
}
# 使用示例
monitor_memory $$ & # 监控当前脚本
monitor_pid=$!
# 主脚本逻辑...
kill "$monitor_pid" # 停止监控
实现分级的日志记录:
bash复制log() {
local level=$1
shift
local message="$*"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
DEBUG) if [[ $VERBOSE -eq 1 ]]; then echo "[$timestamp] [DEBUG] $message" >&2; fi ;;
INFO) echo "[$timestamp] [INFO] $message" >&2 ;;
WARN) echo "[$timestamp] [WARN] $message" >&2 ;;
ERROR) echo "[$timestamp] [ERROR] $message" >&2 ;;
*) echo "[$timestamp] [UNKNOWN] $message" >&2 ;;
esac
}
# 使用示例
VERBOSE=1
log DEBUG "Detailed debug information"
log INFO "Starting processing"
log ERROR "Something went wrong"
实现简单的调用堆栈记录:
bash复制trace() {
echo "Call stack:" >&2
local i=0
while caller $i; do
((i++))
done >&2
}
error_handler() {
echo "Error occurred in ${FUNCNAME[1]}: $1" >&2
trace
exit 1
}
# 使用示例
process_data() {
[[ -f "$1" ]] || error_handler "File not found: $1"
# 处理逻辑...
}
安全的交互式输入处理:
bash复制ask_yes_no() {
local prompt="$1"
local default="${2:-no}"
while true; do
read -rp "$prompt [yes/no] (default: $default): " answer
# 处理默认值
if [[ -z "$answer" ]]; then
answer="$default"
fi
case "$answer" in
[Yy]|[Yy][Ee][Ss]) return 0 ;;
[Nn]|[Nn][Oo]) return 1 ;;
*) echo "Please answer yes or no." >&2 ;;
esac
done
}
# 使用示例
if ask_yes_no "Continue with installation?" "yes"; then
echo "Proceeding with installation..."
else
echo "Installation cancelled."
fi
实现各种进度指示器:
bash复制# 旋转指示器
spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while ps -p "$pid" >/dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# 使用示例
long_running_command &
spinner $!
# 进度条
progress_bar() {
local duration=${1:-10}
local width=${2:-50}
local progress=0
for ((i=0; i<=$width; i++)); do
printf "["
for ((j=0; j<$i; j++)); do printf "="; done
for ((j=i; j<$width; j++)); do printf " "; done
printf "] %3d%%" $((i * 100 / width))
sleep "$(bc <<< "scale=4; $duration/$width")"
printf "\r"
done
printf "\n"
}
捕获信号执行清理:
bash复制cleanup() {
echo "Performing cleanup..."
# 关闭文件描述符、删除临时文件等
exit 1
}
trap cleanup INT TERM EXIT
# 主程序
while true; do
echo "Working..."
sleep 1
done
实现可恢复的任务:
bash复制process_batch() {
local start_from=${1:-0}
local total_items=1000
local checkpoint_file=".checkpoint"
# 加载检查点
if [[ -f "$checkpoint_file" ]]; then
start_from=$(<"$checkpoint_file")
echo "Resuming from item $start_from"
fi
for ((i=start_from; i<total_items; i++)); do
# 处理逻辑...
echo "Processing item $i"
# 保存检查点
echo $((i + 1)) > "$checkpoint_file"
done
# 完成后删除检查点
rm -f "$checkpoint_file"
}
将依赖打包到脚本中:
bash复制#!/bin/bash
# 嵌入式文件
EMBEDDED_DATA=$(cat <<'END_OF_DATA'
This is the embedded data file
with multiple lines of content.
END_OF_DATA
)
# 使用嵌入式数据
echo "Using embedded data:"
echo "$EMBEDDED_DATA"
bash复制#!/bin/bash
set -euo pipefail
# 配置
INSTALL_DIR="/usr/local/bin"
SCRIPT_NAME="my_script"
install_script() {
local source_path="$0"
local target_path="${INSTALL_DIR}/${SCRIPT_NAME}"
# 检查root权限
if [[ $EUID -ne 0 ]]; then
echo "Please run as root for system installation" >&2
return 1
fi
# 备份现有文件
if [[ -f "$target_path" ]]; then
local backup_path="${target_path}.bak.$(date +%s)"
echo "Backing up existing file to $backup_path"
cp