OS X的Bash脚本绝对路径


98

我正在尝试获取OS X上当前正在运行的脚本的绝对路径。

我看到了许多答复readlink -f $0。但是,由于OS X readlink与BSD相同,因此它根本不起作用(它与GNU版本兼容)。

有没有现成的解决方案吗?





16
$( cd "$(dirname "$0")" ; pwd -P )
詹森·S

Answers:


88

有一个realpath()C函数可以完成这项工作,但是我在命令行上看不到任何可用的东西。这是一个快速而肮脏的替代品:

#!/bin/bash

realpath() {
    [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}

realpath "$0"

如果以开头,则会逐字打印路径/。如果不是,它必须是相对路径,因此它$PWD位于最前面。该#./部分./从的前面剥离$1


1
我也注意到了C函数,但是找不到任何相应的二进制或任何东西。无论如何,您的功能非常适合我的需求。谢谢!
强大的橡皮鸭

7
请注意,这不会取消引用符号链接。
亚当·范登堡

17
realpath ../something返回$PWD/../something
Lri

这对我有用,但是我将其重命名为“ realpath_osx()”,因为我可能需要将此bash脚本发送到linux shell,并且不希望它与“真实” realpath相冲突!(我敢肯定有一种更优雅的方法,但我是bash n00b。)
Andrew Theken 2012年

8
command -v realpath >/dev/null 2>&1 || realpath() { ... }
卡拉·布莱特威尔

115

这三个简单的步骤将解决此问题以及许多其他OS X问题:

  1. 安装自制软件
  2. brew install coreutils
  3. grealpath .

(3)可能更改为just realpath,请参阅(2)输出


3
这可以解决问题,但是需要安装您可能不需要或不需要的东西,或者可能不在目标系统上的东西,例如OS X
Jason S

6
@JasonS GNU Coreutils太好了,无法使用。它包含许多强大的实用程序。事实是如此的好,没有它,Linux内核就无用了,为什么有人将其称为GNU / Linux。Coreutils很棒。极好的答案,这应该是选定的答案。

7
@omouse该问题专门提到“现成的解决方案”(对于OS X)。无论coreutils多么出色,它在OS X上都不是“开箱即用”的,因此答案不是一个好答案。这不是coreutils的性能好坏,还是它比OS X更好的问题。有一种“开箱即用”的解决方案$( cd "$(dirname "$0")" ; pwd -P )对我来说很好。
詹森·S

2
OS X的“功能”之一是目录和文件名不区分大小写。因此,如果你有一个名为目录XXX,有一种人cd xxx,然后pwd将返回.../xxx。除了这个答案,以上所有解决方案都会xxx在您真正想要的时候返回XXX。谢谢!
安德鲁(Andrew)

1
我不知道无休止地复制,粘贴和重塑代码比现有的程序包管理器解决方案更好。如果需要realpath,那么当您几乎肯定需要其他物品时会发生什么coreutils?也用bash重写这些功能吗?:P
以西结·维克多

28

啊。由于某些原因,我发现先前的答案有点想要:特别是,它们不能解析多个级别的符号链接,并且它们极度“重击”。尽管最初的问题确实明确要求使用“ Bash脚本”,但同时也提到了Mac OS X的类似BSD的非GNU脚本readlink。因此,这里尝试进行一些合理的可移植性(我已经用bash的“ sh”和dash对其进行了检查),解决了任意数量的符号链接;并且它也应该与路径中的空格一起使用,尽管我不确定该实用程序本身的基本名称是否存在空格,所以也许吧,避免这种情况吗?

#!/bin/sh
realpath() {
  OURPWD=$PWD
  cd "$(dirname "$1")"
  LINK=$(readlink "$(basename "$1")")
  while [ "$LINK" ]; do
    cd "$(dirname "$LINK")"
    LINK=$(readlink "$(basename "$1")")
  done
  REALPATH="$PWD/$(basename "$1")"
  cd "$OURPWD"
  echo "$REALPATH"
}
realpath "$@"

希望对某人有用。


1
我只建议对local函数内部定义的变量使用,以免污染全局名称空间。例如local OURPWD=...。至少对bash有效。
Michael Paesold

另外,代码不应将大写字母用于私有变量。大写变量保留供系统使用。
三人房

感谢您的脚本。如果链接和实际文件的基本名称不同,则最好BASENAME=$(basename "$LINK") 在while内添加一个while并在第二个LINK setter和REALPATH setter中使用它
stroborobo

这不能完全处理符号链接和..父引用realpath。随着自制程序coreutils安装,试ln -s /var/log /tmp/linkexamplerealpath /tmp/linkexample/../; 这印/private/var。但是您的函数会生成/tmp/linkexample/..,因为..它不是符号链接。
马丁·彼得斯

10

Python解决方案的一个更加命令行友好的变体:

python -c "import os; print(os.path.realpath('$1'))"

2
万一任何人都疯狂到足以为单个命令启动python解释器……
Bachsau

我很生气,但习惯了python -c "import os; import sys; print(os.path.realpath(sys.argv[1]))"
Alex Chamberlain

7

我一直在寻找一种用于系统配置脚本的解决方案,即在安装Homebrew之前运行。缺乏适当的解决方案,我只是将任务卸载到了跨平台语言,例如Perl:

script_abspath=$(perl -e 'use Cwd "abs_path"; print abs_path(@ARGV[0])' -- "$0")

通常,我们实际上想要的是包含目录:

here=$(perl -e 'use File::Basename; use Cwd "abs_path"; print dirname(abs_path(@ARGV[0]));' -- "$0")

大!Perl的开销很小!您可能可以将第一个版本简化为FULLPATH=$(perl -e "use Cwd 'abs_path'; print abs_path('$0')")。有什么理由反对吗?
F Pereira

@FPereira从未转义的用户提供的字符串生成程序代码从来都不是一个好主意。''不是防弹的。例如,如果$0包含单引号,则会中断。一个非常简单的示例:在中尝试使用您的版本/tmp/'/test.sh,并按/tmp/'/test.sh其完整路径进行调用。
4ae1e1

甚至更简单/tmp/'.sh
4ae1e1

6

正如其他人指出的那样,这是一条真实的道路

// realpath.c
#include <stdio.h>
#include <stdlib.h>

int main (int argc, char* argv[])
{
  if (argc > 1) {
    for (int argIter = 1; argIter < argc; ++argIter) {
      char *resolved_path_buffer = NULL;
      char *result = realpath(argv[argIter], resolved_path_buffer);

      puts(result);

      if (result != NULL) {
        free(result);
      }
    }
  }

  return 0;
}

生成文件:

#Makefile
OBJ = realpath.o

%.o: %.c
      $(CC) -c -o $@ $< $(CFLAGS)

realpath: $(OBJ)
      gcc -o $@ $^ $(CFLAGS)

然后使用编译make并放入软链接:
ln -s $(pwd)/realpath /usr/local/bin/realpath


我们可以做gcc realpath.c -o /usr/local/bin/realpath吗?
亚历山大·米尔斯

1
@AlexanderMills您不应该以root身份运行编译器。然后,如果没有,您将没有特权写信/usr/local/bin
Tripleee

6

使用Python来获取它:

#!/usr/bin/env python
import os
import sys

print(os.path.realpath(sys.argv[1]))

2
abs_path () {    
   echo "$(cd $(dirname "$1");pwd)/$(basename "$1")"
}

dirname将给出目录名称/path/to/file,即/path/to

cd /path/to; pwd 确保路径是绝对的。

basename将只提供文件名/path/to/file,即file


尽管此代码可以回答问题,但提供有关此代码为何和/或如何回答问题的其他上下文,可以提高其长期价值。
Igor F.

1

因此,如您在上面看到的,我在大约6个月前对此进行了拍摄。我完全忘记了它,直到我再次发现自己需要类似的东西。看到它多么原始,我 感到非常震惊。大约一年以来,我一直在自学密集的代码,但是当情况最糟糕时,我常常觉得自己根本没有学到任何东西。

我将删除上面的“解决方案”,但我真的很喜欢它可以记录我过去几个月真正学到的知识。

但是我离题了。昨晚我坐下来,全力以赴。评论中的解释应该足够。如果您想跟踪我将继续处理的副本,则可以遵循以下要点。这可能会满足您的需求。

#!/bin/sh # dash bash ksh # !zsh (issues). G. Nixon, 12/2013. Public domain.

## 'linkread' or 'fullpath' or (you choose) is a little tool to recursively
## dereference symbolic links (ala 'readlink') until the originating file
## is found. This is effectively the same function provided in stdlib.h as
## 'realpath' and on the command line in GNU 'readlink -f'.

## Neither of these tools, however, are particularly accessible on the many
## systems that do not have the GNU implementation of readlink, nor ship
## with a system compiler (not to mention the requisite knowledge of C).

## This script is written with portability and (to the extent possible, speed)
## in mind, hence the use of printf for echo and case statements where they
## can be substituded for test, though I've had to scale back a bit on that.

## It is (to the best of my knowledge) written in standard POSIX shell, and
## has been tested with bash-as-bin-sh, dash, and ksh93. zsh seems to have
## issues with it, though I'm not sure why; so probably best to avoid for now.

## Particularly useful (in fact, the reason I wrote this) is the fact that
## it can be used within a shell script to find the path of the script itself.
## (I am sure the shell knows this already; but most likely for the sake of
## security it is not made readily available. The implementation of "$0"
## specificies that the $0 must be the location of **last** symbolic link in
## a chain, or wherever it resides in the path.) This can be used for some
## ...interesting things, like self-duplicating and self-modifiying scripts.

## Currently supported are three errors: whether the file specified exists
## (ala ENOENT), whether its target exists/is accessible; and the special
## case of when a sybolic link references itself "foo -> foo": a common error
## for beginners, since 'ln' does not produce an error if the order of link
## and target are reversed on the command line. (See POSIX signal ELOOP.)

## It would probably be rather simple to write to use this as a basis for
## a pure shell implementation of the 'symlinks' util included with Linux.

## As an aside, the amount of code below **completely** belies the amount
## effort it took to get this right -- but I guess that's coding for you.

##===-------------------------------------------------------------------===##

for argv; do :; done # Last parameter on command line, for options parsing.

## Error messages. Use functions so that we can sub in when the error occurs.

recurses(){ printf "Self-referential:\n\t$argv ->\n\t$argv\n" ;}
dangling(){ printf "Broken symlink:\n\t$argv ->\n\t"$(readlink "$argv")"\n" ;}
errnoent(){ printf "No such file: "$@"\n" ;} # Borrow a horrible signal name.

# Probably best not to install as 'pathfull', if you can avoid it.

pathfull(){ cd "$(dirname "$@")"; link="$(readlink "$(basename "$@")")"

## 'test and 'ls' report different status for bad symlinks, so we use this.

 if [ ! -e "$@" ]; then if $(ls -d "$@" 2>/dev/null) 2>/dev/null;  then
    errnoent 1>&2; exit 1; elif [ ! -e "$@" -a "$link" = "$@" ];   then
    recurses 1>&2; exit 1; elif [ ! -e "$@" ] && [ ! -z "$link" ]; then
    dangling 1>&2; exit 1; fi
 fi

## Not a link, but there might be one in the path, so 'cd' and 'pwd'.

 if [ -z "$link" ]; then if [ "$(dirname "$@" | cut -c1)" = '/' ]; then
   printf "$@\n"; exit 0; else printf "$(pwd)/$(basename "$@")\n"; fi; exit 0
 fi

## Walk the symlinks back to the origin. Calls itself recursivly as needed.

 while [ "$link" ]; do
   cd "$(dirname "$link")"; newlink="$(readlink "$(basename "$link")")"
   case "$newlink" in
    "$link") dangling 1>&2 && exit 1                                       ;;
         '') printf "$(pwd)/$(basename "$link")\n"; exit 0                 ;;
          *) link="$newlink" && pathfull "$link"                           ;;
   esac
 done
 printf "$(pwd)/$(basename "$newlink")\n"
}

