跳到主要内容

函数

函数是组织和复用代码的基本单元。Shell 函数可以将一组命令封装成一个可重用的代码块,使脚本更加模块化、可读性更强。本章详细介绍 Shell 函数的定义、参数传递、返回值、作用域等核心概念。

函数定义

Shell 提供了两种函数定义语法。

基本语法

方式一:使用 function 关键字

function function_name {
commands
}

方式二:使用 ()

function_name() {
commands
}

两种方式功能相同,推荐使用第二种方式,因为它与 POSIX 标准兼容,可移植性更好。

简单示例

# 定义函数
greet() {
echo "Hello, World!"
}

# 调用函数
greet

函数定义的位置

函数必须在使用前定义。通常的做法是将函数定义放在脚本开头,或者放在单独的文件中通过 source 引入。

#!/bin/bash

# 函数定义
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# 使用函数
log "脚本开始执行"
# ... 其他代码 ...
log "脚本执行完毕"

删除函数

使用 unset 命令可以删除函数:

my_func() {
echo "Hello"
}

unset -f my_func # -f 表示删除函数

查看函数定义

使用 declare -f 查看函数定义:

# 查看指定函数
declare -f my_func

# 查看所有函数
declare -f

# 只列出函数名
declare -F

参数传递

函数通过位置参数接收参数,与脚本接收命令行参数的方式相同。

位置参数

在函数内部,$1$2 等表示函数的参数,而不是脚本的参数。

greet() {
echo "Hello, $1!"
}

greet "John" # 输出:Hello, John!
greet "World" # 输出:Hello, World!

特殊参数变量

变量含义
$1, $2, ...第 1、2、... 个参数
$0函数名(注意:在函数内部 $0 仍然是脚本名,不是函数名)
$#参数个数
$@所有参数(作为独立字符串)
$*所有参数(作为单个字符串)
show_args() {
echo "函数名: ${FUNCNAME[0]}"
echo "参数个数: $#"
echo "所有参数: $@"
echo "第一个参数: $1"
echo "第二个参数: $2"
}

show_args a b c

输出:

函数名: show_args
参数个数: 3
所有参数: a b c
第一个参数: a
第二个参数: b

参数默认值

greet() {
local name="${1:-World}" # 如果 $1 为空,使用默认值 World
echo "Hello, $name!"
}

greet # 输出:Hello, World!
greet "John" # 输出:Hello, John!

传递数组

数组不能直接作为参数传递,需要使用特殊技巧:

# 方式一:按引用传递
process_array() {
local -n arr=$1 # 使用名称引用
for item in "${arr[@]}"; do
echo "Item: $item"
done
}

my_array=(a b c)
process_array my_array

# 方式二:展开传递
process_array2() {
local arr=("$@")
for item in "${arr[@]}"; do
echo "Item: $item"
done
}

process_array2 "${my_array[@]}"

返回值

Shell 函数的返回值概念与其他编程语言有所不同。

退出状态码

函数使用 return 语句返回一个退出状态码(0-255):

is_even() {
local num=$1
if (( num % 2 == 0 )); then
return 0 # 成功
else
return 1 # 失败
fi
}

is_even 4
echo $? # 输出:0

is_even 3
echo $? # 输出:1

注意return 只能返回整数状态码,不能返回字符串或其他数据类型。

返回字符串

如果需要返回字符串,通常使用命令替换捕获函数的标准输出:

get_full_name() {
local first=$1
local last=$2
echo "$first $last"
}

name=$(get_full_name "John" "Doe")
echo "Full name: $name" # 输出:Full name: John Doe

返回数组

get_numbers() {
local arr=(1 2 3 4 5)
echo "${arr[@]}"
}

result=($(get_numbers))
echo "${result[@]}" # 输出:1 2 3 4 5

返回多个值

get_user_info() {
echo "John"
echo "Doe"
echo "[email protected]"
}

read -r first last email <<< "$(get_user_info)"
echo "Name: $first $last, Email: $email"

使用全局变量返回

result=""

calculate() {
local a=$1
local b=$2
result=$((a + b))
}

calculate 10 20
echo "Result: $result" # 输出:Result: 30

变量作用域

理解变量作用域对于编写可维护的函数至关重要。

全局变量

默认情况下,函数内的变量是全局的:

my_func() {
var="global"
}

my_func
echo $var # 输出:global(函数外可以访问)

局部变量

