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

Relative paths based on file location instead of current working directory

本问题已经有最佳答案,请猛点这里访问。

鉴于:

1
2
3
some.txt
dir
 |-cat.sh

其中cat.sh包含以下内容:

1
cat ../some.txt

然后在dir内运行./cat.sh,运行./dir/cat.shdir不在同一水平,运行得很好。我希望这是由于不同的工作目录。相对于cat.sh的位置,是否有一个简单的方法可以使路径../some.txt


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

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

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

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

请注意,此脚本仅在您直接调用脚本(即不通过symlink)时才起作用,否则查找脚本的当前位置会变得更复杂一些。


@MartinKonecny的答案提供了正确的答案,但正如他所提到的,只有当实际的脚本没有通过位于不同目录中的符号链接调用时,它才起作用。

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

linux/gnu readlink解决方案:

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

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

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

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

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

  • 有一个readlink实用程序,但缺少-f选项(从GNU的意义上来说,解决其最终目标的符号链接),例如macos。

    • MacOS使用了较早版本的readlink的BSD实现;请注意,freebsd/pc-bsd的最新版本确实支持-f
  • 甚至没有readlink,但有与posix兼容的实用程序,例如HP-UX(谢谢,@charles duffy)。

以下解决方案受https://stackoverflow.com/a/1116890/45375启发,定义helper shell函数rreadlink(),它在循环中解析给定的符号链接到其最终目标-此函数实际上是GNU readlink-e选项的符合POSIX的实现,与-f选项类似,但最终目标必须存在。

注意:该函数是一个bash函数,并且仅在使用带有posix兼容选项的posix实用程序的意义上符合posix。对于这个函数本身使用符合POSIX的shell代码编写的版本(对于/bin/sh),请参见这里。

  • 如果readlink可用,则在大多数现代平台上都会使用(无选项)。

  • 否则,将解析来自ls -l的输出,这是确定符号链接目标的唯一符合POSIX的方法。警告:如果一个文件名或路径包含文字子字符串->,这将中断,然而,这是不可能的。(请注意,缺少readlink的平台仍然可以提供其他非posix方法来解析符号链接;例如,@charles duffy提到了支持%l格式字符的HP-UX的find实用程序。由于它的-printfprimary;为了简洁起见,函数不尝试检测这种情况。)

  • 下面功能的可安装实用程序(脚本)形式(具有附加功能)可以在NPM注册表中找到为rreadlink;在Linux和MacOS上,使用[sudo] npm install -g rreadlink进行安装;在其他平台上(假设它们使用bash进行安装),请遵循手动安装说明。

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

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
50
51
52
53
54
#!/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
'
"$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
'
"${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
)

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


只有一条线可以。

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