如何在POSIX sh中获取脚本目录?

我在我的bash脚本中有以下代码。 现在我想在POSIX sh中使用它。 那么如何转换呢? 谢谢。

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )"

$BASH_SOURCE的POSIX-shell( sh )副本是$0 。 请参阅底部获取背景信息

警告 :最重要的区别是, 如果您的脚本正在源代码中(通过.加载到当前shell)下面的代码片段将无法正常工作。 下面进一步解释

请注意,我在下面的代码片段中将DIR更改为dir ,因为最好不要使用全大写变量名称,以避免与环境变量和特殊shell变量发生冲突。
在原始命令中, CDPATH= prefix取代> /dev/null$CDPATH被设置为一个空字符串,以确保cd从不回应任何东西。

在最简单的情况下,这会做( 相当于OP的命令 ):

dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)

如果您还想将生成的目录路径解析为其最终目标,以防目录和/或其组件是符号链接 ,请将-P添加到pwd命令中:

dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)

警告 :这是一样的发现脚本自身起源的真正目录
假设你的脚本foo被连接到$PATH中的/usr/local/bin/foo ,但它的真实路径是/foodir/bin/foo
上面仍然会报告/usr/local/bin ,因为符号链接解析( -P )应用于目录/usr/local/bin ,而不是脚本本身。

要查找脚本自己的真正的原始目录,必须检查脚本的路径以查看它是否是符号链接,如果是,请遵循(链)符号链接到最终目标文件,然后从中提取目录路径目标文件的规范路径。

GNU的readlink -f (更好: readlink -e )可以为你做,但readlink不是POSIX实用程序。
虽然包括OSX在内的BSD平台也有一个readlink实用程序,但在OSX上它不支持-f的功能。 也就是说,要显示如果readlink -f可用,任务变得readlink -f简单: dir=$(dirname "$(readlink -f -- "$0")")

实际上, 没有用于解析文件符号链接的POSIX实用程序 。 有办法可以解决这个问题,但它们很麻烦,而且不完全健壮:

