CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Shell 与 Bash 入门/课时/函数与脚本结构
RawBlame
课时目录12 节

Shell 与 Bash 入门

  1. 01Shell 是什么
  2. 02目录与文件导航
  3. 03文件操作与权限
  4. 04管道与文本处理
  5. 05变量与字符串
  6. 06条件与循环
  7. 07函数与脚本结构
  8. 08正则表达式
  9. 09进程与定时任务
  10. 10网络命令
  11. 11系统管理与编辑器
  12. 12综合练习

函数与脚本结构

脚本骨架、参数、source vs bash。

30 分钟

函数与脚本结构

这一课你将能做到

  1. 定义和调用带参数的函数,说清 return 与 echo 两种「返回数据」方式的区别
  2. 用 local、source、set -euo pipefail 搭出可维护的生产级脚本骨架
  3. 写一个包含日志函数、错误处理、main 入口的部署/检查类脚本,结构对标公司运维脚本规范

为什么要学

脚本一超过 50 行,没有函数就是灾难:同一段「检查进程在不在」的逻辑复制五遍,改一处漏四处。函数让脚本获得和 Java 一样的能力——封装、复用、单一职责。面试官看你的脚本,先看有没有 set -euo pipefail、有没有 main 函数、错误怎么处理,这三点直接暴露工程素养,比背多少命令都有说服力。

人话:函数就像 Java 的方法:起个名字、收点参数、干完活给个交代。区别是 Shell 的 return 只能返 0-255 当状态码,要「返回数据」得靠 echo 输出 + 命令替换接住——这个差异是本课最重要的知识点。

先建立图像

生产级脚本的通用骨架(先看整体再拆解):

#!/bin/bash
─────────────────────────────
set -euo pipefail        ← 第一层:全局安全带
─────────────────────────────
常量与默认配置            ← 第二层:可调参数集中放
─────────────────────────────
函数定义区                ← 第三层:log / die / 业务函数
  log()    统一日志格式
  die()   报错即退出
  业务函数们
─────────────────────────────
main "$@"                ← 第四层:唯一入口

对应到 Java 你已经熟悉的结构:

ShellJava 类比
函数方法
$1 $2方法参数
return N返回状态码(≈ 抛异常与否)
echo 字符串return 返回值
source xx.shimport / include
set -euo pipefail全局 try-catch + fail-fast

跟着做

步骤 1:定义与调用第一个函数

cat > func1.sh << 'EOF'
#!/bin/bash
# 定义:function 关键字可省略,推荐写法如下
greet() {
  echo "你好, $1"          # $1 是函数的第一个参数
}

greet "deploy"             # 调用:函数名 + 参数,不是括号!
greet "reviewer"
EOF
bash func1.sh

注意:调用写 greet "deploy",写成 greet("deploy") 会报错——Shell 函数不是 C 系语法。

步骤 2:return 与 echo 的两种「返回」

cat > func2.sh << 'EOF'
#!/bin/bash
# 方式一:return 返回状态码(0 成功,非 0 失败)
file_exists() {
  [[ -f "$1" ]]
  # 函数最后一个命令的退出码自动成为 return 值,可省略 return
}

# 方式二:echo 输出数据,调用方用命令替换接住
today() {
  date +%F
}

# 判断逻辑走 return
if file_exists /etc/hostname; then
  echo "hostname 文件存在"
fi

# 取数据走 echo + $()
d=$(today)
echo "今天是 $d"

# 两者可结合:输出数据 + 退出码
get_error_count() {
  grep -c ERROR /opt/myapp/logs/app.log 2>/dev/null || echo 0
}
echo "错误行数:$(get_error_count)"
EOF
bash func2.sh

背下来:return 传「成败」,echo 传「数据」。想 return 一个字符串是做不到的,用 echo。

步骤 3:local 变量与作用域

cat > func3.sh << 'EOF'
#!/bin/bash
name="全局"

