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

标签 bash posix sh

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

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

最佳答案

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

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

请注意,在下面的代码段中,我已将dir更改为CDPATH=,因为它是better not to use all-uppercase variable names,以避免与环境变量和特殊的shell变量发生冲突。
原始命令中的> /dev/null前缀代替了$CDPATH:cd设置为空字符串,以确保-P从不回显任何内容。

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

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

如果您还想将生成的目录路径解析为其最终目标,以防目录和/或其组件是符号链接(symbolic link),请将pwd添加到foo命令中:
dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)

警告:这是,而不是,与查找脚本自己的原始来源
的真实目录相同:
假设您的脚本/usr/local/bin/foo$PATH中的/foodir/bin/foo符号链接(symbolic link),但其真实路径为/usr/local/bin
上面的代码仍会报告-P,因为符号链接(symbolic link)解析(/usr/local/bin)应用于目录readlink -f,而不是脚本本身。

要找到脚本自己的原始来源的真实目录,您必须检查脚本到的路径,以查看它是否是symlink ,如果是,请遵循(链)符号链接(symbolic link)到最终目标文件,然后解压缩目标文件规范路径中的目录路径。

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

实际上,没有没有POSIX实用程序来解决文件符号链接(symbolic link)
有一些方法可以解决此问题,但是它们很麻烦并且不够完善:

