Shell 函数详解(新手详细版)
本文档面向零基础新手,从“函数是什么、为什么用”讲起,详细说明定义与调用、参数、返回值、局部变量、作用域、实用示例与常见坑等,并配有大量示例。
一、函数是什么?为什么要用?
1.1 一句话理解
函数就是给一段命令取个名字,以后只要写这个名字(并可按需传参数),就会执行这一段命令。
相当于把重复用到的逻辑“打包”成一个可复用的块。
- 复用:同一段逻辑在多处使用,只写一次函数,多处调用。
- 清晰:把长脚本拆成“做一件事”的小函数,主流程更好读。
- 参数:同一套逻辑可以对不同“输入”(如不同文件名、不同选项)执行。
1.2 和“直接写命令”的区别
不写函数时,重复逻辑就要复制粘贴多遍;改一处要改多处。
写成函数后:改函数里一处,所有调用都生效。
二、如何定义函数
2.1 两种常见写法
写法一:函数名后跟 (),再写 { }
函数名() {
命令1
命令2
}
写法二:用关键字 function(Bash 支持)
function 函数名 {
命令1
命令2
}
带参数时(参数在调用时传入,在函数里用 $1、$2 等取):
函数名() {
echo "第一个参数: $1"
echo "第二个参数: $2"
}
- { 可以和函数名同一行,也可以换行;} 单独一行或与最后一条命令用 ; 隔开。
- 函数必须在调用之前定义(或先 source 含定义的脚本),否则会“找不到命令”。
2.2 示例:无参数
say_hello() {
echo "你好,世界"
}
# 调用
say_hello
# 输出:你好,世界
2.3 示例:带参数
greet() {
echo "你好,$1"
}
greet "张三"
# 输出:你好,张三
greet "李四"
# 输出:你好,李四
三、如何调用函数
3.1 像命令一样写名字
调用时只写函数名,后面可以跟用空格分隔的参数(和普通命令一样)。
函数名
函数名 参数1 参数2 参数3
注意:函数名和括号 () 只在定义时写,调用时不写括号。
# 定义
my_func() { echo "hello"; }
# 正确调用
my_func
# 错误:不要写 my_func()
# my_func() 会变成“执行命令 my_func 并把输出当命令执行”,一般会报错
3.2 在脚本里调用
函数定义和调用可以都在同一个脚本里;也可以把函数写在另一个文件里,用 source 或 . 载入后再调用。
# script.sh
source ./my_functions.sh # 或 . ./my_functions.sh
my_func "参数"
四、函数里的“参数”:$1、$2、$#、$*、$@
4.1 位置参数 $1、$2、$3、…
在函数内部,$1、$2、$3、… 表示本次调用传入的第 1、2、3、… 个参数(和脚本的 $1、$2 类似,但作用域只在函数内)。
show_args() {
echo "第1个参数: $1"
echo "第2个参数: $2"
echo "第3个参数: $3"
}
show_args "one" "two" "three"
# 第1个参数: one
# 第2个参数: two
# 第3个参数: three
4.2 $#:参数个数
count_args() {
echo "一共传了 $# 个参数"
}
count_args a b c
# 一共传了 3 个参数
4.3 $* 与 $@:所有参数
- **$* :所有参数拼成一个字符串**(用空格连接)。
- $@ :所有参数各自保持为独立单词(带双引号时
"$@"是“每个参数一个词”,适合再传给其他命令)。
区别:在需要“把参数原样传给另一条命令”时,用 “$@” 更安全。
print_all() {
echo "用 $*: $*"
echo "用 $@: $@"
}
print_all "a b" "c"
# 用 $*: a b c
# 用 $@: a b c
# 若在函数里写: some_cmd "$@"
# 会变成 some_cmd "a b" "c"(两个参数)
# 若写 some_cmd "$*",会变成 some_cmd "a b c"(一个参数)
实用写法:包装一个命令并透传参数时,常用:
wrapper() {
echo "开始"
real_cmd "$@"
echo "结束"
}
4.4 $0:脚本名(不是函数名)
在函数里 $0 仍然是当前脚本的名字,不是函数名。Shell 没有“当前函数名”的内置变量,如需可用固定字符串或传参。
五、函数的“返回值”
5.1 两种“返回”要分清
- 退出码(exit status):0~255 的数字,表示“成功/失败”,用 return 或 exit 设置;用 $? 取。
- 输出(stdout):函数里 echo 的内容,调用方用 $(函数名 参数) 或反引号捕获成字符串。
函数没有“返回一个任意值”的语法,只能通过:
① return 数字 表示退出码;
② echo 输出 表示“返回一段文本”,由调用方 $( … ) 接收。
5.2 return:设置退出码
return [n] 会结束函数执行,并把退出码设为 n(0~255);不写 n 时 return 使用上一条命令的退出码。
is_even() {
if [ "$(( $1 % 2 ))" -eq 0 ]; then
return 0
else
return 1
fi
}
is_even 4
echo $?
# 0
is_even 3
echo $?
# 1
# 在 if 里用
if is_even 10; then
echo "10 是偶数"
fi
注意:return 只接受 0~255;超过会取模。若想“返回”一个数字给调用方做计算,更常见的做法是用 echo 输出,再用 $( ) 捕获。
5.3 用 echo + $() 得到“返回值”字符串
函数里 echo 的内容,可以被 $( 函数名 参数 ) 当作字符串拿到。
get_name() {
echo "张三"
}
name=$(get_name)
echo "名字是: $name"
# 名字是: 张三
# 返回数字也可以(实际是字符串)
double() {
echo $(( $1 * 2 ))
}
result=$(double 5)
echo "5 的 2 倍是 $result"
# 5 的 2 倍是 10
注意:函数里除了你想“返回”的 echo,不要有多余的 echo(如调试打印),否则会一起被捕获。调试完可删掉或改到 >&2(标准错误)。
5.4 return 与 exit 的区别
- return:只结束当前函数,回到调用处;只能在函数里用。
- exit:结束整个脚本(或当前 Shell 进程);在函数里调 exit 也会直接退出脚本。
f() {
echo "in f"
return 0
}
f
echo "after f"
# in f
# after f
g() {
echo "in g"
exit 1
}
g
echo "after g"
# in g
# (脚本结束,不会打印 after g)
六、局部变量:local
6.1 不用 local 时:变量是“全局”的
在函数里直接赋值 name=value,这个变量在函数外也可见;若外面已有同名变量,会被覆盖。
x=1
f() {
x=2
}
f
echo $x
# 2
6.2 用 local:只在函数内有效
local 变量名=值 声明的变量只在当前函数(及其调用的子函数)内有效,出函数就恢复外层的同名变量(若有)。
x=1
f() {
local x=2
echo "函数内 x=$x"
}
f
echo "函数外 x=$x"
# 函数内 x=2
# 函数外 x=1
6.3 建议
- 函数里只在本函数使用的变量,尽量用 local,避免影响外部或难以排查的冲突。
- local 只能在函数内使用;在脚本顶层写 local 会报错。
count_lines() {
local file=$1
local n
n=$(wc -l < "$file")
echo "$n"
}
七、函数与子 Shell、变量作用域
7.1 管道、$( ) 会开子 Shell
在 管道右侧,或 $( … ) 里执行的代码,是在子 Shell 里跑的:那里对变量的修改不会影响父 Shell(当前脚本)。
x=0
f() {
x=1
}
f | cat # 管道:f 在子 Shell 里执行
echo $x # 仍是 0
x=0
y=$(f) # f 在子 Shell 里执行,x=1 不影响外面
echo $x # 仍是 0
若希望函数里改的变量在外部生效,要避免把函数放在管道右侧或 $( ) 里;或者用全局变量(不用 local)并在同一进程里调用。
7.2 作用域小结
- 全局:在脚本顶层定义的变量,整个脚本可见;函数里不用 local 赋值的变量也是全局的。
- 局部:local 定义的变量只在当前函数(及它调用的函数)内有效。
- 位置参数 $1、$2、$#、$@:在函数内是本次调用的参数,不会改变脚本级的 $1、$2。
八、实用示例
8.1 封装重复逻辑:打印带时间戳的日志
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
log "程序开始"
# [2025-02-25 10:00:00] 程序开始
log "发生错误"
# [2025-02-25 10:00:01] 发生错误
8.2 参数检查
usage() {
echo "用法: $0 <文件名>"
exit 1
}
if [ $# -lt 1 ]; then
usage
fi
8.3 错误处理:检查上一条命令是否成功
check_ok() {
if [ $? -ne 0 ]; then
echo "错误: $1" >&2
exit 1
fi
}
mkdir -p backup
check_ok "创建 backup 失败"
8.4 包装命令并透传参数
safe_rm() {
if [ "$1" = "-f" ] || [ "$1" = "--force" ]; then
rm "$@"
else
rm -i "$@"
fi
}
8.5 返回“计算结果”(用 echo + $())
sum() {
echo $(( $1 + $2 ))
}
s=$(sum 3 5)
echo "3+5=$s"
# 3+5=8
8.6 默认参数
greet() {
local name=${1:-"访客"}
echo "你好,$name"
}
greet
# 你好,访客
greet "张三"
# 你好,张三
九、查看与删除函数定义
9.1 查看当前已定义的函数
declare -f # 列出所有函数定义
declare -f 函数名 # 只列指定函数
9.2 删除函数定义
unset -f 函数名
删除后该名字就不再是函数,若再调用会当作普通命令查找。
十、常见坑与建议
- 调用时不要写括号:写
my_func,不写my_func()。 - 参数含空格要加引号:
my_func "hello world",这样 $1 才是整句。 - “返回”字符串用 echo + $( ):return 只能 0~255,不能返回任意字符串或大数字。
- 函数里少用 exit:除非确实要终止整个脚本,否则用 return 更安全。
- 内部变量尽量 local:避免污染全局、便于维护。
- 透传参数用 “$@”:不要用 $*,以免参数中的空格被破坏。
- 函数要先定义再调用:定义写在调用之前,或通过 source 提前载入。
十一、语法与用法速查
| 内容 | 写法 |
|---|---|
| 定义 | 名() { ... } 或 function 名 { ... } |
| 调用 | 名 或 名 参数1 参数2 |
| 参数 | $1 $2 … $# $* $@ |
| 退出码 | return 0;调用方用 $? |
| “返回”字符串 | 函数里 echo 内容;调用方 x=$(名 参数) |
| 局部变量 | local 变量=值 |
| 查看/删除 | declare -f / unset -f 名 |
十二、小结
- 函数把一段命令命名后复用;定义用
名() { ... },调用只写名字和参数。 - 参数在函数内用 $1、$2、$#、$@;透传时用 “$@”。
- 返回值:return n 表示退出码(0~255);echo + $( 函数 ) 表示返回一段文本或“一个值”。
- local 限制变量作用域;管道/$( ) 会开子 Shell,那里改的变量不影响父脚本。
- 建议:内部变量用 local、少在函数里 exit、参数加引号、先定义再调用。
多写几个小函数(日志、参数检查、简单计算、包装命令),再结合 Shell流程控制.md 和 Linux简介.md 里的脚本示例,就能把 Shell 函数用熟。
文档以 Bash 为准;sh 或其它 Shell 在 function 关键字、local 等上可能略有差异。