使用 OS X 的 Bash 脚本绝对路径

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

我看到很多人回复 readlink -f $0。然而,由于 OS X 的 readlink与 BSD 的 readlink相同,它就是不能工作(它可以工作在 GNU 的版本中)。

有没有现成的解决办法?

122127 次浏览

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

#!/bin/bash


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


realpath "$0"

如果路径以 /开头,它将逐字打印该路径。如果不是,它必须是一个相对路径,所以它前面的 $PWD#./部分从 $1的前面剥离 ./

我发现这个答案有点欠缺,原因有以下几点:
特别是,它们不能解决多层次的符号链接,而且它们非常“ Bash-y”。

虽然最初的问题确实明确要求使用“ Bash 脚本”,但它也提到了 Mac OS X 类似于 BSD 的非 GNU readlink

因此,这里尝试了一些合理的可移植性(我已经用 bash 作为‘ sh’和 dash 检查了它) ,解析了任意数量的符号链接; 它还应该在路径中使用空格。

这个答案是以前编辑过的,重新添加了 local的抨击。这个问题的答案是一个可移植的 POSIX 解决方案。为了解决变量作用域的问题,我对它进行了编辑,将其更改为子 shell 函数,而不是内联函数。请不要编辑。

#!/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 "$@"

希望对某些人有用。

正如你们看到的,我在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

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

  1. 安装 自酿的
  2. brew install coreutils
  3. grealpath .

(3)可改为仅 realpath,见(2)输出

我正在寻找一个解决方案,在系统提供脚本中使用,也就是说,运行之前,家酿甚至安装。由于缺乏合适的解决方案,我只能将任务转移到跨平台语言,例如 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")

使用 Python 来获取:

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


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

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

python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' ./my/path

正如其他人所指出的,由于存在 真实路径:

// 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

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"

基于与评论者的交流,我同意这是非常困难的,没有实现一个真实路径行为完全相同的 Ubuntu。

但下面的版本,可以处理角落情况最好的答案不能满足我在 Macbook 上的日常需求。把这段代码放到你的 ~/中。记住:

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

    # 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
}

在 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. 运行脚本 从那个符号链接

这个解决方案是否解除了对符号链接的引用,并给出了原始目录? 如果是的话,它是可行的。

这似乎为 OSX 工作,不需要任何二进制文件,并拉 从这里

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


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

我喜欢这个:

#!/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"
}
abs_path () {
echo "$(cd $(dirname "$1");pwd)/$(basename "$1")"
}

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

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

basename将只给出 /path/to/file中的文件名,即 file

我需要一个在 OS X 上的 realpath替代品,一个可以正确操作带有符号链接和父引用 就像 readlink -f一样的路径的 realpath替代品。这包括解析路径 之前中的符号链接解析父引用; 例如,如果你已经安装了自制的 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已经解析了 /tmp/linkeddir 之前,解析了 ..父目录引用。当然,Mac 都不是上没有 readlink -f

因此,作为 realpath Bash 实现的一部分,我在 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

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

我检查每一个回答,但错过了最好的一个(恕我直言)由贾森7月14日16日3:12,离开评论领域。

因此,这里是,以防像我这样的人有检查回答的倾向,没有时间去浏览每一条评论:

$( cd "$(dirname "$0")" ; pwd -P )

帮助:

NAME
pwd -- return working directory name


SYNOPSIS
pwd [-L | -P]


DESCRIPTION
The pwd utility writes the absolute pathname of the current working
directory to the standard output.


Some shells may provide a builtin pwd command which is similar or identi-
cal to this utility.  Consult the builtin(1) manual page.


The options are as follows:


-L      Display the logical current working directory.


-P      Display the physical current working directory (all symbolic
links resolved).

对于那些在 Mac 上使用 bash 的 nodejs 开发人员:

realpath() {
node -p "fs.realpathSync('$1')"
}

只是一个使用 pushd的想法:

realpath() {
eval echo "$(pushd $(dirname "$1") | cut -d' ' -f1)/$(basename "$1")"
}

eval用于扩展波浪,如 ~/Downloads