遵循POSIX的以下符合POSIX的 shell 函数实现了GNU的->所做的,并且是一种相当健壮的解决方案仅在两种罕见的极端情况下失败:

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

  • 使用名为readlink的此函数定义为,以下命令将确定脚本的原始目录的真实目录路径:
    dir=$(dirname -- "$(rreadlink "$0")")
    

    注意:如果您愿意假设存在一个(非POSIX)rreadlink()实用程序-它将覆盖macOS,FreeBSD和Linux-类似但更简单的解决方案可以在this answer中找到相关问题。
    command 源代码-脚本中调用
    之前的位置:

    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\n' "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\n' "${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\n' "$(command dirname -- "${targetDir}")"
      else
        command printf '%s\n' "${targetDir%/}/$fname"
      fi
    )
    

    为了健壮和可预测,该函数使用bash来确保仅调用shell内置函数或外部实用程序(忽略别名和函数形式的重载)。
    已经在以下Shell的最新版本中对进行了测试:dashkshzshzsh

    如何处理源调用:

    tl;博士:

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

  • 为了显示为什么无法做到这一点,让我们分析Walter A's answer的命令:
        # NOT recommended - see discussion below.
        DIR=$( cd -P -- "$(dirname -- "$(command -v -- "$0")")" && pwd -P )
    
  • (两个方面:
  • 重复使用pwd是多余的-与cd一起使用就足够了。
  • 如果碰巧设置了$CDPATH,则该命令缺少command -v -- "$0"的潜在stdout输出的沉默。)
  • command -v -- "$0"
  • $0旨在解决另一种情况:如果脚本是从交互式 shell 程序中获取的,则sh通常仅包含 shell 程序可执行文件的文件名(dirname),在这种情况下,.只会返回dirname(因为command -v -- "$0"始终会执行此操作)给定不带路径成分的参数时)。
    然后,$PATH通过/bin/sh查找(-)返回该 shell 的绝对路径。但是请注意,在某些平台上(例如OSX),登录shell的文件名带有$0(-sh)中的command -v -- "$0"前缀,在这种情况下command -v -- "$0"无法按预期工作(返回空字符串)。
  • 相反, sh在两种非源场景中可能会出现异常,在这种情况下,直接以脚本作为参数调用shell可执行文件command -v -- "$0":
  • (如果脚本本身不可执行):sh可能返回空字符串,具体取决于给定系统上特定的shell充当bash:kshzshdash返回空字符串;仅$0回显commandPOSIX spec. for command -v 并未明确说明bash在应用于文件系统路径时是否仅应返回可执行文件(kshzshcommand会执行此操作),但您可以辩称这是dash的用途所隐含的;奇怪的是,通常是最合规的POSIX公民ksh正在偏离此处的标准。相比之下,$PATH在这里是唯一的模型公民,因为它是唯一仅报告可执行文件并按照规范要求以绝对(尽管未标准化)路径报告它们的文件。
  • (如果脚本是可执行文件,但不是sh myScript可执行文件),并且调用仅使用文件名(例如command -v -- "$0"),则dash还将返回空字符串,但$0除外。
  • 鉴于脚本的来源无法确定脚本的目录-因为zsh然后不包含该信息(除了sh,通常不充当$0),因此没有很好的解决方案。
  • 在这种情况下,返回shell可执行文件的目录路径的用途有限-毕竟不是脚本的目录-除非以后在测试中使用该路径以确定脚本的来源。
  • 一种更可靠的方法是直接直接测试[ "$0" = "sh" ] || [ "$0" = "-sh" ] || [ "$0" = "/bin/sh" ]:$0
  • 但是,如果脚本是从另一个脚本(本身直接调用)中获取的,那也不起作用,因为command -v -- "$0"然后仅包含源脚本的路径。
  • 鉴于$dir在来源场景中的用处有限,并且它破坏了两个非来源场景的事实,因此我的投票是不使用它,这给我们留下了:
  • 涵盖了所有非来源的方案。
  • 在源调用中,您无法确定脚本的路径,并且最好在有限的情况下,您可以检测到是否在进行采购:
  • 如果直接由 shell 程序(例如,从 shell 程序配置文件/初始化文件中)获取,则.要么包含dirname,要么将 shell 程序可执行文件作为纯文件名调用(将.应用于纯文件名始终返回.),或者shell可执行文件的目录路径,否则。无法可靠地将$0与当前目录中的非源调用区分开。
  • 当从另一个脚本(本身也未获取)来获取时,$0包含该脚本的路径,并且正在获取的脚本无法判断是否是这种情况。


  • 背景资料:

    POSIX 定义$0相对于shell脚本
    here行为。

    本质上,$0应该反射(reflect)指定的脚本文件的路径,这意味着:
  • 不要依赖包含绝对路径$0
  • ~/bin/myScript仅在以下情况下包含绝对路径:
  • 您明确指定了绝对路径;例如。:
  • sh ~/bin/myScript(假设脚本本身是可执行的)
  • $PATH
  • 您仅通过文件名调用可执行脚本,这要求它既是可执行文件,又是myScript;在后台,系统将myScript # executes /home/jdoe/bin/myScript, for instance转换为绝对路径,然后执行它。例如。:
  • $0
  • 在所有其他情况下,sh将按照指定的反射(reflect)脚本路径:
  • 使用脚本显式调用sh myScript时,它可以仅仅是一个文件名(例如sh ./myScript)或相对路径(例如./myScript)
  • 直接调用可执行脚本时,这可以是相对路径(例如$PATH-请注意,仅文件名只能在bash中找到脚本)。

  • 实际上, dashkshzsh$0都表现出此行为。

    相比之下, POSIX在采购脚本时(使用特殊的内置实用程序.(“dot”))不要求$0的值,因此您不能依赖它,实际上,的行为有所不同跨壳。
  • 因此,在源代码中使用脚本并期望标准化行为时,您不能盲目使用bash
  • 实际上,在采购脚本时,dashksh$0保持$0不变,这意味着$0包含调用者的$0值,或更准确地说,包含调用链中最近未提供自身的调用者的$0值;因此,zsh可能指向 shell 程序的可执行文件,也可能指向源于当前脚本的另一个(直接调用)脚本的路径。
  • 相反,作为唯一反对者,$0实际上在$0中报告了当前脚本的路径。相反,$0将不提供有关脚本是否正在来源的指示。
  • 简而言之:仅使用POSIX功能,您既不能可靠地判断当前脚本是否是源程序,也不可以确定当前脚本的路径是什么,或者bash与当前脚本路径之间的关系是什么。
  • 如果您确实需要处理这种情况,则必须使用标识手边的特定 shell 并访问其特定的非标准功能:
  • kshzsh$0都提供了自己的方式来获取运行脚本的路径,即使是在获取源脚本的情况下也是如此。

  • 为了完整起见:在其他上下文中$0:
  • 在 shell 函数中,POSIX要求bash保持不变;因此,无论它在函数外部具有什么值,它也将在内部具有。
  • 实际上,dashkshzsh确实具有这种行为。
  • 同样,-c是唯一的反对者,它报告函数的名称。
  • 在启动时通过$0选项接受命令字符串的shell中,它是设置sh -c 'echo \$0: $0 \$1: $1' foo one # -> '$0: foo $1: one'的第一个操作数(非选项参数);例如。:
  • bash
  • dashkshzsh$0都采用这种方式。
  • 否则为,在不执行脚本文件的 shell 中, sh是 shell 的父进程传递了的第一个参数的值-通常是 shell 的名称或路径(例如/bin/sh-);这包括:
  • 交互式 shell
  • 警告:某些平台,特别是OSX,总是在创建交互式shell时创建登录shell,并在将$0放在shell名称之前添加$0,以便向shell发出信号,表明它是_login shell;因此,默认情况下,-bash在OSX的交互式shell中报告bash而不是sh < myScript
  • 一个从stdin 读取命令的 shell
  • 这也适用于通过stdin(例如bash)将脚本文件管道传输到 shell
  • dashkshzsh和ojit_code都采用这种方式。
  • 关于bash - 如何在POSIX sh中获取脚本目录?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29832037/

    相关文章:

    linux - 字符串中带有空格的环境变量 - 如何从/proc/pid/environ 中使用它们

    java - Java 垃圾收集器会停止我从 JNI 调用创建的 POSIX 线程吗?

    sockets - 是否accept()应该在失败时修改address_len?

    docker - 如何在Docker sh入口点修复 'Permission denied'

    c - 为什么在重定向到同一文件时会覆盖部分输出,但在重定向到终端时却不会?

    bash - 如何将变量中包含的多个值插入到 json 字符串中

    linux - 如何在 Linux POSIX shell 脚本中捕获 CTRL+Z;可能与否?

    linux bash 大于 code -gt 给出错误 [: too many arguments

    bash - 我在这里对这些变量做错了什么?

    c - 为什么这段代码会触发段错误?