【问题标题】:One-liner to determine the directory of the currently running bash script单行判断当前运行的 bash 脚本的目录
【发布时间】:2018-10-22 08:38:40
【问题描述】:

此问题与:Getting the source directory of a Bash script from within 有关 - 在该问题中,有一个完美的答案显示 show 以获取当前运行的 bash 脚本的目录(包括读取符号链接的实际位置)。

SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"

这很好用,除了它是很多样板代码。我正在开发一个脚本包,其中包含许多相互使用的小脚本。此脚本包托管在 git 存储库中,它必须与位置无关。例如。无论它被克隆到哪个目录,它都应该始终以相同的方式工作。必须能够将此 repo 克隆到许多不同的目录中并独立使用它们。许多脚本只是命令的包装器,或者它们正在调用在同一目录中或相对于包含目录的其他脚本。例如,这是一个模拟 'mongo' 命令的命令,但在 docker 容器中运行它:

#!/bin/bash

set -e

SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
# mongo-container is another script that determines the container id
# and it must be referenced relatively to the containing directory
MONGOE="docker exec -ti `${DIR}/mongo-container`"
${MONGOE} mongo "$@"

这段代码90%用于确定脚本的目录,只有10%做了一些有用的事情。我有大约 20 个这样的脚本。这意味着我 90% 的 bash 脚本只是“确定包含目录”。当我看着它们时,我几乎看不到它们在做什么,因为有太多的样板代码。一定有更好的方法,我就是想不通。

有什么想法吗?

【问题讨论】:

  • 大部分代码仅用于解析符号链接。你真的需要那个吗?我认为 dirname "$0" 对你来说应该足够了。
  • 这些小脚本将被符号链接到系统上各个用户的 ~/bin 目录。所以是的,阅读符号链接很重要。
  • 我很确定你不会得到比你链接的问题更好的结果。如果您需要重用代码,您可以在系统范围的函数或别名中声明它,甚至只是在主脚本执行之前系统调用的外部脚本
  • 通常情况下,当您安装代码时,您可以通过将代码放在众所周知的位置来避免这种情况,而不是让脚本找出您放置它的随机目录。
  • 你们都在说创建与位置无关的代码是个坏主意。脚本的位置是环境,脚本可以使用它。它只是碰巧在 bash 中很笨拙。它证明 bash 不是完成这项任务的完美工具。但这并不能证明与位置无关的代码不好。是否有充分的理由不编写与位置无关的代码?我不明白为什么“应该避免相对路径”?也许他们应该是,但我需要一个解释。

标签: bash


【解决方案1】:

请参阅BashFAQ/028 (How do I determine the location of my script? ...) 以获得对这个问题的良好处理。特别要注意第二段:“重要的是要认识到,在一般情况下,这个问题没有解决方案。你可能听说过的任何方法,以及将在下面详述的任何方法,都有缺陷并且只能在具体情况。首先,不依赖于脚本的位置,尽量完全避免该问题!"。

您的情况可能是一个足够好的解决方案。如果您的系统有一个支持-e 选项的readlink,请尝试:

prog_realpath=$(readlink -e -- "${BASH_SOURCE[0]}")
prog_realdir=${prog_realpath%/*}/

请注意,如果程序的“真实”路径以换行符结尾,prog_realpath 中的值将是错误的。这可以修复,但这种情况不太可能发生,并且不会阻止 prog_realdir 中的值正确。

请参阅Correct Bash and shell script variable capitalization,了解我更改SOURCE 和DIR 名称的原因。

prog_realdir 值后面的/ 是为了确保即使程序在根目录下也有效。使用dirname 获取目录将避免/ 问题,但除非使用额外的技巧,否则容易受到尾随换行问题的影响。有关尾随换行问题的更多信息,请参阅shell: keep trailing newlines ('\n') in command substitution。

如果您的系统不(全部)支持readlink -e,您可以改用realpath。有关readlink 和realpath 在多个操作系统上的可用性的更多信息,请参阅What's the difference between “realpath” and “readlink -f”。

【讨论】:

    【解决方案2】:

    也许您想在专用的通用 GNU/Bash 脚本中“分解”此源代码?

    首先,我认为您可以使用大多数 GNU 操作系统上都可用的 which 命令(通常无需额外安装)

    https://savannah.gnu.org/projects/which/

    然后您可以创建一个“通用功能”文件,其中包含用于定义完整路径的源代码,包括符号链接管理,我们将其命名为 /tmp/myTrueDir/common.sh:

    #!/bin/bash
    
    set -e
    
    # Usage: resolveCompleteAbsolutePath <$0 path to regard>
    function resolveCompleteAbsolutePath() {
      local SOURCE="$1"
      while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
        DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
        SOURCE="$(readlink "$SOURCE")"
        [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
      done
      DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null && pwd )"
    
      echo "$DIR"
    }
    

    然后您可以在所有小脚本的开头使用它(首先我们不需要管理符号链接以访问位于脚本同一目录中的 common.sh 文件)

    currentDir=$( dirname "$( which "$0" )" )
    source "$currentDir/common.sh"
    

    最终,您可以使用 resolveCompleteAbsolutePath 函数(在您的 common.sh 文件中定义)来获取完整路径,包括符号链接解析。

    这样一来,您的脚本将只包含您想要的有趣的源代码,并且路径管理将在同一个地方分解。

    例如,您可以使用此文件系统结构轻松测试所有这些:

    /tmp/myTrueDir/
    /tmp/myTrueDir/common.sh
    /tmp/myTrueDir/test.sh
    

    使用具有以下示例行的 /tmp/myTrueDir/test.sh 文件:

    #!/bin/bash
    
    currentDir=$( dirname "$( which "$0" )" )
    source "$currentDir/common.sh"
    resolvedPath=$( resolveCompleteAbsolutePath "$0" )
    
    echo "currentDir: $currentDir"
    echo "resolvedPath: $resolvedPath"
    

    然后你可以创建一个指向你的真实目录的符号链接,比如说:

    ln -s /tmp/myTrueDir /tmp/mySymbDir
    

    然后您可以从符号路径(即 /tmp/mySymbDir/test.sh)调用您的测试脚本,并查看它是否像您想要的那样工作:

    currentDir: /tmp/mySymbDir
    resolvedDir: /tmp/myTrueDir
    

    【讨论】:

    • 好吧,如果我们不关心符号链接,那么一个简单的目录名 $0 就可以了。但我需要注意,在这种情况下,您的代码肯定会失败。 which命令不能使用,请再读一遍问题。这些脚本必须与位置无关,而且它们可能根本不在 PATH 上。 (只有 一些 符号链接到其中 一些)
    • 我想我的第一个答案是模棱两可的,我对其进行了编辑,以便为您提供完整的解释,并附有工作示例。希望它能满足您的需求。
    • 请参阅 How to check if a program exists from a Bash script? 了解有关 which 问题的信息,以及改用什么。
    • 对于这样的应用程序,${BASH_SOURCE[0]} 优于 $0。见choosing between $0 and BASH_SOURCE。
    【解决方案3】:

    我倾向于在我的代码中加入以下行

    PROGDIR=$(cd $(dirname $0) && pwd)
    

    这将更改为脚本的目录,然后运行 ​​pwd 以获取目录路径。我从来没有遇到过这个问题。

    希望这会有所帮助。

    【讨论】:

    • 是的,除非脚本是从符号链接执行的,否则此方法有效。您似乎没有阅读整个问题。
    猜你喜欢
    • 2023-01-12
    • 2013-06-26
    • 2014-06-30
    • 2023-03-29
    • 1970-01-01
    • 1970-01-01
    • 2019-02-10
    • 2011-03-13
    • 2014-03-25
    相关资源
    最近更新 更多