使用 local 关键字声明局部变量,其作用域仅限于函数内部:

my_func() {
local var="local"
echo "Inside: $var"
}

my_func
echo "Outside: $var" # 输出为空(函数外无法访问)

变量遮蔽

局部变量会遮蔽同名的外部变量:

var="global"

my_func() {
local var="local"
echo "Inside: $var"
}

my_func # 输出:Inside: local
echo "Outside: $var" # 输出:Outside: global

最佳实践

始终在函数内部使用 local 声明变量,避免污染全局命名空间:

calculate_average() {
local sum=0
local count=0
local num

for num in "$@"; do
((sum += num))
((count++))
done

echo $((sum / count))
}

递归函数

Shell 函数支持递归调用,但需要注意设置终止条件。

基本递归

factorial() {
local n=$1

if (( n <= 1 )); then
echo 1
return
fi

local prev=$(factorial $((n - 1)))
echo $((n * prev))
}

result=$(factorial 5)
echo "5! = $result" # 输出:5! = 120

斐波那契数列

fibonacci() {
local n=$1

if (( n <= 1 )); then
echo $n
return
fi

local a=$(fibonacci $((n - 1)))
local b=$(fibonacci $((n - 2)))
echo $((a + b))
}

for i in {0..10}; do
echo -n "$(fibonacci $i) "
done
# 输出:0 1 1 2 3 5 8 13 21 34 55

递归遍历目录