rename() {
  local name="局部"       # 不加 local 会污染全局!
  echo "函数内:$name"
}

rename
echo "函数外:$name"       # 仍是 全局
EOF
bash func3.sh

函数里可变的临时变量一律 local,这是脚本能维护的前提,和 Java 方法内局部变量同理。

步骤 4:log 与 die:所有脚本的标配

cat > lib.sh << 'EOF'
#!/bin/bash
log() {
  echo "[$(date '+%F %T')] $*"
}
die() {
  log "ERROR: $*" >&2      # 错误走 stderr
  exit 1
}
EOF

$* 把所有参数拼成一句话,>&2 把输出重定向到标准错误——日志和错误分流,采集系统才能正确归类。

步骤 5:source 与脚本模块化

cat > main.sh << 'EOF'
#!/bin/bash
source ./lib.sh                 # 加载函数库,当前 Shell 直接获得 log/die

TARGET=${1:?用法:$0 <目录>}
[[ -d "$TARGET" ]] || die "目录不存在:$TARGET"
log "开始检查 $TARGET"
log "文件数:$(find "$TARGET" -type f | wc -l)"
EOF
mkdir -p ~/func-lab/demo && touch ~/func-lab/demo/a.txt
cd ~/func-lab && bash main.sh demo

# 对比:source 与 bash 的区别
cat > env-demo.sh << 'EOF'
export DEMO_VAR=hello
myfunc() { echo "我是函数"; }
EOF
bash env-demo.sh; echo "bash 方式后:$DEMO_VAR"       # 空:子进程里执行
source env-demo.sh; echo "source 方式后:$DEMO_VAR"   # hello:当前 Shell 执行
myfunc                                               # 函数也留在当前 Shell

bash xx.sh 开子进程跑完即焚;source xx.sh 在当前 Shell 里执行,变量函数全留下。.bashrc 之所以要 source,就是这个原理。

步骤 6:set -euo pipefail 与 main 收口

cat > deploy-demo.sh << 'EOF'
#!/bin/bash
set -euo pipefail
# -e:任何命令失败立即退出(不再默默往下跑)
# -u:引用未定义变量直接报错(拼错变量名当场暴露)
# -o pipefail:管道中任何一环失败,整条管道算失败

source ./lib.sh

APP_NAME="demo-app"

check() {
  log "检查 $APP_NAME"
  [[ -d "/opt/$APP_NAME" ]] || die "/opt/$APP_NAME 不存在"
}

backup() {
  log "备份 $APP_NAME"
  tar -czf "/tmp/$APP_NAME-$(date +%F).tar.gz" -C /opt "$APP_NAME"
}

main() {
  check
  backup
  log "全部完成"
}

main "$@"
EOF
cd ~/func-lab && bash deploy-demo.sh demo-app && ls -lh /tmp/demo-app-*.tar.gz

main "$@" 把脚本参数转交给 main 函数,逻辑全在函数里,排查时一眼定位。这就是可以直接放进公司仓库的脚本形态。

讲清楚要点

  1. 函数先定义后调用:bash 逐行解释执行,调用写在定义前会报 command not found。所以约定「函数区在上,main 在下」。
  2. 函数内部 $1 是函数自己的参数,与脚本参数 $1 是两个世界;$# 同理在函数内变成函数的参数个数。想拿脚本参数,在 main 里显式传递 main "$@"。
  3. return 数值上限 255:return 256 等于 return 0。所以「返回行数、大小」这类数据绝不能走 return,必须 echo + $()。
  4. set -e 不是万能的:条件判断里的命令失败(if、&&、|| 右侧)不触发退出;管道默认只看最后一环(pipefail 补上)。理解边界才不会被「它怎么没退出」困惑。
  5. $? 检查要在命令后立刻做:中间插入一条 echo,$? 就变成 echo 的退出码(0)。要么紧跟检查,要么直接依赖 set -e。
  6. 库脚本只放函数和常量:库文件被 source,任何顶层执行语句都会在 source 时跑一遍。lib.sh 里出现 rm、启动服务这类语句是重大事故源。

