Contents

从脚本中获取Bash脚本位置

1.简介

有时我们想从其代码中操作脚本。例如,我们可能必须获取一个不打算执行的脚本。我们甚至可以更改它的代码。为此,我们需要脚本的位置。

在本文中,我们将讨论如何从 Bash 脚本内部获取该脚本的位置。我们首先检查运行 Bash 脚本的不同方法。之后,我们查看不同运行模式的上下文后果。最后,我们检查如何获取脚本的位置并在每种情况下对其进行优化。最后,我们展示了一个通用的解决方案,结合了所有讨论过的方法。

我们使用 GNU Bash 5.1.4 在 Debian 11 (Bullseye) 上测试了本教程中的代码。它是 POSIX 兼容的,应该可以在任何这样的环境中工作。

2. Bash 脚本

对于本节中的示例,我们将使用具有以下内容的var.sh

#!/usr/bin/env bash
declare -a value=666
echo "Inside script: value=${value}"

在 Bash 中使用脚本有两种主要方法—— source 和 execute。Sourcing 在当前 shell 中运行所有命令,而 execution 在新 shell 中运行它们。让我们简要讨论解释脚本的每种方法。

2.1. source

要获取脚本,我们可以使用source. (点)命令。两个源命令之间的唯一区别是sourcePOSIX 标准*.*命令的 Bash 别名:

$ declare -a value=0
$ echo "${value}"
0
$ source var.sh
Inside script: value=666
$ echo "${value}"
666

请注意我们当前 shell 中的变量如何获取我们在脚本中分配的内容。这意味着我们对当前会话进行了更改。由于这种影响,采购是在日常活动中手动运行 Bash 脚本的不太常见的方式。

另一方面,source是 Bash 中大多数附加功能的基础。例如,至少,默认的*/etc/profile* Bash 配置文件通常会获取当前用户的bashrc 文件。实际上,/etc/profile本身就是有源的。

实际上,用户更经常手动执行而不是源脚本。让我们看看这是怎么发生的。

2.2. 执行

要执行脚本,我们可以使用bash 命令或在确保通过chmod 启用执行后直接按名称调用脚本:

$ declare -a value=0
$ bash var.sh
Inside script: value=666
$ echo "${value}"
0
$ chmod +x var.sh
$ ./var.sh
Inside script: value=666
$ echo "${value}"
0

在这里,正如我们所料,执行var.sh不会修改我们当前 shell 中的value变量。

让我们探讨采购和执行上下文的不同之处。

3. Bash 脚本上下文

获取脚本相当于将其内容复制并粘贴到交互式提示中。因此,我们在此过程中读取的任何上下文都将来自当前 shell。

另一方面,执行脚本等同于为单个命令生成带有*-c标志的bash 。*

考虑到这一点,我们可以简单地比较两种运行模式环境中的命令上下文

$ echo "Current shell: \$=${$}"
Current shell: $=666
$ bash -c -- 'echo "Subshell: \$=${$}"'
Subshell: $=667
$ echo "Current shell: \$=${$}"
Current shell: $=666
$ bash -c -- 'echo "Subshell: \$=${$}"'
Subshell: $=668

请注意,对于当前 shell,shell PID $$ 始终保持为666。但是,它会在每次使用bash执行新的子shell 时发生变化。

我们还可以期待哪些其他变化?由于主 shell 和子 shell 显然是不同的进程,因此它们可以有不同的配置、环境和状态

运行脚本所特有的环境变化之一是该脚本的位置。脚本执行的第零个参数*$0*包含脚本调用路径:

$ echo '
> echo "${0}"
> ' > location.sh
$ bash location.sh
location.sh

至少它应该并且经常这样做,但有一些警告。主要的是 $0仅在执行的脚本中像这样工作,而不是在 sourcing 时

$ source location.sh
bash

当我们 source 时,$0只是指定我们正在使用的解释器。由于命令是内置的,而不是可执行文件,因此它不会添加到参数计数中。

由于这些限制,不建议我们将*$0*用于我们的目的。我们还有什么其他方法可以获取脚本的位置?

4. Bash 脚本位置

获取脚本位置的最可靠方法是BASH_SOURCE 特殊变量:

$ echo "${BASH_SOURCE}"
$ echo '
> echo "${BASH_SOURCE}"
> ' > location.sh
$ ./location.sh
location.sh
$ . location.sh
location.sh

BASH_SOURCE的元素 0 (即${BASH_SOURCE})包含脚本的相对或绝对路径**。它大致相当于*$0*,但结果可预测。请注意交互式提示如何没有关联的命名脚本。

一旦我们获得脚本位置,我们可能希望以多种方式之一对其进行转换。

从这里开始,我们只对我们的示例使用采购,因为BASH_SOURCE不区分。

4.1. 链接