list_files() {
local dir=$1
local indent=${2:-""}

for item in "$dir"/*; do
if [[ -d "$item" ]]; then
echo "${indent}📁 $(basename "$item")/"
list_files "$item" " $indent"
elif [[ -f "$item" ]]; then
echo "${indent}📄 $(basename "$item")"
fi
done
}

list_files "/path/to/directory"

函数库

将常用函数放在单独的文件中,通过 source 引入,可以实现代码复用。

创建函数库

# 文件:lib/utils.sh

log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $1"
}

log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $1" >&2
}

check_file() {
if [[ ! -f "$1" ]]; then
log_error "File not found: $1"
return 1
fi
return 0
}

get_file_ext() {
local filename=$1
echo "${filename##*.}"
}

使用函数库

#!/bin/bash

# 引入函数库
source lib/utils.sh
# 或使用简写
. lib/utils.sh

# 使用函数
log_info "脚本开始执行"

if check_file "/etc/passwd"; then
log_info "文件存在"
ext=$(get_file_ext "document.txt")
log_info "文件扩展名: $ext"
fi

log_info "脚本执行完毕"

防止重复引入

# 文件:lib/utils.sh

# 检查是否已引入
[[ -n "${_UTILS_LOADED:-}" ]] && return 0
_UTILS_LOADED=1

# 函数定义...
log_info() { ... }

高级特性

函数属性

使用 declare 可以设置函数属性:

# 只读函数
readonly_func() {
echo "This function cannot be unset"
}
readonly -f readonly_func

# 导出函数(使其在子进程中可用)
export_func() {
echo "This function is exported"
}
export -f export_func

bash -c 'export_func' # 子进程可以调用

函数作为参数

execute_and_log() {
local func=$1
shift
echo "Executing $func with args: $@"
"$func" "$@"
echo "Done"
}

my_task() {
echo "Task: $1"
}

execute_and_log my_task "hello"

回调函数

process_items() {
local callback=$1
shift

for item in "$@"; do
$callback "$item"
done
}

print_item() {
echo "Item: $1"
}

process_items print_item a b c

高阶函数

# map 函数
map() {
local func=$1
shift
local result=()

for item in "$@"; do
result+=($($func "$item"))
done

echo "${result[@]}"
}

double() {
echo $(($1 * 2))
}

result=$(map double 1 2 3 4 5)
echo "Doubled: $result" # 输出:Doubled: 2 4 6 8 10

闭包模拟

Shell 不支持真正的闭包,但可以通过一些技巧模拟:

create_counter() {
local count=0

counter() {
((count++))
echo $count
}
}

create_counter
counter # 输出:1
counter # 输出:2
counter # 输出:3

函数调试

使用 set 追踪

debug_func() {
set -x # 开启追踪
echo "Debugging..."
local var="test"
echo $var
set +x # 关闭追踪
}

使用 trap 捕获错误

my_func() {
trap 'echo "Error at line $LINENO"' ERR

echo "Step 1"
false # 这个命令会失败
echo "Step 2"
}

打印函数调用栈

print_call_stack() {
local i
for ((i=0; i<${#FUNCNAME[@]}; i++)); do
echo " ${FUNCNAME[$i]}() at ${BASH_SOURCE[$i]}:${BASH_LINENO[$i-1]}"
done
}

inner_func() {
print_call_stack
}

outer_func() {
inner_func
}

outer_func

实用函数示例

日志函数

#!/bin/bash

# 日志级别
LOG_LEVEL_DEBUG=0
LOG_LEVEL_INFO=1
LOG_LEVEL_WARN=2
LOG_LEVEL_ERROR=3
CURRENT_LOG_LEVEL=$LOG_LEVEL_INFO

log() {
local level=$1
shift
local message="$@"

local level_name
case $level in
$LOG_LEVEL_DEBUG) level_name="DEBUG" ;;
$LOG_LEVEL_INFO) level_name="INFO" ;;
$LOG_LEVEL_WARN) level_name="WARN" ;;
$LOG_LEVEL_ERROR) level_name="ERROR" ;;
esac

if (( level >= CURRENT_LOG_LEVEL )); then
echo "[$level_name] $(date '+%Y-%m-%d %H:%M:%S') $message"
fi
}

log_debug() { log $LOG_LEVEL_DEBUG "$@"; }
log_info() { log $LOG_LEVEL_INFO "$@"; }
log_warn() { log $LOG_LEVEL_WARN "$@"; }
log_error() { log $LOG_LEVEL_ERROR "$@"; }

# 使用
log_info "Starting application"
log_debug "Debug information"
log_error "An error occurred"

参数解析函数

#!/bin/bash

parse_args() {
VERBOSE=0
OUTPUT=""
INPUT=""

while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
VERBOSE=1
shift
;;
-o|--output)
OUTPUT="$2"
shift 2
;;
-i|--input)
INPUT="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
}

show_help() {
cat << EOF
Usage: $0 [OPTIONS]

Options:
-v, --verbose Enable verbose mode
-o, --output Output file
-i, --input Input file
-h, --help Show this help message
EOF
}

parse_args "$@"
echo "Verbose: $VERBOSE"
echo "Input: $INPUT"
echo "Output: $OUTPUT"

文件处理函数

#!/bin/bash

# 检查文件是否存在且可读
check_readable_file() {
local file=$1

if [[ ! -e "$file" ]]; then
echo "Error: File does not exist: $file" >&2
return 1
fi

if [[ ! -f "$file" ]]; then
echo "Error: Not a regular file: $file" >&2
return 1
fi

if [[ ! -r "$file" ]]; then
echo "Error: File is not readable: $file" >&2
return 1
fi

return 0
}

# 安全创建目录
ensure_dir() {
local dir=$1

if [[ ! -d "$dir" ]]; then
mkdir -p "$dir" || {
echo "Error: Failed to create directory: $dir" >&2
return 1
}
fi

return 0
}

# 备份文件
backup_file() {
local file=$1
local backup="${file}.bak.$(date '+%Y%m%d_%H%M%S')"

if [[ -f "$file" ]]; then
cp "$file" "$backup"
echo "Backup created: $backup"
fi
}

字符串处理函数

#!/bin/bash

# 去除首尾空白
trim() {
local var="$1"
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
echo "$var"
}

# 字符串是否为空
is_empty() {
[[ -z "$(trim "$1")" ]]
}

# 是否为数字
is_number() {
[[ "$1" =~ ^[0-9]+$ ]]
}

# 是否为有效邮箱
is_email() {
[[ "$1" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
}

# 使用示例
str=" hello world "
trimmed=$(trim "$str")
echo "Trimmed: '$trimmed'"

if is_number "12345"; then
echo "Valid number"
fi

if is_email "[email protected]"; then
echo "Valid email"
fi

小结

本章详细介绍了 Shell 函数:

  • 函数定义:两种语法,推荐使用 name() { } 形式
  • 参数传递:使用位置参数 $1$2
  • 返回值return 返回状态码,echo 返回字符串
  • 变量作用域:使用 local 声明局部变量
  • 递归函数:支持递归调用,注意终止条件
  • 函数库:通过 source 引入外部函数文件
  • 高级特性:函数属性、回调、高阶函数

函数是编写模块化、可维护脚本的基础。下一章将学习文本处理,包括 grep、sed、awk 等强大的文本处理工具。