Bash 零基础教程

Bash 脚本参数传递机制

脚本参数(Script arguments)是在从命令行执行 Bash 脚本时传递给它的值。这些参数允许用户在不直接修改脚本代码的情况下自定义其行为,从而让你能够创建更加灵活和可复用的脚本。

理解如何访问和使用脚本参数,是编写高效 Bash 脚本的必备基础。本章将详细介绍用于访问这些参数的特殊变量:$1$2$@ 以及 $*

1. 访问脚本参数:$1$2

在 Bash 中,特殊变量 $1$2$3 一直到 $9,分别用于访问传递给脚本的第一个、第二个、第三个直到第九个参数。如果需要的参数超过九个,可以使用 ${10}${11} 等语法来访问第十个、第十一个以及后续的参数。

1.1 基础用法

让我们创建一个名为 greet.sh 的简单脚本。它接收一个名字作为第一个参数,接收一句问候语作为第二个参数,并打印出个性化的问候。

#!/bin/bash
# 访问第一个参数(名字)
name="$1"

# 访问第二个参数(问候语)
greeting="$2"

# 检查是否同时提供了这两个参数
if [ -z "$name" ] || [ -z "$greeting" ]; then
  echo "用法: $0 <名字> <问候语>"
  exit 1
fi

# 打印个性化问候语
echo "$greeting, $name!"

要运行这个脚本,你需要在终端中执行以下命令:

./greet.sh John "Hello"

这将会产生以下输出:

Hello, John!

在这个例子中,$1 保存了值 "John",而 $2 保存了值 "Hello"。脚本访问了这些参数,并利用它们构建了最终的问候语。

1.2 处理多个参数的示例

考虑一个名为 process_info.sh 的脚本,它接收名(first name)、姓(last name)和年龄(age)作为参数。

#!/bin/bash
# 访问第一个参数(名)
first_name="$1"

# 访问第二个参数(姓)
last_name="$2"

# 访问第三个参数(年龄)
age="$3"

# 检查是否提供了所有参数
if [ -z "$first_name" ] || [ -z "$last_name" ] || [ -z "$age" ]; then
  echo "用法: $0 <名> <姓> <年龄>"
  exit 1
fi

# 打印信息
echo "名: $first_name"
echo "姓: $last_name"
echo "年龄: $age"

运行这个脚本:

./process_info.sh Alice Smith 30

输出:

名: Alice
姓: Smith
年龄: 30

1.3 处理缺失的参数

在使用参数之前,检查用户是否真的提供了这些参数是非常重要的。如果缺少某个参数,其对应的特殊变量将为空(empty)。在 if 语句中,-z 选项专门用于检查字符串是否为空。如果缺少参数,脚本会打印一条“用法说明(Usage)”并退出。

提示: 变量 $0 是一个特殊变量,它始终保存着脚本自身的文件名。

2. 访问第九个以上的参数

要访问第九个之后的参数,必须将变量数字用花括号括起来。例如,${10} 用于访问第十个参数。如果不加花括号写成 $10,Bash 会将其解析为 $1 的值后面紧跟一个字符 0

考虑以下脚本 process_args.sh

#!/bin/bash
# 访问第十个参数
tenth_arg="${10}"

# 检查是否提供了第十个参数
if [ -z "$tenth_arg" ]; then
  echo "缺少第十个参数。"
else
  echo "第十个参数是: $tenth_arg"
fi

运行此脚本需要传递至少十个参数:

./process_args.sh a b c d e f g h i j

输出:

第十个参数是: j

3. 使用 $@ 访问所有参数

特殊变量 $@ 代表传递给脚本的所有参数,并且将它们视为独立的单词(字符串)。当你需要遍历所有参数或将它们传递给另一个命令时,它特别有用。

3.1 使用 $@ 遍历参数

你可以使用 for 循环来迭代 $@ 代表的所有参数。考虑脚本 print_args.sh

#!/bin/bash
# 遍历所有参数
for arg in "$@"; do
  echo "参数: $arg"
done

带上几个参数运行此脚本:

./print_args.sh one "two words" three

输出:

参数: one
参数: two words
参数: three

重点注意: "two words" 被视为一个单一的参数,因为它被引号括起来了。$@ 会完美保留引号内参数的空格,将它们作为一个整体单元来处理。

3.2 将参数传递给另一个命令

$@ 也常用于在脚本内部将所有收到的参数原封不动地传递给另一个命令。这对于创建用来修改现有命令行为的“包装器(wrapper)脚本”非常有用。

考虑一个名为 wrapper.sh 的脚本,它包装了 ls 命令:

#!/bin/bash
# 将所有接收到的参数传递给 ls 命令
ls -l "$@"

运行这个包装器脚本:

./wrapper.sh -a -h /tmp

这完全等同于在终端中直接运行 ls -l -a -h /tmp,它将以人类可读的格式列出 /tmp 目录下的详细内容(包括隐藏文件)。

4. 区分 $@$*

虽然 $@$* 都代表所有参数,但它们在处理包含空格的参数时有着本质的区别。

  • $@ 将每个参数视为一个独立的词,即使参数内部包含空格。
  • $* 将所有参数拼接成一个长字符串,参数之间使用 IFS(Internal Field Separator,内部字段分隔符)变量的第一个字符进行分隔。默认情况下,IFS 包含空格、制表符(Tab)和换行符。

4.1 使用 $* 访问作为单一字符串的参数

考虑脚本 print_star.sh

#!/bin/bash
# 将所有参数作为单一字符串打印
echo "所有参数: $*"

运行这个脚本:

./print_star.sh one "two words" three

输出:

所有参数: one two words three

正如你看到的,所有参数被拼接成了一个由空格分隔的单一字符串。与 $@ 不同,如果没有使用引号或者使用方式不当,$* 并不会保留带引号参数的独立性。

4.2 修改 IFS 变量

你可以通过更改 IFS 变量来修改 $* 的拼接行为。例如,你可以将 IFS 设置为逗号,从而用逗号将所有参数连接起来。

#!/bin/bash
# 将 IFS 设置为逗号
IFS=','

# 将所有参数作为单一字符串打印,用逗号分隔
echo "所有参数: $*"

再次运行这个脚本:

./print_star.sh one "two words" three

输出:

所有参数: one,two words,three

4.3 什么时候使用 $*,什么时候使用 $@

  • 强烈推荐使用 "$@": 当你需要保留每个参数的独立性时(尤其是当参数可能包含空格时)。在绝大多数情况下,这是最安全、最首选的选择。
  • 偶尔使用 "$*": 当你明确需要将所有参数作为一个单一的字符串输出或处理时(例如,传递给一个只接受单个长字符串输入的命令,或者仅仅是为了将它们合并输出)。

5. 实用案例演示

5.1 创建一个自动备份脚本

让我们回到系统管理的实战案例。假设我们想创建一个脚本来备份多个目录。该脚本应该接收要备份的目录列表作为参数,并创建一个 tar 压缩包。

#!/bin/bash
# 检查是否至少提供了一个参数 ($# 代表参数总数)
if [ $# -eq 0 ]; then
  echo "用法: $0 <目录1> <目录2> ..."
  exit 1
fi

# 定义备份文件的名称 (包含时间戳)
backup_file="backup_$(date +%Y%m%d%H%M%S).tar.gz"

# 创建 tar 压缩包 ("$@" 将所有传入的目录传递给 tar)
tar -czvf "$backup_file" "$@"

# 检查 tar 命令是否执行成功 ($? 检查上一条命令的退出状态码)
if [ $? -eq 0 ]; then
  echo "备份创建成功: $backup_file"
else
  echo "备份失败。"
  exit 1
fi

在这个脚本中:

  • $# 包含了传递给脚本的参数个数。
  • $@ 被用来将所有的目录参数完美传递给 tar 命令。
  • $? 保存了最后执行命令(这里是 tar)的退出码。0 表示成功。

运行这个脚本:

./backup_directories.sh /home/user/documents /var/log /etc

这将会创建一个包含所指定目录的 tar.gz 压缩包。

5.2 创建一个批量处理日志的脚本

假设你有一个用来处理日志文件的脚本。你希望能够一次性将多个日志文件作为参数传递给它。

#!/bin/bash
# 检查是否至少提供了一个参数
if [ $# -eq 0 ]; then
  echo "用法: $0 <日志文件1> <日志文件2> ..."
  exit 1
fi

# 遍历并处理每个日志文件
for log_file in "$@"; do
  if [ -f "$log_file" ]; then
    echo "正在处理日志文件: $log_file"
    # 在这里添加你的日志处理命令
    grep "ERROR" "$log_file"
  else
    echo "错误: 找不到文件: $log_file"
  fi
done

在这个脚本中,for 循环遍历作为参数传递的每一个日志文件。在循环内部,你可以执行具体的处理命令,例如使用 grep 搜索特定的错误模式。

运行这个脚本:

./process_logs.sh /var/log/syslog /var/log/auth.log

它将依次处理 syslogauth.log 文件,搜索并打印出包含 "ERROR" 的记录。