我们正在编写的脚本可以通过符号链接 获取,即符号链接。**由于BASH_SOURCE将包含链接的路径而不是实际的脚本路径,我们可以通过readlink **递归地解析链接。以下是实现此目的的location.sh的内容:

SCRIPT_PATH="${BASH_SOURCE}"
while [ -L "${SCRIPT_PATH}" ]; do
  TARGET="$(readlink "${SCRIPT_PATH}")"
  if [[ "${TARGET}" == /* ]]; then
    SCRIPT_PATH="$TARGET"
  else
    SCRIPT_PATH="$(dirname "${SCRIPT_PATH}")/${TARGET}"
  fi
done
echo "BASH_SOURCE=${BASH_SOURCE}"
echo "SCRIPT_PATH=${SCRIPT_PATH}"

在一个循环中,我们使用*-L*标志来测试 当前脚本路径是否是符号链接。虽然它是,但我们检查它是绝对的还是相对的,解析它并继续解析路径。

循环之后,SCRIPT_PATH指向脚本的硬链接

$ ln --symbolic --relative location.sh /subdir/location.sh
$ . location.sh
BASH_SOURCE=location.sh
SCRIPT_PATH=location.sh
$ . subdir/location.sh
BASH_SOURCE=./subdir/locationrelativelink.sh
SCRIPT_PATH=./subdir/../location.sh

我们使用ln (Link) 创建脚本的符号链接,并比较源代码和符号链接的结果

当然,所有这些路径仍然是相对的。我们可能想要改变这一点。

4.2. 绝对路径

带有前导斜杠的路径是绝对路径为了解决相对于绝对路径的问题,我们可以再次使用readlink,这次使用-f*标志*:

$ echo '
> echo "$(readlink -f "${BASH_SOURCE}")"
> ' > location.sh
$ . location.sh
/location.sh

注意前导斜杠,表示绝对文件路径。一旦我们有了它,我们就可以进一步操纵它。

4.3. 脚本目录

接下来,我们可以通过dirname (目录名称)仅提取包含目录路径:

$ echo '
> echo "$(dirname -- "$(readlink -f "${BASH_SOURCE}")")"
> ' > location.sh
$ . location.sh
/

脚本目录本身也可以是一个链接,我们可以通过cd (更改目录)及其-P 标志来解析:

$ mkdir subdir
$ ln --symbolic --relative subdir subdirlink
$ echo "$(cd "subdirlink" >/dev/null 2>&1 && pwd)"
/subdirlink
$ echo "$(cd -P "subdirlink" >/dev/null 2>&1 && pwd)"
/subdir

首先,我们创建一个新目录的符号链接。之后,每次在子shell中,我们通过pwd 检查当前目录在使用和不使用*-P标志更改为cd*后的内容。在前一种情况下,我们得到实际的目录名称。

我们现在有多种方法来获取有关正确脚本位置的信息。让我们把它们结合起来。

4.4. 完整的 Bash 脚本位置

我们将前面小节中的所有场景都考虑在内,以达到一个通用的解决方案来获取 Bash 脚本的位置,而不管运行模式和条件如何

SCRIPT_PATH="${BASH_SOURCE}"
while [ -L "${SCRIPT_PATH}" ]; do
  SCRIPT_DIR="$(cd -P "$(dirname "${SCRIPT_PATH}")" >/dev/null 2>&1 && pwd)"
  SCRIPT_PATH="$(readlink "${SCRIPT_PATH}")"
  [[ ${SCRIPT_PATH} != /* ]] && SCRIPT_PATH="${SCRIPT_DIR}/${SCRIPT_PATH}"
done
SCRIPT_PATH="$(readlink -f "${SCRIPT_PATH}")"
SCRIPT_DIR="$(cd -P "$(dirname -- "${SCRIPT_PATH}")" >/dev/null 2>&1 && pwd)"

让我们用上面的内容在*/中创建uniloc.sh*并执行它:

$ mkdir subdir1
$ ln --relative --symbolic . subdir1/linkdir
$ ln --relative --symbolic uniloc.sh subdir1/linkdir/linkuniloc.sh
$ . subdir1/linkdir/linkdiruniloc.sh
$ echo "${SCRIPT_PATH}"
/uniloc.sh
$ echo "${SCRIPT_DIR}"
/

首先,我们添加一个子目录,在其中我们创建一个名为linkdir的当前子目录的相对符号链接。在其中,我们为我们的脚本创建了另一个相对符号链接,称为linkdiruniloc.sh。获取脚本后,我们看到分配给当前 shell 中的SCRIPT_PATHSCRIPT_DIR变量的正确路径。

测试这个解决方案揭示了一个小缺陷:在我们运行上面的行之前,不允许更改目录。考虑到其包罗万象的性质,付出的代价很小: