关于bash:如何在POSIX中获取脚本目录?

how to get script directory in POSIX sh?


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

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



$BASH_SOURCE的POSIX-shell(sh)对应是$0。请参阅底部的背景信息


警告:关键的区别在于,如果您的脚本被采购(使用.加载到当前shell中),下面的代码段将无法正常工作。进一步解释如下


请注意,我在下面的代码段中将DIR更改为DIR,因为最好不要使用全大写变量名,以避免与环境变量和特殊shell变量发生冲突。
CDPATH=前缀取代原始命令中的> /dev/null$CDPATH设置为空字符串,以确保cd永远不会回显任何内容。


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

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


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

1
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实用程序。
虽然BSD平台(包括OSX)也有readlink实用程序,但在OSX上它不支持-f的功能。也就是说,要显示如果readlink -f可用,任务变得多么简单:dir=$(dirname"$(readlink -f --"$0")")


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


以下,符合POSIX标准的shell函数实现了GNU的readlink -e所做的功能,并且是一个相当强大的解决方案,只在两种罕见的边缘情况下失败:

  • 嵌入换行符的路径(非常罕见)
  • 包含文字字符串->的文件名(也很少见)

  • 使用名为rreadlink的此函数定义,以下内容确定脚本的真实目录路径:

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    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 '%s
    '
    "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 '%s
    '
    "${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 '%s
    '
    "$(command dirname --"${targetDir}")"
      else
        command printf '%s
    '
    "${targetDir%/}/$fname"
      fi
    )


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

    如何处理源调用:


    TL;博士:


    仅使用POSIX功能:

  • 您无法在源调用中确定脚本的路径(zsh除外,但zsh通常不会作为sh)。
  • 如果您的脚本是由shell直接提供的(例如在shell配置文件/初始化文件中;可能通过一连串的源代码),通过将$0与shell可执行文件进行比较,您可以检测脚本是否仅来源名称/路径(zsh除外,其中,如上所述$0确实是当前脚本的路径)。相比之下(zsh除外),源自另一个本身直接调用的脚本的脚本在$0中包含该脚本的路径。
  • 为了解决这些问题,bashkshzsh具有非标准功能,即使在源方案中也可以确定实际的脚本路径,还可以检测脚本是否来源;例如,在bash中,$BASH_SOURCE始终包含正在运行的脚本的路径,无论它是否来源,并且[[ $0 !="$BASH_SOURCE" ]]可用于测试脚本是否来源。

  • 为了说明为什么不能这样做,让我们分析Walter A答案中的命令:

    1
    2
        # NOT recommended - see discussion below.
        DIR=$( cd -P --"$(dirname --"$(command -v --"$0")")" && pwd -P )

  • (两个旁边:

  • 使用-P两次是多余的 - 将它与pwd一起使用就足够了。
  • 如果$CDPATH碰巧被设置,则该命令缺少cd的潜在stdout输出的静默。)
  • command -v --"$0"

  • command -v --"$0"旨在涵盖一个额外的场景:如果脚本来自交互式shell,$0通常只包含shell可执行文件的文件名(sh),在这种情况下,dirname将只返回< x5>(因为当给出没有路径组件的参数时,dirname总是会这样做)。
    command -v --"$0"然后通过$PATH查找(/bin/sh)返回该shell的绝对路径。但请注意,某些平台(例如OSX)上的登录shell在$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只是包含了源脚本的路径。
  • 鉴于command -v --"$0"在源场景中的有用性有限,以及它打破了两个非源场景的事实,我的投票是不使用它,这让我们:

  • 涵盖所有非源方案。
  • 在源调用中,您无法确定脚本的路径,并且在有限的情况下,您可以检测是否正在进行采购:

  • 当直接由shell(例如来自shell配置文件/初始化文件)获取时,$dir最终包含.,如果shell可执行文件仅作为文件名调用(将dirname应用于纯文件名始终返回.),否则为shell可执行文件的目录路径。无法可靠地将.与当前目录中的非源调用区分开来。
  • 当源自另一个脚本(本身也不是源代码)时,$0包含该脚本的路径,而源代码的脚本无法判断是否是这种情况。
  • 背景资料:


    POSIX在这里定义了关于shell脚本的$0的行为。


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

  • 不要依赖包含绝对路径的$0

  • $0仅在以下情况下包含绝对路径:

  • 你明确指定一个绝对路径;例如。:

  • ~/bin/myScript(假设脚本本身是可执行的)
  • sh ~/bin/myScript
  • 你只需要文件名来调用一个可执行文件,这要求它既可以执行又可以在$PATH中;在幕后,系统将myScript转换为绝对路径,然后执行它;例如。:

  • myScript # executes /home/jdoe/bin/myScript, for instance

  • 在所有其他情况下,$0将反映指定的脚本路径:

  • 当用脚本显式调用sh时,这可能只是文件名(例如,sh myScript)或相对路径(例如,sh ./myScript)
  • 当直接调用可执行脚本时,这可以是相对路径(例如,./myScript - 请注意,仅仅文件名只会在$PATH中找到脚本)。

  • 实际上,bashdashkshzsh都表现出这种行为。


    相比之下,POSIX在获取脚本时不会强制使用$0的值(使用特殊的内置实用程序.("dot")),因此您不能依赖它,并且在实践中,行为会有所不同炮弹。

  • 因此,当您的脚本被采购并期望标准化行为时,您不能盲目地使用$0

  • 实际上,bashdashksh在获取脚本时保持$0不变,这意味着$0包含调用者的$0值,或者更确切地说,$0值包含呼叫链中最近的呼叫者本身并未采购;因此,$0可以指向shell的可执行文件,也可以指向源于当前shell的另一个(直接调用的)脚本的路径。
  • 相比之下,作为唯一的反对者,zsh实际上会在$0中报告当前脚本的路径。相反,$0将不提供脚本是否来源的指示。
  • 简而言之:仅使用POSIX功能,既不能可靠地判断手头的脚本是否来源,也不能说明手头的脚本是什么,也不知道$0与当前脚本路径的关系是什么。
  • 如果确实需要处理这种情况,则必须确定手头的特定shell并访问其特定的非标准功能:

  • 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,并在将-放入$0之前将-添加到shell名称之前,以便向shell发出信号,告知它是_login shell;因此,默认情况下,$0在OSX上的交互式shell中报告-bash,而不是bash
  • 从stdin读取命令的shell

  • 这也适用于通过stdin将脚本文件传递给shell(例如,sh < myScript)
  • bashdashkshzsh都表现得那样。
  • 好。


    1
    2
    3
    4
    if      OLDPWD=/dev/fd/0 \
            cd - && ls -lLidFH ?
    then    cd . <8
    fi      </proc/self/fd 8<. 9<$0


    那里。这应该使您能够通过一些魔术链接更改directpry作为文件描述符。


    设置$OLDPWD pre- cd导出一个更改目录的持续时间的值(注意:cd可能对hash表有残留影响,但唯一的sh我知道实际是男性任何好用的都是kevin ahlmquists - 并且因为herbert xu - dash,也许是一些bsd的东西,但我知道什么?)但是由于改变而没有带来任何cd导出。


    因此,$OLDPWD实际上并没有改变,如果它有任何价值,那么它仍然保持不变。作为第一个cd的结果,$PWD被更改,并且值变为/dev/fd/0,其指向/proc/self/fd,其中我们的进程的文件描述符列表应该在.中,以包含$0 >在./2上。


    所以我们做了ls ... ?,看看我们可以得到的所有精彩信息,我们从哪里来。


    好极了!



    @City回答说

    1
    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找到了这个命令。