## Demo. Install somewhere deep in the filesystem, then symlink somewhere 
## else, symlink again (maybe with a different name) elsewhere, and link
## back into the directory you started in (or something.) The absolute path
## of the script will always be reported in the usage, along with "$0".

if [ -z "$argv" ]; then scriptname="$(pathfull "$0")"

# Yay ANSI l33t codes! Fancy.
 printf "\n\033[3mfrom/as: \033[4m$0\033[0m\n\n\033[1mUSAGE:\033[0m   "
 printf "\033[4m$scriptname\033[24m [ link | file | dir ]\n\n         "
 printf "Recursive readlink for the authoritative file, symlink after "
 printf "symlink.\n\n\n         \033[4m$scriptname\033[24m\n\n        "
 printf " From within an invocation of a script, locate the script's "
 printf "own file\n         (no matter where it has been linked or "
 printf "from where it is being called).\n\n"

else pathfull "$@"
fi

1
要点链接似乎已断开。
亚历克斯(Alex)

这个答案有效:stackoverflow.com/a/46772980/1223975 ....您应该只使用它。
亚历山大·米尔斯

请注意,您的实现不会在..父引用之前解析符号链接。例如,/foo/link_to_other_directory/..被解析为/foo符号链接/foo/link_to_other_directory指向的路径的父级,而不是父级。readlink -f并从根开始realpath解析每个路径组件,并将前置链接目标更新为仍在处理的其余路径。我为这个问题添加了一个答案,重新实现了这一逻辑。
马丁·彼得斯

