Bash 零基础教程

Bash 变量

变量是任何编程语言的基础构建块,Bash 也不例外。它们允许我们存储和操作数据,使我们的脚本更加动态和可重用。理解变量在 Bash 中的工作原理,有助于编写能够处理从简单的系统管理到复杂的自动化流程等各种任务的高效脚本。在本章中,我们将深入探讨 Bash 变量的世界,涵盖它们的声明、赋值、作用域以及具体用法。

变量就像是一个个贴着标签的储物盒,你可以把数据放进去,并在需要时通过标签名字把数据取出来使用。

1. 声明与赋值变量

在 Bash 中,你在使用变量之前不需要显式地声明它的“数据类型”(比如整数、字符串)。你只需要将一个值赋给一个名称,Bash 就会自动创建这个变量。变量名区分大小写,并且应该以字母或下划线开头,后面可以跟字母、数字或下划线。

1.1 变量赋值语法

要给变量赋值,请使用以下语法:

variable_name=value

极其重要: 等号 (=) 的两边绝对不能有空格。如果加了空格,Bash 会错误地将这部分解释为独立的命令和参数,从而导致报错。

示例 1:分配字符串值

NAME="张三"
echo $NAME # 输出: 张三

示例 2:分配整数值

AGE=30
echo $AGE # 输出: 30

示例 3:将命令的输出分配给变量
这是通过“命令替换”完成的(下文会详细说明)。

CURRENT_DATE=$(date)
echo $CURRENT_DATE # 输出: 例如 Tue Oct 27 10:30:00 UTC 2023

示例 4:分配一个空值

EMPTY_VAR=
echo "这个变量是空的: [$EMPTY_VAR]" # 输出: 这个变量是空的: []

1.2 访问变量值

要访问并读取存储在变量中的值,你通常需要使用 $ 符号,紧跟在变量名之前:

echo $variable_name

或者,你可以使用大括号 {} 将变量名括起来。当你需要避免歧义,或者需要将变量与其他纯文本紧密拼接在一起时,这种方法特别有用。

echo ${variable_name}

示例 1:基本变量访问

CITY="北京"
echo "我住在 $CITY" # 输出: 我住在 北京

示例 2:使用大括号消除歧义

NUMBER=5
echo "这个数字是 ${NUMBER}0" # 输出: 这个数字是 50
echo "这个数字是 $NUMBER0" # 输出: (可能什么都不输出,因为系统会去寻找名为 NUMBER0 的变量,而它并未被定义)

1.3 变量命名规范

虽然 Bash 在变量命名上提供了很大的灵活性,但遵循一定的规范可以极大地提高代码的可读性和可维护性:

  • 使用描述性名称: 名称应清楚表明变量的用途。
  • 大小写区分: 环境变量(系统级)通常使用全部大写,而脚本内部专用的局部变量通常使用小写。
  • 单词分隔: 变量名中的多个单词使用下划线分隔(例如 user_namefile_path)。

2. 变量作用域 (Scope)

变量的“作用域”决定了它在脚本的哪些部分可以被访问和读取。Bash 主要有两种类型的变量作用域:局部(Local)和全局(Global)。

2.1 局部变量 (Local Variables)

局部变量只能在定义它们的函数或代码块内部被访问。要声明一个局部变量,必须明确使用 local 关键字:

local variable_name=value

如果你尝试在其作用域之外访问局部变量,它要么是未定义的(空的),要么会读取到同名的全局变量的值。

示例:

my_function() {
  local MY_VAR="在函数内部"
  echo "函数内读取: $MY_VAR"
}

MY_VAR="在函数外部"
my_function # 调用该函数,将打印 '函数内读取: 在函数内部'
echo "函数外读取: $MY_VAR" # 打印 '函数外读取: 在函数外部'

在这个例子中,MY_VAR 既在全局定义了,又在 my_function 函数内部被定义为局部变量。当调用时,my_function 只能看到它自己的局部变量 MY_VAR。在函数执行完毕后,全局变量 MY_VAR 依然保留着它原本的值。

2.2 全局变量 (Global Variables)

全局变量可以在整个脚本的任何地方被访问,包括在函数内部。默认情况下,在函数外部声明的任何变量都是全局变量。

示例:

GLOBAL_VAR="这是一个全局变量"

my_function() {
  echo "函数内读取: $GLOBAL_VAR"
}

echo "函数外读取: $GLOBAL_VAR"
my_function # 打印 "函数内读取: 这是一个全局变量"

在这个例子中,GLOBAL_VAR 在函数外部声明,因此它可以同时在函数外部和 my_function 内部被访问。

2.3 作用域使用建议

  • 在函数内部使用局部变量有助于防止命名冲突,并确保函数独立运行(不干扰外部环境)。
  • 在函数内部修改全局变量可能会产生意想不到的后果(副作用),特别是当该变量在脚本的其他地方也被使用时。
  • 尽可能在函数内部使用局部变量,以提高代码的模块化程度并避免副作用。

3. 命令替换 (Command Substitution)

命令替换允许你捕获某个命令在终端上的执行输出,并将其直接赋值给一个变量。在 Bash 中,执行命令替换主要有两种方式:

方式一:使用反引号 (`)

variable_name=`command`

方式二:使用 $() (推荐)

variable_name=$(command)

强烈建议使用 $(...) 语法,因为它更具可读性,并且可以轻松嵌套。反引号在处理转义字符时容易出错,且难以进行多层嵌套。

示例 1:捕获当前日期

CURRENT_DATE=$(date)
echo "当前日期是:$CURRENT_DATE"

示例 2:捕获目录中的文件列表

FILES=$(ls)
echo "当前目录中的文件有:$FILES"

示例 3:捕获带有参数的复杂命令的输出

FREE_MEMORY=$(free -m | grep Mem | awk '{print $4}')
echo "可用内存:$FREE_MEMORY MB"

在这个例子中,我们使用 free -m 来以兆字节为单位显示内存信息,用 grep Mem 过滤出包含 "Mem" 的行,并用 awk '{print $4}' 精确提取第四列的数据(即可用内存)。

3.1 嵌套命令替换

$(...) 语法允许你嵌套多个命令替换,这在执行复杂操作时非常有用。

示例:

CURRENT_USER=$(whoami)
HOME_DIR=$(eval echo ~$CURRENT_USER)
echo "$CURRENT_USER 的家目录是:$HOME_DIR"

在这个例子中,我们首先使用 whoami 捕获当前用户的用户名。然后,我们使用 eval echo ~$CURRENT_USER 将波浪号 ~ 动态展开为该用户的家目录路径。这里需要 eval,因为单独的 echo 命令不会对跟在变量后面的波浪号进行路径展开。

4. 特殊变量

Bash 提供了几个具有预定义特殊含义的变量。这些变量对于获取有关脚本本身、它接收的参数以及运行环境的信息非常有用。

4.1 位置参数 (Positional Parameters)

位置参数是用来存储传递给脚本或函数的外部参数的变量。

  • $0: 脚本本身的名称。
  • $1, $2, $3...: 传递给脚本或函数的第一个、第二个、第三个及后续参数。
  • $#: 传递给脚本或函数的参数总个数。
  • $*: 一个包含所有参数的单一字符串,参数之间用空格分隔。
  • $@: 一个包含所有参数的列表(数组形式),在循环遍历时它比 $* 更安全、更常用。

示例脚本:

#!/bin/bash
echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "参数总数: $#"
echo "所有参数 (作为单一字符串): $*"
echo "所有参数 (作为独立个体): $@"

# 在循环中访问参数 (强烈推荐使用 "$@" 而不是 "$*")
for arg in "$@"; do
  echo "提取到参数: $arg"
done

如果你使用参数 foo bar baz 运行此脚本(例如 ./my_script.sh foo bar baz),输出将是:

脚本名称: ./my_script.sh
第一个参数: foo
第二个参数: bar
参数总数: 3
所有参数 (作为单一字符串): foo bar baz
所有参数 (作为独立个体): foo bar baz
提取到参数: foo
提取到参数: bar
提取到参数: baz

4.2 其他常见特殊变量

  • $?: 上一个执行命令的退出状态码。值为 0 表示执行成功,任何非 0 的值都表示发生了错误。
  • $$: 当前正在运行的脚本的进程 ID (PID)。
  • $!: 最后一个被放入后台运行的进程的 PID。
  • $_: 上一个命令的最后一个参数。

示例 1:检查命令的退出状态

ls -l non_existent_file
if [ $? -eq 0 ]; then
  echo "命令执行成功"
else
  echo "命令执行失败,发生错误"
fi

示例 2:使用当前脚本的 PID

echo "当前脚本的进程 PID 是: $$"

示例 3:访问上一个命令的最后一个参数

echo "你好 世界"
echo "上一个命令的最后一个参数是: $_" # 将输出 "世界"

5. 综合实战:使用变量创建系统信息展示脚本

我们将创建一个脚本,使用变量来自动收集并显示基本的系统信息。这将综合运用到命令替换和特殊变量。

#!/bin/bash

# 1. 使用命令替换收集系统信息,并赋值给变量
HOSTNAME=$(hostname)
KERNEL_VERSION=$(uname -r)
UPTIME=$(uptime | awk -F',' '{print $1}')
CURRENT_USER=$(whoami)
CURRENT_DIR=$(pwd)

# 2. 将信息格式化并显示到终端
echo "系统运行信息报告:"
echo "-------------------"
echo "主机名称: $HOSTNAME"
echo "内核版本: $KERNEL_VERSION"
echo "运行时间: $UPTIME"
echo "当前登录用户: $CURRENT_USER"
echo "当前工作目录: $CURRENT_DIR"
echo "-------------------"

# 3. 打印脚本自身的 PID
echo "此监控脚本的 PID: $$"

# 4. 打印传递给此脚本的参数个数
echo "接收到的附加参数个数: $#"

# 5. 检查上一条指令(echo)的退出状态
if [ $? -eq 0 ]; then
  echo "脚本运行结束,一切顺利。"
else
  echo "警告:脚本执行期间可能遇到了错误。"
fi

这个脚本通过 $(command) 捕获了主机名、内核版本、运行时间等关键数据,并将它们安全地存入了对应的变量中。随后它进行了格式化输出,并展示了如何调用 $$$? 等特殊系统变量。