在Linux系统管理和自动化运维领域,Shell脚本是最基础也是最强大的工具之一。很多工程师在掌握了基本的if判断、for循环后,就止步不前了。但实际上,当脚本复杂度上升到需要处理批量数据、实现模块化功能时,函数和数组这两个核心特性就变得不可或缺。
我曾在服务器迁移项目中,亲眼见证一个300行的流水账式脚本被重构为不到100行的函数化脚本后,不仅执行效率提升了40%,维护成本更是降低了70%。这就是掌握Shell函数和数组的威力。
Shell函数的定义语法看似简单,但实际使用中有很多细节需要注意。标准定义格式为:
bash复制function_name() {
commands
[return value]
}
或者使用更显式的function关键字:
bash复制function function_name {
commands
[return value]
}
注意:在bash中,两种形式完全等效,但第一种更符合POSIX标准,兼容性更好。
调用函数时直接写函数名即可,不需要括号:
bash复制function_name
Shell函数传参方式很特殊 - 它使用位置参数($1, $2等)而不是显式的参数列表。例如:
bash复制greet() {
echo "Hello, $1! Today is $2."
}
greet "Alice" "Monday"
返回值方面,Shell函数只能返回0-255的整数状态码。如果需要返回字符串等复杂数据,可以通过以下方式:
bash复制get_date() {
echo $(date +%F)
}
today=$(get_date)
当函数数量增多时,合理的组织方式至关重要。我推荐的做法是:
bash复制source /path/to/lib.sh
经验:在库文件中加入防重复引入机制:
bash复制[ -n "$_LIB_SH_LOADED" ] && return
_LIB_SH_LOADED=1
Bash支持两种数组:索引数组和关联数组。索引数组是最常用的形式:
bash复制# 声明并初始化
fruits=("Apple" "Banana" "Orange")
# 获取元素
echo ${fruits[0]} # Apple
# 获取所有元素
echo ${fruits[@]}
# 获取数组长度
echo ${#fruits[@]}
关联数组(类似其他语言的字典)需要先声明:
bash复制declare -A user
user=([name]="Alice" [age]=25)
echo ${user[name]}
bash复制for i in "${fruits[@]}"; do
echo $i
done
bash复制for ((i=0; i<${#fruits[@]}; i++)); do
echo "${fruits[i]}"
done
bash复制i=0
while [ $i -lt ${#fruits[@]} ]; do
echo "${fruits[i]}"
((i++))
done
bash复制for i in "${!fruits[@]}"; do
echo "${fruits[i]}"
done
bash复制readarray -t fruit_array <<< "$(printf "%s\n" "${fruits[@]}")"
数组合并:
bash复制vegetables=("Carrot" "Tomato")
food=("${fruits[@]}" "${vegetables[@]}")
数组切片:
bash复制echo ${food[@]:1:2} # 从索引1开始取2个元素
数组元素删除:
bash复制unset fruits[1] # 删除Banana
数组排序:
bash复制sorted=($(printf "%s\n" "${fruits[@]}" | sort))
假设我们需要处理多个日志文件,统计每种错误出现的次数:
bash复制process_logs() {
declare -A error_counts
local file
for file in "$@"; do
while read -r line; do
if [[ $line =~ ERROR ]]; then
error_type=$(echo "$line" | awk '{print $3}')
((error_counts["$error_type"]++))
fi
done < "$file"
done
# 输出统计结果
for type in "${!error_counts[@]}"; do
echo "$type: ${error_counts[$type]}"
done
}
用函数和数组构建简单的配置管理工具:
bash复制declare -A config
load_config() {
while IFS='=' read -r key value; do
config["$key"]="$value"
done < "$1"
}
get_config() {
echo "${config[$1]}"
}
set_config() {
config["$1"]="$2"
}
save_config() {
for key in "${!config[@]}"; do
echo "$key=${config[$key]}"
done > "$1"
}
bash复制declare -a test_cases
declare -A test_results
register_test() {
test_cases+=("$1")
}
run_tests() {
local test
for test in "${test_cases[@]}"; do
if $test; then
test_results["$test"]="PASS"
else
test_results["$test"]="FAIL"
fi
done
}
generate_report() {
echo "Test Report:"
for test in "${!test_results[@]}"; do
printf "%-40s %s\n" "$test" "${test_results[$test]}"
done
}
bash复制# 较差的方式
count=$(echo "$data" | wc -l)
# 更好的方式
count=$(wc -l <<< "$data")
bash复制# 较差的方式
for file in *; do
basename "$file"
done
# 更好的方式
for file in *; do
name="${file##*/}"
done
bash复制# 使用readarray处理大文件
readarray -t lines < large_file.txt
# 或者逐行处理
while IFS= read -r line; do
process_line "$line"
done < large_file.txt
bash复制arr=([0]="a" [100]="b")
echo ${#arr[@]} # 输出2,不是101
bash复制set -x # 开启命令追踪
set +x # 关闭命令追踪
bash复制# 在函数开头加入
debug() {
echo "DEBUG: $*" >&2
}
my_func() {
debug "Entering my_func with args: $@"
# 函数体
}
bash复制declare -p array_name # 显示数组完整定义
bash复制func() {
var="inside"
}
var="outside"
func
echo $var # 输出inside,可能不是预期结果
解决方案:总是使用local声明局部变量
bash复制# 如果函数有输出非结果的内容,会被捕获
get_data() {
echo "Debug info..." >&2
echo "result"
}
output=$(get_data) # output包含Debug info...
解决方案:将调试信息输出到stderr
bash复制files=("file 1" "file 2")
for file in ${files[@]}; do # 错误!会拆分成4个元素
echo "$file"
done
正确做法:
bash复制for file in "${files[@]}"; do
echo "$file"
done
bash复制arr=(a b c)
echo ${arr[3]} # 空,不是d
bash复制declare -A map # 必须要有
map[key]=value
解决方案脚本开头加入:
bash复制[ -z "$BASH_VERSION" ] && { echo "Require Bash"; exit 1; }
bash复制# Bash/zsh支持
function func { ... }
# POSIX兼容写法
func() { ... }
让我们综合运用函数和数组,构建一个实用的日志分析系统:
bash复制#!/bin/bash
declare -A error_stats
declare -A user_stats
declare -a log_files
# 初始化统计
init_stats() {
error_stats=()
user_stats=()
}
# 分析单个日志文件
analyze_log() {
local file="$1"
while IFS= read -r line; do
if [[ "$line" =~ \[ERROR\] ]]; then
# 提取错误类型和用户
error_type=$(echo "$line" | awk -F'[][]' '{print $4}')
user=$(echo "$line" | awk '{print $6}')
# 更新统计
((error_stats["$error_type"]++))
((user_stats["$user"]++))
fi
done < "$file"
}
# 生成报告
generate_report() {
echo "==== Error Statistics ===="
for err in "${!error_stats[@]}"; do
printf "%-20s %d\n" "$err" "${error_stats[$err]}"
done | sort -nr -k2
echo -e "\n==== User Error Count ===="
for user in "${!user_stats[@]}"; do
printf "%-15s %d\n" "$user" "${user_stats[$user]}"
done | sort -nr -k2
}
# 主流程
main() {
if [ $# -eq 0 ]; then
echo "Usage: $0 logfile [logfile...]"
exit 1
fi
log_files=("$@")
init_stats
for file in "${log_files[@]}"; do
[ -f "$file" ] || continue
analyze_log "$file"
done
generate_report
}
main "$@"
这个脚本展示了如何:
bash复制# 在函数开头使用here document
func() {
: <<'HELP'
Description:
Parameters:
HELP
# 函数体
}
在实际工作中,我发现很多Shell脚本最终会演变成小型系统。这时候良好的函数设计和合理的数据结构(数组)使用就变得至关重要。建议从小的工具脚本开始实践,逐步构建自己的Shell工具库。