基于文件位置而不是当前工作目录的相对路径

ben*_*ben 82 bash shell

鉴于:

some.txt
dir
 |-cat.sh
Run Code Online (Sandbox Code Playgroud)

使用cat.sh的内容:

cat ../some.txt
Run Code Online (Sandbox Code Playgroud)

然后在./cat.sh内部运行dir工作正常,而不是./dir/cat.sh在同一级别上运行dir.我希望这是由于不同的工作目录.是否有一个容易的是../some.txt相对于位置的路径cat.sh

Mar*_*cny 118

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

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

cd "$parent_path"
cat ../some.text
Run Code Online (Sandbox Code Playgroud)

这将使您的shell脚本独立于您调用它的位置.每次运行它,就好像你在./cat.sh里面跑dir.

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

  • @Gsinti`pwd -P`解析路径中的所有符号链接,因此该语句返回规范路径. (5认同)
  • 提示:如果不想更改 cwd,也可以使用 `pushd $parent_path` 代替 `cd` 并在 `cat` 之后调用 `popd`。 (3认同)
  • 有没有理由为什么你`cd`到脚本目录然后调用`pwd`来存储它而不是直接将`dirname`的结果存储到`parent_path`? (2认同)

mkl*_*nt0 26

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

这个答案涵盖案例:一个解决方案时,脚本通过调用也适用符号链接甚至一个符号链接的链条:


Linux/GNUreadlink解决方案:

如果您的脚本只需要在Linux上运行,或者您知道GNUreadlink$PATH,则使用readlink -f,这可以方便地将符号链接解析为其最终目标:

 scriptDir=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
Run Code Online (Sandbox Code Playgroud)

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


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

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

  • 有一个readlink实用程序,但缺乏-f选项(在GNU意义上解析符号链接到其最终目标) - 例如,macOS.

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

以下解决方案受/sf/answers/78182331/的启发,定义了辅助shell函数,rreadlink()它将给定的符号链接解析为循环中的最终目标 - 此函数实际上是符合POSIX的实现GNU readlink-e选项,类似于-f选项,但最终目标必须存在.

注意:该函数是一个bash函数,仅在使用POSIX兼容POSIX实用程序的意义上才符合POSIX标准.对于此函数的一个版本,它本身是用符合POSIX的shell代码(for /bin/sh)编写的,请参见此处.

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

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

  • 可以在npm注册表中找到以下函数的可安装实用程序(脚本)形式(具有附加功能); 在Linux和macOS上,安装它; 在其他平台上(假设他们有),请按照手动安装说明进行操作. rreadlink[sudo] npm install -g rreadlinkbash

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

#!/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")")
Run Code Online (Sandbox Code Playgroud)

  • `scriptDir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")` 以使 shellcheck 满意。 (2认同)

Nan*_*ncy 7

只要一行就可以了。

cat "`dirname $0`"/../some.txt
Run Code Online (Sandbox Code Playgroud)

  • 与 Martin 的回答相同的问题——需要更多的引号,否则当 `$0` 包含空格、glob 字符等时,这会表现得非常糟糕。 (3认同)
  • 这个答案的强大版本:`cat "$(dirname "$0")/../some.txt"`。请注意,在 2 种情况下它仍然会失败:(a) 如果脚本本身是通过符号链接调用的 _script_ 位于 _in 不同的目录_;(b) 如果脚本是通过包含指向脚本 _directory_ 的符号链接的路径调用的。在这两种情况下,`..` 都不会引用脚本目录的实际父目录。除此之外,当脚本是 _sourced_ 时,`$0` 不会按预期工作;`$BASH_SOURCE` 是更好的选择。 (3认同)