【问题标题】:Relative paths based on file location instead of current working directory [duplicate]基于文件位置而不是当前工作目录的相对路径[重复]
【发布时间】:2014-07-29 13:08:36
【问题描述】:

给定:

some.txt
dir
 |-cat.sh

随着 cat.sh 的内容:

cat ../some.txt

然后在dir 中运行./cat.sh 可以正常工作,而在与dir 相同的级别上运行./dir/cat.sh 则不行。我希望这是由于不同的工作目录造成的。有没有一种简单的方法可以使路径../some.txt 相对于cat.sh 的位置?

【问题讨论】:

  • 这是 BashFAQ #28:mywiki.wooledge.org/BashFAQ/028
  • 这是一个非常常见的问题(即如何确定正在运行的脚本的位置)的变体。我在思考它是否真的足够不同以至于不能重复。
  • ...所以,鉴于脚本存储的位置(根据答案,这被标记为重复),只需 cd 到该目录,然后继续获得此处请求的效果。跨度>

标签: bash shell


【解决方案1】:

您要做的是获取脚本的绝对路径(可通过${BASH_SOURCE[0]} 获得),然后在脚本开头使用它来获取父目录和cd

#!/bin/bash
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )

cd "$parent_path"
cat ../some.text

这将使您的 shell 脚本独立于您从何处调用它。每次运行它,就好像你在 dir 中运行 ./cat.sh

请注意,此脚本仅在您直接调用脚本时才有效(即不通过符号链接),否则查找脚本的当前位置会有点棘手)

