4. 脚本参数传递:位置参数、特殊变量与 getopts

脚本写出来,总不能把数据写死在代码里吧?那也太不灵活了。真正好用的脚本,得能接收外部输入。这一章我们就聊聊参数传递——说白了,就是怎么让脚本「听懂人话」。

核心要点:参数传递是脚本与外界交互的桥梁。掌握好它,你的脚本才能从「一次性工具」进化为「可复用组件」。

4.1 位置参数:$0 到 $9 的江湖

位置参数是最基础的参数传递方式。你运行脚本时跟在后面的那些值,脚本里用 $1、$2 就能拿到。

#!/bin/bash
# 一个简单的部署脚本
echo "脚本名: $0"
echo "目标服务器: $1"
echo "部署版本: $2"
echo "操作: $3"

运行一下:

$ ./deploy.sh prod-server v2.1 restart
脚本名: ./deploy.sh
目标服务器: prod-server
部署版本: v2.1
操作: restart

这里有个坑,我踩过好几次。$0 是脚本本身的路径,不是参数。如果你用相对路径执行,它返回的就是相对路径。用绝对路径,它就返回绝对路径。做日志记录时,这个差异可能会让你排查半天。

注意:位置参数最多支持 $9。想用第10个参数?得写成 ${10}。别问我怎么知道的——我曾经在循环里写 $10,结果它被解析成了 $1 后面跟个 "0"。

4.2 参数个数与遍历:$#、$@、$*

这三个变量,我刚开始学的时候总觉得差不多。用多了才发现,它们各有各的脾气。

变量 含义 典型场景
$# 参数个数 校验用户是否传够了参数
$@ 所有参数(每个参数独立) 循环遍历每个参数
$* 所有参数(当作一个字符串) 直接传给另一个命令

看个例子就明白了:

#!/bin/bash
echo "参数个数: $#"
echo "使用 \$@ 遍历:"
for arg in "$@"; do
    echo "  - [$arg]"
done

echo "使用 \$* 遍历:"
for arg in "$*"; do
    echo "  - [$arg]"
done

运行 ./test.sh a "b c" d,输出差异很明显:

参数个数: 3
使用 $@ 遍历:
  - [a]
  - [b c]    # 保持了引号内的空格
  - [d]

使用 $* 遍历:
  - [a b c d]  # 所有参数粘在一起了

我个人习惯在循环里用 "$@",带引号那种。它能保留参数中的空格,不会把 "b c" 拆成两个。这个细节,当年让我在日志解析脚本上折腾了一下午。

4.3 返回值与退出码:$?

$? 记录上一条命令的退出码。0 表示成功,非 0 表示出错了。

#!/bin/bash
ping -c 1 8.8.8.8 > /dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "网络通了"
else
    echo "网络不通,检查一下"
fi

小技巧:检查 $? 要趁早。每执行一条新命令,它就会被覆盖。我习惯把关键步骤的退出码存到变量里:ret=$?,后面慢慢分析。

4.4 高级参数解析:getopts 实战

位置参数虽然简单,但参数一多就乱了。用户得记住 $1 是服务器、$2 是版本……这体验太差了。getopts 就是来解决这个问题的——它支持 -h、-v 这种风格。

#!/bin/bash
usage() {
    echo "用法: $0 -s <服务器> -v <版本> [-d] [-h]"
    echo "  -s  目标服务器(必填)"
    echo "  -v  部署版本(必填)"
    echo "  -d  调试模式"
    echo "  -h  帮助信息"
    exit 1
}

server=""
version=""
debug=false

while getopts "s:v:dh" opt; do
    case $opt in
        s) server="$OPTARG" ;;
        v) version="$OPTARG" ;;
        d) debug=true ;;
        h) usage ;;
        ?) usage ;;
    esac
done

# 校验必填参数
if [ -z "$server" ] || [ -z "$version" ]; then
    echo "错误:-s 和 -v 是必填参数"
    usage
fi

echo "部署到: $server"
echo "版本: $version"
[ "$debug" = true ] && echo "调试模式已开启"

运行效果:

$ ./deploy.sh -s web01 -v 2.1.0 -d
部署到: web01
版本: 2.1.0
调试模式已开启

$ ./deploy.sh -h
用法: ./deploy.sh -s <服务器> -v <版本> [-d] [-h]
  -s  目标服务器(必填)
  -v  部署版本(必填)
  -d  调试模式
  -h  帮助信息

getopts 的坑:

  • 选项字母后面的冒号表示「这个选项需要参数」,比如 s: 表示 -s 后面必须跟值
  • 不支持长选项(如 --server),那是 getopt 的活
  • 选项顺序无所谓,但参数值不能跟选项粘在一起(-sweb01 可能不生效)

4.5 知识体系总览

下面这张图,把本章的知识点串起来了。你可以把它当作参数传递的「地图」——遇到问题先看它,定位到具体模块再细查。

脚本参数传递知识体系 参数传递 位置参数 特殊变量 getopts 解析 $0 ~ $9 ${10} 以上 shift 移位 $# 参数个数 $@ 独立参数 $* 整体字符串 选项定义 OPTARG 取值 case 分支 💡 核心原则 参数校验要早、错误提示要清、帮助信息要全 实战建议 • 简单脚本(参数≤3个):用位置参数就够了 • 复杂脚本(参数≥4个):上 getopts,用户体验好很多 • 生产环境脚本:一定要加参数校验和帮助信息 • 日志里记录完整的命令行:方便事后排查问题

4.6 避坑指南与最佳实践

做运维这些年,参数传递这块我踩过的坑,能写满一页纸。挑几个典型的说说:

我的经验:

  • 引号别省:所有变量引用都加双引号,除非你明确知道不加的后果。"$@" 和 $@ 是两个东西。
  • 参数校验前置:脚本开头就检查参数,别等到执行到一半才发现缺参数。
  • 帮助信息要全:写 -h 选项,把用法、参数说明、示例都写上。用户会感谢你的。
  • 退出码要规范:0 成功,1 通用错误,2 参数错误。别乱用退出码。

我曾经写过一个数据库备份脚本,参数解析用了位置参数。后来需求变了,要加新参数,结果所有调用方都得改顺序。那次之后,但凡超过两个参数,我必上 getopts。你想想看,改一个脚本要通知十几个团队改调用方式,那画面太美我不敢看。

一句话总结:参数传递是脚本的「门面」。门面做得好,用户用得爽;门面做得差,天天被吐槽。花 10 分钟把参数解析写好,能省下后面 10 小时的沟通成本。