1

适用于Mac OS X的realpath

realpath() {
    path=`eval echo "$1"`
    folder=$(dirname "$path")
    echo $(cd "$folder"; pwd)/$(basename "$path"); 
}

相关路径示例:

realpath "../scripts/test.sh"

主文件夹示例

realpath "~/Test/../Test/scripts/test.sh"

1
一个很好的简单解决方案,我只是发现一个警告,用..它来调用时并不能产生正确的答案,因此我添加了一个检查:给定的路径是否是目录: if test -d $path ; then echo $(cd "$path"; pwd) ; else [...]
herbert

不是为我"$(dirname $(dirname $(realpath $0)))"工作,而是为我工作,所以还需要其他东西……
亚历山大·米尔斯

没用使用echo也不应该犯。
Tripleee

这实际上并不能像realpath那样完全解析符号链接。它解析符号链接之前而不是之后解析..父引用。尝试在安装了Homebrew的情况下尝试此操作,使用创建链接,然后运行;这印。但是您的函数却产生,因为仍然在cd之后显示。coreutilsln -s /var/log /tmp/linkexamplerealpath /tmp/linkexample/..//private/var/tmp/linkexample/..pwd/tmp/linkexample
马丁·彼得斯

1

在macOS上,我找到的唯一可以可靠处理符号链接的解决方案是使用realpath。鉴于此brew install coreutils,我只是自动化了这一步。我的实现如下所示:

#!/usr/bin/env bash

set -e

if ! which realpath >&/dev/null; then
  if ! which brew >&/dev/null; then
    msg="ERROR: This script requires brew. See https://brew.sh for installation instructions."
    echo "$(tput setaf 1)$msg$(tput sgr0)" >&2
    exit 1
  fi
  echo "Installing coreutils/realpath"
  brew install coreutils >&/dev/null
fi

thisDir=$( dirname "`realpath "$0"`" )
echo "This script is run from \"$thisDir\""


如果尚未brew安装,则会出现此错误,但您也可以安装它。我只是不愿意自动化从网上卷曲任意红宝石代码的东西。

请注意,这是Oleg Mikheev的答案的自动变体。


一项重要的测试

这些解决方案中的任何一个的良好测试是:

  1. 将代码放在脚本文件中的某个位置
  2. 在另一个目录中,将symlink(ln -s)链接到该文件
  3. 从该符号链接运行脚本

解决方案是否取消引用符号链接,并给您原始目录?如果是这样,那就行了。


0

这似乎适用于OSX,不需要任何二进制文件,并且已从此处撤出

function normpath() {
  # Remove all /./ sequences.
  local path=${1//\/.\//\/}

  # Remove dir/.. sequences.
  while [[ $path =~ ([^/][^/]*/\.\./) ]]; do
    path=${path/${BASH_REMATCH[0]}/}
  done
  echo $path
}

0

我喜欢这个:

#!/usr/bin/env bash
function realpath() {
    local _X="$PWD"
    local _LNK=$1
    cd "$(dirname "$_LNK")"
    if [ -h "$_LNK" ]; then
        _LNK="$(readlink "$_LNK")"
        cd "$(dirname "$_LNK")"
    fi
    echo "$PWD/$(basename "$_LNK")"
    cd "$_X"
}

0

我需要一个realpath在OS X上的更换,一个与符号链接和家长参考路径是否正常工作,就像readlink -f。这包括解析父引用之前解析路径中的符号链接;例如,如果您已经安装了自制coreutils瓶,则运行:

$ ln -s /var/log/cups /tmp/linkeddir  # symlink to another directory
$ greadlink -f /tmp/linkeddir/..      # canonical path of the link parent
/private/var/log

请注意,解析父目录引用之前readlink -f已解决该问题。当然,在Mac上没有。/tmp/linkeddir ..readlink -f

因此,作为bash实现的一部分,realpath我在Bash 3.2中重新实现了GNUlib canonicalize_filename_mode(path, CAN_ALL_BUT_LAST)函数调用的功能。这也是GNU readlink -f进行的函数调用:

# shellcheck shell=bash
set -euo pipefail

_contains() {
    # return true if first argument is present in the other arguments
    local elem value

    value="$1"
    shift

    for elem in "$@"; do 
        if [[ $elem == "$value" ]]; then
            return 0
        fi
    done
    return 1
}

_canonicalize_filename_mode() {
    # resolve any symlink targets, GNU readlink -f style
    # where every path component except the last should exist and is
    # resolved if it is a symlink. This is essentially a re-implementation
    # of canonicalize_filename_mode(path, CAN_ALL_BUT_LAST).
    # takes the path to canonicalize as first argument

    local path result component seen
    seen=()
    path="$1"
    result="/"
    if [[ $path != /* ]]; then  # add in current working dir if relative
        result="$PWD"
    fi
    while [[ -n $path ]]; do
        component="${path%%/*}"
        case "$component" in
            '') # empty because it started with /
                path="${path:1}" ;;
            .)  # ./ current directory, do nothing
                path="${path:1}" ;;
            ..) # ../ parent directory
                if [[ $result != "/" ]]; then  # not at the root?
                    result="${result%/*}"      # then remove one element from the path
                fi
                path="${path:2}" ;;
            *)
                # add this component to the result, remove from path
                if [[ $result != */ ]]; then
                    result="$result/"
                fi
                result="$result$component"
                path="${path:${#component}}"
                # element must exist, unless this is the final component
                if [[ $path =~ [^/] && ! -e $result ]]; then
                    echo "$1: No such file or directory" >&2
                    return 1
                fi
                # if the result is a link, prefix it to the path, to continue resolving
                if [[ -L $result ]]; then
                    if _contains "$result" "${seen[@]+"${seen[@]}"}"; then
                        # we've seen this link before, abort
                        echo "$1: Too many levels of symbolic links" >&2
                        return 1
                    fi
                    seen+=("$result")
                    path="$(readlink "$result")$path"
                    if [[ $path = /* ]]; then
                        # if the link is absolute, restart the result from /
                        result="/"
                    elif [[ $result != "/" ]]; then
                        # otherwise remove the basename of the link from the result
                        result="${result%/*}"
                    fi
                elif [[ $path =~ [^/] && ! -d $result ]]; then
                    # otherwise all but the last element must be a dir
                    echo "$1: Not a directory" >&2
                    return 1
                fi
                ;;
        esac
    done
    echo "$result"
}

它包括循环符号链接检测,如果两次(相同)路径相同,则退出。

如果您需要的只是readlink -f,则可以将以上内容用作:

readlink() {
    if [[ $1 != -f ]]; then  # poor-man's option parsing
        # delegate to the standard readlink command
        command readlink "$@"
        return
    fi

    local path result seenerr
    shift
    seenerr=
    for path in "$@"; do
        # by default readlink suppresses error messages
        if ! result=$(_canonicalize_filename_mode "$path" 2>/dev/null); then
            seenerr=1
            continue
        fi
        echo "$result"
    done
    if [[ $seenerr ]]; then
        return 1;
    fi
}

对于realpath,我还需要--relative-to--relative-base支持,这将为您提供规范化后的相对路径:

_realpath() {
    # GNU realpath replacement for bash 3.2 (OS X)
    # accepts --relative-to= and --relative-base options
    # and produces canonical (relative or absolute) paths for each
    # argument on stdout, errors on stderr, and returns 0 on success
    # and 1 if at least 1 path triggered an error.

    local relative_to relative_base seenerr path

    relative_to=
    relative_base=
    seenerr=

    while [[ $# -gt 0 ]]; do
        case $1 in
            "--relative-to="*)
                relative_to=$(_canonicalize_filename_mode "${1#*=}")
                shift 1;;
            "--relative-base="*)
                relative_base=$(_canonicalize_filename_mode "${1#*=}")
                shift 1;;
            *)
                break;;
        esac
    done

    if [[
        -n $relative_to
        && -n $relative_base
        && ${relative_to#${relative_base}/} == "$relative_to"
    ]]; then
        # relative_to is not a subdir of relative_base -> ignore both
        relative_to=
        relative_base=
    elif [[ -z $relative_to && -n $relative_base ]]; then
        # if relative_to has not been set but relative_base has, then
        # set relative_to from relative_base, simplifies logic later on
        relative_to="$relative_base"
    fi

    for path in "$@"; do
        if ! real=$(_canonicalize_filename_mode "$path"); then
            seenerr=1
            continue
        fi

        # make path relative if so required
        if [[
            -n $relative_to
            && ( # path must not be outside relative_base to be made relative
                -z $relative_base || ${real#${relative_base}/} != "$real"
            )
        ]]; then
            local common_part parentrefs

            common_part="$relative_to"
            parentrefs=
            while [[ ${real#${common_part}/} == "$real" ]]; do
                common_part="$(dirname "$common_part")"
                parentrefs="..${parentrefs:+/$parentrefs}"
            done

            if [[ $common_part != "/" ]]; then
                real="${parentrefs:+${parentrefs}/}${real#${common_part}/}"
            fi
        fi

        echo "$real"
    done
    if [[ $seenerr ]]; then
        return 1
    fi
}

if ! command -v realpath > /dev/null 2>&1; then
    # realpath is not available on OSX unless you install the `coreutils` brew
    realpath() { _realpath "$@"; }
fi

我在针对此代码的代码审查请求中包含了单元测试。


-2

基于与评论者的沟通,我同意这非常困难并且没有实现Realpath的简单方法,其行为与Ubuntu完全相同。

但是以下版本可以处理无法解决的极端情况,并满足我在Macbook上的日常需求。将此代码放入〜/ .bashrc中并记住:

  • arg只能是1个文件或目录,不能使用通配符
  • 目录或文件名中没有空格
  • 至少文件或目录的父目录存在
  • 随时使用。.. /东西,这些很安全

    # 1. if is a dir, try cd and pwd
    # 2. if is a file, try cd its parent and concat dir+file
    realpath() {
     [ "$1" = "" ] && return 1

     dir=`dirname "$1"`
     file=`basename "$1"`

     last=`pwd`

     [ -d "$dir" ] && cd $dir || return 1
     if [ -d "$file" ];
     then
       # case 1
       cd $file && pwd || return 1
     else
       # case 2
       echo `pwd`/$file | sed 's/\/\//\//g'
     fi

     cd $last
    }

您要避免对的无用使用echo。只是pwd不一样的echo $(pwd),而不必产卵壳的第二个副本。另外,不引用参数to echo是一个错误(您将丢失任何前导或尾随空格,任何相邻的内部空格字符以及通配符展开等)。详情请参见stackoverflow.com/questions/10067266/…–
Tripleee,

同样,不存在的路径的行为也有问题。但是我想这就是“记住”一句话的意思。尽管在Ubuntu上的行为肯定是当您请求realpath不存在的目录时不打印当前目录。
Tripleee'1

为了保持一致性,可能更喜欢使用dir=$(dirname "$1"); file=$(basename "$1")过时的反引号语法。还要再次注意正确引用参数。
Tripleee

您更新后的答案似乎无法修复许多错误,并添加了新的错误。
三人

请给我一些具体的失败案例,因为我在ubuntu 18.04桌面上进行的所有测试都可以,谢谢。
occia
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.