优秀的编程知识分享平台

网站首页 > 技术文章 正文

shell中如何确定脚本的位置?这篇文章告诉你

nanyue 2024-07-20 23:55:17 技术文章 9 ℃

我想从同一个位置读取一些配置文件,如何确定脚本的位置?。

这个问题的出现主要是由两个原因引发的:一是您希望将脚本的数据或配置进行外部化,因此需要一种方式来寻找这些外部资源;二是您的脚本需要对某些捆绑资源(如构建脚本)进行操作,因此需要确定这些资源的位置。

关键在于,你必须明白在通常情况下,这个问题并没有一个标准的解决方案。您或许听过一些方式,或者下文也会详细介绍几种可能的方法,但这些方法都未必完美,仅在特定的场景下有效。首要的一点是,尽量避免过度依赖脚本的位置来解决问题!

在我们详细解读解决方案之前,有必要先消除一些常见的误区。这里有两点很重要的概念需要明确:
首先,你的脚本实际上并没有一个固定的“位置”!不论脚本的字节数据从何而来,都不存在一个“规范路径”。这是肯定的。
其次,$0并不是您问题的答案。如果您坚持认为这是解决方案,那您可以选择此刻停止阅读,继续在错误的道路上探索,或者,您也可以接收这个观点,然后继续阅读下文。

我需要访问我的数据/配置文件

很多时候,人们希望使他们的脚本可配置。分离原则告诉我们,将配置和代码分开是一个好主意。然而,问题是,我的脚本如何知道在哪里找到用户的配置文件?

很多人错误地认为脚本的配置应该与脚本放置在同一个目录中。这正是问题所在。

有一个UNIX的范例可以帮助解决这个问题:您的脚本的配置文件应该存在于用户的主目录或/etc目录中。这样,您的脚本就可以使用绝对路径查找文件,立即解决了问题:您不再依赖于脚本的“位置”。

示例代码

 if [[ -e ~/.myscript.conf ]]; then
     source ~/.myscript.conf
 elif [[ -e /etc/myscript.conf ]]; then
     source /etc/myscript.conf
 fi

对于其他类型的数据文件也是如此。日志应写入/var/log目录或用户的主目录。支持文件应安装到文件系统中的绝对路径,或者与配置文件一起放置在/etc目录或用户的主目录中。

我需要访问与脚本捆绑的文件

有时候脚本是一个“捆绑包”的一部分,并在其中执行特定的操作。这通常适用于在捆绑目录中解压或包含的应用程序。用户可以在任意位置解压或安装捆绑包;理想情况下,捆绑包的脚本应该能够在家目录、/var/tmp或/usr/local等任何地方正常运行。这些文件是临时的,没有固定或可预测的位置。

当脚本需要独立于其绝对位置访问其他捆绑文件时,我们有两个选项:要么依赖PWD,要么依赖BASH_SOURCE。这两种方法都有一些问题;以下是你需要了解的内容。

使用BASH_SOURCE

BASH_SOURCE是bash的内部变量,实际上是一个路径名数组。如果将其作为简单字符串展开,例如"$BASH_SOURCE",你会得到第一个元素,它是当前执行的函数或脚本的路径名。使用BASH_SOURCE方法,你可以这样访问捆绑包中的文件:

切换行号显示