【讨论】:

  • cd到脚本目录,然后调用pwd来存储它,而不是直接将dirname的结果直接存储到parent_path,有什么原因吗?
  • @Gsinti pwd -P 解析路径中的任何符号链接,因此该语句返回规范路径。
  • 如果您收到cat.sh: 2: cat.sh: Bad substitution 的警告,请参阅此处:stackoverflow.com/questions/29832037/…
  • 提示:如果不想更改 cwd,他们也可以使用 pushd $parent_path 代替 cd 并在 cat 之后调用 popd
  • 您能否详细说明Bad substitution @CameronHudson 提供了另一个帖子的链接,但那里的答案又长又复杂:-( 谢谢!
【解决方案2】:

只要一行就可以了。

cat "`dirname $0`"/../some.txt

【讨论】:

  • 与 Martin 的回答相同的问题 - 需要更多引号,否则当 $0 包含空格、全局字符等时,这将表现得非常糟糕。
  • @CharlesDuffy 是的,绝对是。更多的报价会更好。
  • 这个答案的强大版本:cat "$(dirname "$0")/../some.txt"。请注意,它在 2 种情况下仍然会失败:(a) 如果脚本本身通过符号链接调用到位于 在不同目录中的 脚本; (b) 如果脚本是通过包含指向脚本目录的符号链接的路径调用的。在这两种情况下,.. 都不会引用脚本目录的实际父目录。除此之外,当一个脚本被 sourced 时,$0 将无法按预期工作; $BASH_SOURCE 是更好的选择。
  • @mklement0 我已经在答案中增强了它。
  • 谢谢,但$0 的未引用使用仍然是个问题。
【解决方案3】:

@Martin Konecny's answer 提供了正确的答案,但是 - 正如他所提到的 - 它仅在不通过驻留在 不同目录 中的 符号链接 调用实际脚本时才有效。

这个答案涵盖了这种情况:当脚本通过 symlink 甚至是 符号链接链 调用时也可以工作的解决方案:


Linux / GNU readlink 解决方案:

如果您的脚本只需要在 Linux 上运行,或者您知道 GNU readlink$PATH 中,请使用 readlink -f,它可以方便地解决指向其最终目标的符号链接:

 scriptDir=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")

请注意,GNU readlink 有 3 个相关选项用于将符号链接解析为其最终目标的完整路径:-f (--canonicalize)、-e (--canonicalize-existing) 和 -m ( --canonicalize-missing) - 见man readlink
由于此场景中存在定义的目标,因此可以使用 3 个选项中的任何一个;我在这里选择了-f,因为它是最知名的。


多(类 Unix)平台解决方案(包括具有 POSIX-only 实用程序集的平台):

如果您的脚本必须在满足以下条件的任何平台上运行:

  • 具有readlink 实用程序,但缺少-f 选项(在 GNU 意义上,将符号链接解析为其最终目标) - 例如,ma​​cOS

    • macOS 使用旧版本的 BSD 实现 readlink;请注意,FreeBSD/PC-BSD 的最新版本确实支持-f
  • 甚至没有 readlink,但有 POSIX 兼容 实用程序 - 例如,HP-UX(感谢@Charles Duffy)。

以下解决方案,灵感来自https://stackoverflow.com/a/1116890/45375, 定义 helper shell 函数 rreadlink(),它将给定的符号链接解析为循环中的最终目标 - 此函数实际上是 GNU readlink 的 POSIX 兼容实现987654348@ 选项,它类似于-f 选项,只是最终目标必须存在

注意:该函数是 bash 函数,仅在仅使用带有 POSIX 兼容选项的 POSIX 实用程序的意义上才兼容 POSIX。对于使用符合 POSIX 的 shell 代码(对于 /bin/sh)编写的此函数的自身版本,请参阅 here

  • 如果readlink 可用,则使用它(不带选项)- 在大多数现代平台上都是如此。

  • 否则,ls -l 的输出将被解析,这是确定符号链接目标的唯一符合 POSIX 标准的方法。
    警告:如果文件名或path 包含文字子字符串 -> - 然而,这不太可能。
    (请注意,缺少readlink 的平台可能仍会提供其他非POSIX 方法来解析符号链接;例如,@Charles Duffy 提到了HP-UX 的find 实用程序支持%l 格式字符。其-printf 主要; 为简洁起见,该函数不会尝试检测此类情况。)

  • 以下功能的可安装实用程序(脚本)形式(带有附加功能)可以在 rreadlink in the npm registry 中找到;在 Linux 和 macOS 上,使用[sudo] npm install -g rreadlink 安装它;在其他平台上(假设他们有bash),请关注manual installation instructions

如果参数是符号链接,则返回最终目标的规范路径;否则,返回参数自己的规范路径。

#!/usr/bin/env bash

# Helper function.
rreadlink() ( # execute function in a *subshell* to localize the effect of `cd`, ...

  local target=$1 fname targetDir readlinkexe=$(command -v readlink) CDPATH= 

  # Since we'll be using `command` below for a predictable execution
  # environment, we make sure that it has its original meaning.
  { \unalias command; \unset -f command; } &>/dev/null

  while :; do # Resolve potential symlinks until the ultimate target is found.
      [[ -L $target || -e $target ]] || { command printf '%s\n' "$FUNCNAME: 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 is defined
        # relative to the symlink's own directory.
        if [[ -n $readlinkexe ]]; then # Use `readlink`.
          target=$("$readlinkexe" -- "$fname")
        else # `readlink` utility not available.
          # Parse `ls -l` output, which, unfortunately, is the only POSIX-compliant 
          # way to determine a symlink's target. Hypothetically, this can break with
          # filenames containig literal ' -> ' and embedded newlines.
          target=$(command ls -l -- "$fname")
          target=${target#* -> }
        fi
        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
)

# Determine ultimate script dir. using the helper function.
# Note that the helper function returns a canonical path.
scriptDir=$(dirname -- "$(rreadlink "$BASH_SOURCE")")

【讨论】:

  • scriptDir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")") 让 shellcheck 开心。
  • @iElectric:(我最初误读了您与rreadlink 函数有关的评论)。是的,shellcheck.net 在引用技术上是 array 的变量时发出警告,但同时没有指定 index,在这种情况下是 1st 元素返回(这里,$BASH_SOURCE 等价于 ${BASH_SOURCE[0]})。虽然这样做可能是一个陷阱,但它也是一个方便的捷径,尤其是对于像 BASH_SOURCE 这样的变量,它几乎总是第一个感兴趣的元素。
猜你喜欢
  • 2019-03-08
  • 2020-03-19
  • 1970-01-01
  • 1970-01-01
  • 2012-07-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-22
相关资源
最近更新 更多