你可能会懵的地方

  • $* 和 $@ 在函数里怎么用? 和脚本顶层规则一致:遍历用 "$@"(保参数边界),拼一句话用 "$*"。log 函数里 echo "$*" 就是把「多个参数拼成一条日志」。

  • 函数名能不能和命令重名? 能,函数优先于外部命令。有人用 grep() { command grep --color=auto "$@"; } 给 grep 加默认参数,command 用于调原名避免无限递归。日常别滥用,坑队友。

  • 如何让函数在每次开终端都可用? 写进 ~/.bashrc,或单独建 ~/.bash_functions 再在 .bashrc 里 source 它。改完 source ~/.bashrc 立即生效。

  • 脚本退出码到底是什么在决定? 最后一条被执行命令的退出码;显式 exit N 可随时指定;main "$@" 收尾的脚本,main 里最后一条命令决定成败。crontab、CI 正是读这个码判断任务成败。

  • 递归函数 Shell 支持吗? 支持,语法和 Java 方法递归一样。但 Shell 没有尾递归优化,深层递归性能差。需要递归遍历时优先考虑 find 或 tree 这类现成命令。

错法对照

错法问题正解
greet ("deploy") 调用带括号Shell 不认 C 系语法greet "deploy"
函数内不 local 直接赋值污染全局变量,连环事故临时变量一律 local
return "some string"return 只接受 0-255数据用 echo,调用方 $() 接
在 if 外写 [[ ... ]] 但依赖其失败set -e 下脚本直接退出判断逻辑放进 if / && / || 语境
lib.sh 顶层写了执行语句source 时被意外执行库文件只放函数、常量定义

卡点急救

  1. command not found 调用函数时:检查函数是否在调用点之前定义;是否拼写不一致;是否忘了 source 库文件。
  2. unbound variable 报错:这是 set -u 在保护你——引用了未定义变量。给变量补默认值 ${VAR:-},或修正拼写。上线脚本建议带着 -u 跑一遍测试。
  3. 脚本中途失败但看不出哪行:加 PS4='+ $LINENO: ' set -x 调试模式,每条命令执行前打印行号;或临时 bash -x script.sh 运行。排查完记得关掉,trace 日志量很大。

面试常问

  • Shell 函数如何返回字符串? 函数不能 return 字符串(return 仅 0-255 状态码)。标准做法是函数内 echo 输出,调用方用命令替换接收:result=$(my_func)。副作用变量(需多处共享)也可用全局变量,但要控制数量。

  • set -euo pipefail 分别是什么?为什么生产脚本要加? -e 任何命令失败立即退出,避免「前一步备份失败还继续删除旧包」的灾难;-u 禁止引用未定义变量,拦截拼写错误;-o pipefail 让管道任一环节失败都判定失败,避免 grep x file | wc -l 在 grep 失败时返回假成功。三者合称脚本安全带。

  • source 和 bash 执行脚本的区别? bash xx.sh 创建子 Shell 执行,变量、函数、cd 都随子进程销毁;source xx.sh 在当前 Shell 环境中逐行执行,变量函数直接生效。典型应用:.bashrc 必须 source、配置文件加载、让 cd/export 影响当前终端。

自测三问

  1. 函数既要返回「是否成功」又要返回数据,怎么设计?
  2. set -e 在哪些情况下不会触发退出?
  3. 为什么说 lib.sh 里不能有顶层执行语句?

小结一句话

函数 + local + source + set -euo pipefail + main 收口——这套组合拳让你的脚本从「一次性命令堆」升级为可测试、可维护、可进公司仓库的工程件。

延伸

  • Google Shell 风格指南
  • 下一课:正则表达式——给 grep/awk/sed 装上精准匹配的眼睛。
加载本节练习…
← 上一课条件与循环下一课 →正则表达式