Shell函数

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 的数字,表示“成功/失败”,用 returnexit 设置;用 $? 取。
  • 输出(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 函数名

删除后该名字就不再是函数,若再调用会当作普通命令查找。


十、常见坑与建议

  1. 调用时不要写括号:写 my_func,不写 my_func()
  2. 参数含空格要加引号my_func "hello world",这样 $1 才是整句。
  3. “返回”字符串用 echo + $( ):return 只能 0~255,不能返回任意字符串或大数字。
  4. 函数里少用 exit:除非确实要终止整个脚本,否则用 return 更安全。
  5. 内部变量尽量 local:避免污染全局、便于维护。
  6. 透传参数用 “$@”:不要用 $*,以免参数中的空格被破坏。
  7. 函数要先定义再调用:定义写在调用之前,或通过 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流程控制.mdLinux简介.md 里的脚本示例,就能把 Shell 函数用熟。


文档以 Bash 为准;sh 或其它 Shell 在 function 关键字、local 等上可能略有差异。

发表评论