以下符合POSIX标准的shell函数实现了GNU的readlink -e是一个相当健壮的解决方案仅在两种罕见边缘情况下失败

  • 嵌入换行符的路径(非常罕见)
  • 包含文字字符串的文件名-> (也很少见)
  • 有了这个名为rreadlink函数, 下面的函数确定了脚本的原始路径

    dir=$(dirname -- "$(rreadlink "$0")")
    

    rreadlink() 源代码 - 在脚本调用之前放置它

    rreadlink() ( # Execute the function in a *subshell* to localize variables and the effect of `cd`.
    
      target=$1 fname= targetDir= CDPATH=
    
      # Try to make the execution environment as predictable as possible:
      # All commands below are invoked via `command`, so we must make sure that `command`
      # itself is not redefined as an alias or shell function.
      # (Note that command is too inconsistent across shells, so we don't use it.)
      # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not even have
      # an external utility version of it (e.g, Ubuntu).
      # `command` bypasses aliases and shell functions and also finds builtins 
      # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for that
      # to happen.
      { unalias command; unset -f command; } >/dev/null 2>&1
      [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too.
    
      while :; do # Resolve potential symlinks until the ultimate target is found.
          [ -L "$target" ] || [ -e "$target" ] || { command printf '%sn' "ERROR: '$target' does not exist." >&2; return 1; }
          command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path.
          fname=$(command basename -- "$target") # Extract filename.
          [ "$fname" = '/' ] && fname='' # !! curiously, `basename /` returns '/'
          if [ -L "$fname" ]; then
            # Extract [next] target path, which may be defined
            # *relative* to the symlink's own directory.
            # Note: We parse `ls -l` output to find the symlink target
            #       which is the only POSIX-compliant, albeit somewhat fragile, way.
            target=$(command ls -l "$fname")
            target=${target#* -> }
            continue # Resolve [next] symlink target.
          fi
          break # Ultimate target reached.
      done
      targetDir=$(command pwd -P) # Get canonical dir. path
      # Output the ultimate target's canonical path.
      # Note that we manually resolve paths ending in /. and /.. to make sure we have a normalized path.
      if [ "$fname" = '.' ]; then
        command printf '%sn' "${targetDir%/}"
      elif  [ "$fname" = '..' ]; then
        # Caveat: something like /var/.. will resolve to /private (assuming /var@ -> /private/var), i.e. the '..' is applied
        # AFTER canonicalization.
        command printf '%sn' "$(command dirname -- "${targetDir}")"
      else
        command printf '%sn' "${targetDir%/}/$fname"
      fi
    )
    

    为了健壮和可预测,该函数使用command来确保只调用shell内建函数或外部实用程序(忽略以别名和函数形式的重载)。
    它已在最近版本的以下shell中进行了测试: bashdashkshzsh


    如何处理源调用:

    tl; dr

    仅使用POSIX功能:

  • 无法确定 源代码调用中 的脚本路径 (除非在zsh中,但通常不会像sh )。
  • 可以检测您的脚本是否仅在脚本直接由shell (例如在shell概要文件/初始化文件;可能通过一系列的源代码)中进行采购时,通过将$0与shell可执行文件名称/路径(在zsh除外,其中$0是真正的当前脚本路径)。 相比之下(除了zsh ),源自另一个本身直接调用的脚本的脚本包含该脚本的$0路径。
  • 为了解决这些问题, bashkshzsh具有非标准功能 ,即使在源方案中也可以确定实际的脚本路径,还可以检测脚本是否正在源代码中; 例如,在bash$BASH_SOURCE总是包含正在运行的脚本的路径,无论它是否来源,并且可以使用[[ $0 != "$BASH_SOURCE" ]]来测试脚本是否来源。
  • 为了说明为什么不能这样做 ,让我们分析一下Walter A的答案:

        # NOT recommended - see discussion below.
        DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )
    
  • (旁白:
  • 使用-P两次是多余的 - 使用pwd就足够了。
  • 如果$CDPATH恰好设置,该命令缺少cd的潜在标准输出静音。)
  • command -v -- "$0"
  • command -v -- "$0"旨在涵盖一个额外的场景:如果脚本来自交互式shell,则$0通常包含shell可执行文件( sh )的简单文件名,在这种情况下, dirname将简单地返回. (因为这是dirname在给定没有路径组件的情况下总是会执行的操作)。 command -v -- "$0"然后通过$PATH查找( /bin/sh )返回该shell的绝对路径。 但是,请注意,某些平台(例如OSX)上的登录shell的文件名前缀为- in $0-sh ),在这种情况下, command -v -- "$0"不能按预期工作(返回一个空字符串)。
  • 相反, command -v -- "$0"在两个非源代码的情况下可能会出错,这些情况下可直接调用shell可执行文件sh ,并将脚本作为参数:
  • 如果脚本本身不可执行: command -v -- "$0"可能会返回一个空字符串,具体取决于特定shell在给定系统上作为sh作用: bashkshzsh返回一个空字符串; 只有dash回应$0
    POSIX规范。 for command并没有明确地说出command -v是否应用于文件系统路径时,应该只返回可执行文件 - 这就是bashkshzsh所做的 - 但是您可以争辩说它是由command目的所暗示的; 奇怪的是, dash通常是最符合标准的POSIX公民,这里偏离了标准。 相比之下, ksh是这里唯一的模型公民,因为它是唯一一个仅报告可执行文件并按照规范要求以绝对路径(尽管不是标准化)报告路径的公民。
  • 如果脚本是可执行的,但不在$PATH ,并且调用使用其单纯的文件名(例如sh myScript ),则command -v -- "$0"也将返回空字符串,除了dash
  • 考虑到脚本的源代码无法确定脚本的目录 - 因为$0不包含这些信息(除了zsh ,通常不会像sh ) - 这个问题没有好的解决方案。
  • 在这种情况下返回shell可执行文件的目录路径的用途有限 - 毕竟,它不是脚本的目录 - 除了稍后可能在测试中使用该路径来确定脚本是否来源之外。
  • 更可靠的方法是直接简单测试$0[ "$0" = "sh" ] || [ "$0" = "-sh" ] || [ "$0" = "/bin/sh" ] [ "$0" = "sh" ] || [ "$0" = "-sh" ] || [ "$0" = "/bin/sh" ]
  • 但是,如果脚本来自另一个脚本(本身直接调用它),那么即使这样也行不通,因为$0只包含脚本的路径。
  • 鉴于command -v -- "$0"在源代码方案中的有用性有限,以及它打破了两个非源方案的事实, 我的投票是不使用它 ,这留下了我们:
  • 所有非源方案都包含在内。
  • 在源调用中,您无法确定脚本的路径,并且至多在有限的情况下,您可以检测是否发生采购:
  • 当直接由shell(例如从shell配置文件/初始化文件)获取时, $dir最终会包含. 如果外壳可执行文件被调用作为一个单纯的文件名(在应用dirname到只有文件名总是返回. ),或外壳可执行文件的目录路径,否则。 . 不能可靠地从当前目录中的非源调用区分开来。
  • 当源自另一个脚本(本身不是来源)时, $0包含该脚本的路径,并且源代码的脚本无法判断是否属于这种情况。

  • 背景信息:

    POSIX在这里定义了$0相对于shell脚本行为

    实质上, $0应该反映脚本文件的路径,这意味着:

  • 不要依赖包含绝对路径的$0
  • 只有在以下情况下$0包含绝对路径

  • 你明确指定了一个绝对路径; 例如:
  • ~/bin/myScript (假设脚本本身是可执行的)
  • sh ~/bin/myScript
  • 你可以通过mere文件名来调用一个可执行的脚本,它需要它们都是可执行文件并且在$PATH ; 在幕后,系统将myScript转换为绝对路径,然后执行它; 例如:
  • myScript # executes /home/jdoe/bin/myScript, for instance
  • 在所有其他情况下, $0按照指定反映脚本路径

  • 当使用脚本显式调用sh时,这可以仅仅是一个文件名(例如sh myScript )或一个相对路径(例如sh ./myScript
  • 当直接调用可执行脚本时,这可以是一个相对路径(例如,./ ./myScript - 请注意,单纯的文件名只能在$PATH找到脚本)。
  • 实际上, bashdashkshzsh都表现出这种行为。

    相比之下,POSIX不强制的价值$0采购的脚本 (使用特殊的内置工具 . (“点”)),所以你不能依赖它 ,并在实践中, 行为跨越炮弹有所不同。

  • 因此,当您的脚本来源并且期望标准化行为时,您不能盲目使用$0
  • 在实践中, bashdashksh在采购脚本时保持$0不变,这意味着$0包含调用者的$0值,或者更准确地说,调用链中最近一次调用者的$0值没有被发现; 因此, $0可能指向shell的可执行文件或指向源自当前文件的另一个(直接调用的)脚本的路径。
  • 相比之下,作为唯一的反对者, zsh实际上报告当前脚本的路径为$0 。 相反, $0不会提供脚本是否来源的指示。
  • 简而言之,仅使用POSIX功能,您既不能可靠地判断手头的脚本是否正在获取,也不能确定脚本的路径是什么,也不能确定$0与当前脚本路径之间的关系。
  • 如果您确实需要处理这种情况,则必须识别具体的外壳并访问其特定的非标准功能
  • bashkshzsh都提供了自己的方式来获取正在运行的脚本的路径,即使它正在被获取。
  • 为了完整起见: 在其他情况下$0价值

  • 在shell函数内部 ,POSIX强制$0保持不变; 因此,无论它在功能之外有什么价值,它都会在里面。
  • 实际上, bashdashksh确实表现得那么好。
  • 同样, zsh是唯一的反对者,并报告函数的名称。
  • 在启动时通过-c选项接受命令字符串的shell中,它是第一个操作数(非选项参数),它设置$0 ; 例如:
  • sh -c 'echo $0: $0 $1: $1' foo one # -> '$0: foo $1: one'
  • bashdashkshzsh都表现得那样。
  • 否则 ,在不执行脚本文件的shell中, $0是shell父进程传递的第一个参数的值 - 通常是shell的名称或路径(例如sh/bin/sh ); 这包括:
  • 一个交互式shell
  • 警告 :一些平台,特别是OSX,在创建交互式shell时总是创建登录shell,并且在将shell放入$0之前预先挂载-以便向shell发出它是_login shell的信号; 因此,默认情况下, $0在OSX的交互式shell中报告-bash ,而不是bash
  • 一个从stdin读取命令的shell
  • 这也适用于通过stdin将脚本文件传送到shell(例如sh < myScript
  • bashdashkshzsh都表现得那样。

  • @City回应说

    DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )
    

    作品。 我也使用它。
    我在https://stackoverflow.com/questions/760110/can-i-get-the-absolute-path-to-the-cu rrent-script-in-kornshell中找到了该命令。

    链接地址: http://www.djcxy.com/p/56779.html

    上一篇: how to get script directory in POSIX sh?

    下一篇: How can I increase the MAX