# 进入捆绑包目录并使用相对路径
if [[ $BASH_SOURCE = */* ]]; then
    cd -- "${BASH_SOURCE%/*}/" || exit
fi
read somevar < etc/somefile

切换行号显示

# 直接使用dirname,无需改变目录
if [[ $BASH_SOURCE = */* ]]; then
    bundledir=${BASH_SOURCE%/*}/
else
    bundledir=./
fi
read somevar < "${bundledir}etc/somefile"

请注意,在使用BASH_SOURCE时,以下注意事项适用:

  • 当bash无法确定代码的来源时,$BASH_SOURCE会展开为空。通常,这意味着代码来自标准输入(例如ssh host 'somecode')或交互式会话。
  • $BASH_SOURCE不会跟随符号链接(当你在/x/y中运行z时,即使z是指向/p/q/r的符号链接,你也会得到/x/y/z)。通常,这是期望的效果。然而,有时候并非如此。想象一下,你的软件包将其启动脚本链接到了/usr/local/bin。现在,该脚本的BASH_SOURCE将引导你进入/usr/local,而不是进入软件包。

如果你不是在编写bash脚本,就无法使用BASH_SOURCE变量。然而,有一个常见的约定,即在启动脚本时将脚本的位置作为进程名称传递。大多数shell都会这样做,但并非所有shell都能可靠地实现,也不是所有shell都会尝试将相对路径解析为绝对路径。依赖这种行为是危险且脆弱的,但可以通过查看$0来实现(参见下面)。在执行此操作之前,请务必考虑所有选项:你很可能会制造更多问题而不是解决问题。

使用当前工作目录(PWD)

另一个选择是依赖当前工作目录(PWD)。在这种情况下,你可以假设用户已经首先进入了你的资源包目录,并使所有路径名都相对于该目录。使用PWD方法,你可以像这样访问资源包中的文件:

read somevar < etc/somefile                 
# 使用相对于PWD的路径名
read somevar < "${PWD%/}/etc/somefile"      
# 如果需要绝对路径名,扩展PWD

bundledir=$PWD                              
# 如果你的脚本中需要进行cd操作,可以保存PWD
cd /somewhere/else
read somefile < "${bundledir%/}/etc/somefile"

为了减少脆弱性,你甚至可以测试脚本名称的相对路径是否正确,以确保用户确实已经进入了资源包目录:

if [[ ! -e bin/myscript ]]; then
    echo >&2 "Please cd into the bundle before running this script."
    exit 1
fi

你还可以尝试一些启发式方法,以防用户位于资源包的上一级目录:

if [[ ! -e bin/myscript ]]; then
    if [[ -d mybundle-1.2.5 ]]; then
        cd mybundle-1.2.5 || {
            echo >&2 "Bundle directory exists but I can't cd there."
            exit 1
        }
    else
        echo >&2 "Please cd into the bundle before running this script."
        exit 1
    fi
fi

如果你确实需要绝对路径,你可以通过在相对路径前加上PWD/result.csv"

这里唯一的困难是你强制用户在脚本能够正常运行之前进入资源包的目录。不管怎样,这可能是你最好的选择!

使用配置文件/包装器

如果既不感兴趣使用BASH_SOURCE也不感兴趣使用PWD选项,你可以考虑使用配置文件的方法(参见前面的部分)。在这种情况下,你要求用户在配置文件中设置资源包的位置,并让他将该配置文件放在你能够轻松找到的位置。例如:

[[ -e ~/.myscript.conf ]] || {
    echo >&2 "First configure the product in ~/.myscript.conf"
    exit 1
}

# ~/.myscript.conf defines something like bundleDir=/x/y
source ~/.myscript.conf

[[ $bundleDir ]] || {
    echo >&2 "Please define bundleDir='/some/path' in ~/.myscript.conf"
    exit 1
}

cd "$bundleDir" || {
    printf >&2 'Could not cd to <%s>\n' "$bundleDir"
    exit 1
}

# 现在你可以使用PWD方法:使用相对路径。

这种方法的变体是使用一个配置你的资源包位置的包装器。你不是直接调用资源包中的脚本,而是在标准系统PATH中安装一个包装器,该包装器进入资源包目录并从那里调用真正的脚本,然后可以安全地使用上面介绍的PWD方法:

#!/usr/bin/env bash
cd /path/to/where/bundle/was/installed
exec "bin/realscript"

为什么$0不是一个可靠的选项

通常,通过预定义变量$0获取脚本位置的常见方式是依赖于脚本的名称。然而,遗憾的是,通过$0?提供脚本名称只是一种(常见的)约定,并非强制要求。实际上,$0?并不表示脚本的位置,而是由父进程确定的进程名称。它可以是任何值。

有人可能会说,在某些shell中,?$0?始终是绝对路径,即使您使用相对路径或根本不使用路径来调用脚本也是如此。然而,这种行为在不同的shell中并不可靠;其中一些shell(包括BASH)返回的是用户实际输入的命令,而不是完全限定的路径。这只是问题的冰山一角!

考虑到您的脚本实际上可能根本不存在于本地可访问的磁盘上。想象一下以下情况:

ssh remotehost bash < ./myscript

在remotehost上运行的shell是通过管道获取命令的。bash无法在任何磁盘上找到脚本。

此外,即使您的脚本存储在本地磁盘上并执行,它也可能会移动。在您输入命令和脚本检查?$0?之间的时间窗口内,有人可能将脚本移动到另一个位置。或者在同一时间窗口内,有人可能删除了脚本的链接,因此它实际上不再存在于文件系统中。

(这可能听起来有些牵强,但实际上非常常见。假设脚本被安装在/opt/foobar/bin目录中,并且在某个时刻,有人升级了foobar到一个新版本。他们可能会删除整个/opt/foobar/目录结构,或者在放置新版本之前,将/opt/foobar/bin/foobar脚本移动到一个临时名称。因此,即使使用"使用lsof查找shell正在使用的标准输入文件"的方法,仍然会失败。)

即使脚本在本地磁盘上的固定位置,使用$0?的方法仍然存在一些重要缺点。最重要的是,脚本名称(在?$0?中可见)可能与当前工作目录无关,而是相对于程序搜索路径PATH中的某个目录(这在KornShell中经常出现)。或者(这很可能是最常见的问题...)可能存在多个链接指向脚本,其中一个是从常见的PATH目录(如/usr/local/bin)创建的简单符号链接,这就是脚本被调用的方式。您的脚本可能位于/opt/foobar/bin/script,但是简单地读取?$0?的方式无法告诉您这一点——它可能显示为/usr/local/bin/script。

有些人会尝试通过readlink -f "?$0?"来解决符号链接的问题。然而,这种方法在某些情况下可能有效,但并不是绝对可靠的。因为任何读取?$0?的方法都不可能是百分之百可靠的,因为?$0?本身就是不可靠的。此外,readlink是非标准的,可能在某些平台上不可用。

更多

如果您觉得文章内容对你有一点帮助可以关注我,我在头条平台会持续分享更多实用的shell技巧和最佳实践,如果想系统的快速学习shell的各种高阶用法和生产环境避坑指南可以看看《shell脚本编程最佳实践》专栏,专栏里有更多的实用小技巧和脚本代码分享。

最近发表
标签列表