网站搜索

Bash 中最有用的特殊变量和环境变量介绍


你目前正在学习 Bash 吗?你有没有看到过像 $0 和 $EUID 这样的东西,并想知道它们是什么意思?或者 $UID 和 $EUID 之间有什么区别?我会讲解这些以及更多内容:它们的作用以及为什么重要。

特殊参数

特殊参数是 Bash 提供用于特定目的的变量。它们少于 10 个,下面这些是我觉得最有用的。

获取脚本路径

有时候在编写脚本时,你可能想知道它的路径。例如,在创建帮助菜单时,通常会在顶部包含脚本的名称。以下是 ls 命令的帮助菜单:

ls --help

您可以使用 $0 特殊参数访问(相对)路径。这是 POSIX 标准,因此您可以在大多数 shell 中使用它:

echo "$0"

然后你可以使用 $ (basename $0) 获取脚本名称:

然而,有时 $0 并不可靠——例如,源 /path/to/script.sh 返回的是 “bash”,而不是脚本路径。取而代之,应使用 $BASH_SOURCE 变量,它的工作方式几乎相同,但不会出现意外。如果你仅使用 Bash,那么这是更好的选择,但它在其他 shell 中不可移植。

确定进程的退出状态

每个程序在退出时都会返回一个数字。这个数字称为退出码,用于在命令返回错误时做出决策。例如,如果你尝试 ls 一个不存在的目录,它会返回一个非零值。

零退出状态表示成功;非零表示出现问题。

当你遇到问题时,你可能希望停止执行,而 $? 特殊参数提供所需的退出代码:

ls /system32
echo $?

有几种方法可以检测和处理退出码。最明显的方法是使用条件语句来评估“$?”:

if [[ $? -eq 0 ]]; then
   # If the script executed successfully.
fi

退出代码通常有特定含义,并且是针对具体程序的,但你可以使用 case 语句来处理不同的值:

case $? in
  0) echo "OK";;
  1) echo "Err";;
  *) echo "Unhandled error";;
esac

我喜欢的一种方法是使用没有大括号的条件语句:

if ls /system32; then
  # Upon zero exit code.
  echo "/system32 exists"
fi

但最优雅的解决方案是使用逻辑运算符:

ls /system32 && echo "/system32 exist" # && executes on zero exit status.
ls /system32 || echo "/system32 does not exist" # || executes on non-zero exit status.

访问参数

如果你在编写 Bash 脚本,你必然需要向它传递值——也就是参数或位置参数。Bash 的方法有点笨拙,但它可行。通过示例是理解它们的最简单方式:

foo() {
  echo "$1" # First argument.
  echo "$2" # Second argument.
}
foo "first" "second"

我已经向函数传递了参数,但它们在脚本的顶层运行时完全一样。例如:

#!/usr/bin/env bash
# This is the top-level of your script.
echo "The first argument: $1"
echo "The second argument: $2"

然后你可以通过命令行向你的脚本传递参数:script.sh "first" "second"。

接下来,"$@" 特殊参数表示所有参数作为数组:

foo() {
  for arg in "$@"; do
    echo "$arg"
  done
}
foo "first" "second"

“$*” 特殊参数与 “$@” 相同,不同之处在于它会将所有参数放入一个字符串中(如果 “$*” 使用双引号):

at() {
  printf '@: [%s]\n' "$@"
}

star() {
  printf '*: [%s]\n' "$*"
}

at   "one two" "three"
star "one two" "three"

你可以看到 “$@” 会将两个参数分别打印在两行,而 “$*” 会将它们打印在一行。

要获取参数的数量,可以使用 ${#@} 或 ${#*} 语法:

foo() {
  echo ${#@}
  echo ${#*}
}
foo "one" "two"

环境变量

环境变量是由 shell 提供给正在运行的程序的值。例如,$HOME 提供当前用户主目录的路径。

获取用户ID

有时,我需要在脚本中知道当前用户的 ID。例如,最近我在 /run/user/1000 目录下与一个套接字进行交互。这里的 “1000” 是用户 ID,如果我们想要一个健壮的脚本,就不应该硬编码这个值,因为执行该脚本的其他用户可能有不同的用户 ID。

为了解决这个问题,我们可以使用 $UID 和 $EUID 变量。"$UID" 显示执行二进制文件的用户 ID。"$EUID"(有效用户 ID)是执行二进制文件的进程的用户 ID。通常,它们是相等的,但对于 setuid 二进制文件,它们可能不同。例如,当通过 sudo 执行某些操作时,随着进程在不同阶段的分叉,它们会有所不同。

ps --forest -eo cmd,euid,ruid | grep -C 1 '[s]udo'

我在后台运行着“sudo sleep 600”,上面的提示会显示命令、EUID(“1”)和 UID(“2”)列,针对任何正在使用 sudo 的进程。你可以看到 EUID 会立即变为“0”,但 UID 直到最终被 fork 出的进程(即以 root 身份运行的“sleep”)才会改变。

我们能从中得出什么结论?嗯,实际上使用哪一个并不重要。有些人说使用“$EUID”来检查你是否拥有 root 权限,因为它表示有效权限。然而,你可以从图片中看到,最终的“sleep”进程的 UID 和 EUID 都设置为“0”(root),而 shell 进程的 UID 和 EUID 都设置为“1000”(用户)。这些分别代表了 fork 过程的两端——你发起 sudo 请求的时刻,以及最终执行的进程。因此,在关键时刻,它们之间没有实际的区别,但要记住,在 fork 过程中,对于 setuid 二进制文件,它们是不同的。

获取常用用户路径

在我编写的几乎每个脚本中,我都会访问主目录。较少情况下,我需要访问 ~/.config 或 ~/.local/share。访问文件系统中的典型位置很常见,但像这样硬编码路径是不好的做法,因为它们可能会发生变化。推荐的方法是使用 XDG 目录规范,这是由 freedesktop.org 提供的一组标准变量。

env | grep XDG | sort -u

并非所有变量都已设置,有些也不是路径。

在使用 XDG 变量时,务必始终设置一个合理的默认值:

export "${XDG_CACHE_HOME:=$HOME/.cache}"

这也会设置该变量(如果尚未设置),并使其可供你执行的任何二进制文件或脚本使用。

订阅通讯以获取实用的 Bash 指南

订阅通讯,获取实用的 Bash 指南

订阅即表示您同意接收新闻通讯和营销邮件,并接受 Valnet 的使用条款和隐私政策。您可以随时取消订阅。

我最常使用的变量有:

export "${XDG_CACHE_HOME:=$HOME/.cache}"       # Semi-temporary data.
export "${XDG_CONFIG_HOME:=$HOME/.config}"     # Configuration files.
export "${XDG_DATA_HOME:=$HOME/.local/share}"  # Downloaded data, etc.

还有更多,我鼓励你去学习它们。你应该使用这些位置,而不是将所有内容写入 /tmp 或 ~/ 中的自定义目录。


在这些变量之上,通过 shell 还可以使用许多有用的环境变量。本文涉及的变量是我最常使用的。如果没有它们,我无法编写 Bash 脚本。相反,硬编码值最终会导致脚本出错。当你有数百个脚本时,这种错误会成为维护噩梦。因此,最好尽可能从其他地方推断变量值,最好是从标准变量中,而今天介绍的这些变量正好符合这一要求。

最后,尽量学习并使用尽可能多的标准变量。

相关文章