简体   繁体   English

Bash 脚本绝对路径与 OS X

[英]Bash script absolute path with OS X

I am trying to obtain the absolute path to the currently running script on OS X.我正在尝试获取 OS X 上当前正在运行的脚本的绝对路径。

I saw many replies going for readlink -f $0 .我看到很多回复readlink -f $0 However since OS X's readlink is the same as BSD's, it just doesn't work (it works with GNU's version).然而,由于 OS X 的readlink与 BSD 的相同,它只是不起作用(它适用于 GNU 的版本)。

Is there an out-of-the-box solution to this?有开箱即用的解决方案吗?

These three simple steps are going to solve this and many other OS X issues:这三个简单的步骤将解决这个问题和许多其他 OS X 问题:

  1. Install Homebrew安装Homebrew
  2. brew install coreutils
  3. grealpath .

(3) may be changed to just realpath , see (2) output (3) 可以改为只是realpath ,见 (2) 输出

There's a realpath() C function that'll do the job, but I'm not seeing anything available on the command-line.有一个realpath() C 函数可以完成这项工作,但我在命令行上看不到任何可用的东西。 Here's a quick and dirty replacement:这是一个快速而肮脏的替代品:

#!/bin/bash

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

realpath "$0"

This prints the path verbatim if it begins with a / .如果路径以/开头,则会逐字打印路径。 If not it must be a relative path, so it prepends $PWD to the front.如果不是,则它必须是相对路径,因此它会在前面添加$PWD The #./ part strips off ./ from the front of $1 . #./部分从$1的前面./

I found the answer a bit wanting for a few reasons:由于以下几个原因,我发现答案有点缺乏:
in particular, they don't resolve multiple levels of symbolic links, and they are extremely "Bash-y".特别是,它们不解析多个级别的符号链接,并且它们非常“Bash-y”。

While the original question does explicitly ask for a "Bash script", it also makes mention of Mac OS X's BSD-like, non-GNU readlink .虽然最初的问题确实明确要求“Bash 脚本”,但它也提到了 Mac OS X 的类似 BSD 的非 GNU readlink

So here's an attempt at some reasonable portability (I've checked it with bash as 'sh' and dash), resolving an arbitrary number of symbolic links;所以这里尝试了一些合理的可移植性(我已经用 bash 作为“sh”和破折号进行了检查),解析任意数量的符号链接; and it should also work with whitespace in the path(s).它也应该与路径中的空格一起使用。

This answer was previously edited, re-adding the local bashism.这个答案以前被编辑过,重新添加了local bashism。 The point of this answer is a portable, POSIX solution.这个答案的重点是一个可移植的 POSIX 解决方案。 I have edited it to address variable scoping by changing it to a subshell function, rather than an inline one.我已经编辑它以通过将其更改为子 shell 函数而不是内联函数来解决变量作用域。 Please do not edit.请不要编辑。

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

Hope that can be of some use to someone.希望这对某人有用。

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

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

Use Python to get it:使用 Python 获取:

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

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

I was looking for a solution for use in a system provision script, ie, run before Homebrew is even installed.我正在寻找一种用于系统配置脚本的解决方案,即在 Homebrew 安装之前运行。 Lacking a proper solution I'd just offload the task to a cross-platform language, eg, Perl:缺乏适当的解决方案,我只是将任务卸载到跨平台语言,例如 Perl:

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

More often what we actually want is the containing directory:更多时候我们真正想要的是包含目录:

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

Since there is a realpath as others have pointed out:由于其他人指出存在真实路径

// 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:生成文件:

#Makefile
OBJ = realpath.o

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

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

Then compile with make and put in a soft link with:然后用make编译并放入一个软链接:
ln -s $(pwd)/realpath /usr/local/bin/realpath

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

dirname will give the directory name of /path/to/file , ie /path/to . dirname将给出/path/to/file的目录名称,即/path/to

cd /path/to; pwd cd /path/to; pwd ensures that the path is absolute. cd /path/to; pwd确保路径是绝对的。

basename will give just the filename in /path/to/file , ie file . basename将只给出/path/to/file ,即file

realpath for Mac OS X Mac OS X 的真实路径

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

Example with related path:具有相关路径的示例:

realpath "../scripts/test.sh"

Example with home folder主文件夹示例

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

So as you can see above, I took a shot at this about 6 months ago.正如您在上面看到的,我大约在 6 个月前就对此进行了尝试。 I totally forgot about it until I found myself in need of a similar thing again.我完全忘记了它,直到我发现自己再次需要类似的东西。 I was completely shocked to see just how rudimentary it was;看到它是多么简陋,我完全震惊了。 I've been teaching myself to code pretty intensively for about a year now, but I often feel like maybe I haven't learned anything at all when things are at their worst.大约一年以来,我一直在密集地自学编码,但我经常觉得,当事情最糟糕的时候,我可能根本没有学到任何东西。

I would remove the 'solution' above, but I really like it sort of being a record of of how much I really have learnt over the past few months.我会删除上面的“解决方案”,但我真的很喜欢它记录了过去几个月我真正学到的东西。

But I digress.但我离题了。 I sat down and worked it all out last night.昨晚我坐下来解决了所有问题。 The explanation in the comments should be sufficient.评论中的解释应该足够了。 If you want to track the copy I'm continuing to work on, you can follow this gist.如果你想跟踪我继续工作的副本, 你可以遵循这个要点。 This probably does what you need.这可能会满足您的需求。

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

I checked every answered, but missed the best one (IMHO) by Jason S Jul 14 '16 at 3:12, left the comment field.我检查了每个答案,但错过了 Jason S 2016 年 7 月 14 日 3:12 的最佳答案(恕我直言),离开了评论字段。

So here it is, in case someone like me having the tendency to check answered and don't have time to go through every single comments:所以这里是,以防像我这样的人倾向于检查答案并且没有时间浏览每一条评论:

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

Help:帮助:

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).

On macOS, the only solution that I've found to this that reliably handles symlinks is by using realpath .在 macOS 上,我找到的唯一可靠处理符号链接的解决方案是使用realpath Since this requires brew install coreutils , I just automated that step.由于这需要brew install coreutils ,我只是自动化了这一步。 My implementation looks like this:我的实现是这样的:

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


This errors if they don't have brew installed, but you could alternatively just install that too.如果他们没有安装brew ,则会出现此错误,但您也可以安装它。 I just didn't feel comfortable automating something that curls arbitrary ruby code from the net.我只是觉得自动化一些从网络上卷曲任意 ruby​​ 代码的东西并不舒服。

Note that this an automated variation on Oleg Mikheev's answer .请注意,这是 Oleg Mikheev答案的自动变体。


One important test一项重要的测试

One good test of any of these solutions is:对任何这些解决方案的一个很好的测试是:

  1. put the code in a script file somewhere将代码放在某个脚本文件中
  2. in another directory, symlink ( ln -s ) to that file在另一个目录中,符号链接( ln -s )到该文件
  3. run the script from that symlink从该符号链接运行脚本

Does the solution dereference the symlink, and give you the original directory?解决方案是否取消引用符号链接,并为您提供原始目录? If so, it works.如果是这样,它的工作原理。

This seems to work for OSX, doesnt require any binaries, and was pulled from here这似乎适用于 OSX,不需要任何二进制文件,并且是从这里提取的

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

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

I like this:我喜欢这个:

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

I needed a realpath replacement on OS X, one that operates correctly on paths with symlinks and parent references just like readlink -f would .我需要在 OS X 上替换realpath ,一个readlink -f一样在带有符号链接和父引用的路径上正确运行的替换。 This includes resolving symlinks in the path before resolving parent references;这包括解析父引用之前解析路径中的符号链接; eg if you have installed the homebrew coreutils bottle, then run:例如,如果您安装了自制的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

Note that readlink -f has resolved /tmp/linkeddir before resolving the .. parent dir reference.请注意, readlink -f解析..父目录引用之前已解析/tmp/linkeddir Of course, there is no readlink -f on Mac either .当然,Mac 上没有readlink -f

So as part of the a bash implementation for realpath I re-implemented what a GNUlib canonicalize_filename_mode(path, CAN_ALL_BUT_LAST) function call does , in Bash 3.2;因此,作为realpath bash 实现的一部分,我在 Bash 3.2 中重新实现了 GNUlib canonicalize_filename_mode(path, CAN_ALL_BUT_LAST)函数调用所做的 this is also the function call that GNU readlink -f makes:这也是 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"
}

It includes circular symlink detection, exiting if the same (intermediary) path is seen twice.它包括循环符号链接检测,如果看到两次相同的(中间)路径,则退出。

If all you need is readlink -f , then you can use the above as:如果您只需要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
}

For realpath , I also needed --relative-to and --relative-base support, which give you relative paths after canonicalizing:对于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

I included unit tests in my Code Review request for this code .我在对此代码的代码审查请求中包含了单元测试。

For those nodejs developers in a mac using bash:对于那些在 Mac 中使用 bash 的 nodejs 开发人员:

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

Just an idea of using pushd :只是使用pushd的想法:

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

The eval is used to expand tilde such as ~/Downloads . eval用于扩展波浪号,例如~/Downloads

Based on the communication with commenter, I agreed that it is very hard and has no trival way to implement a realpath behaves totally same as Ubuntu.根据与评论者的交流,我同意它非常困难并且没有简单的方法来实现与 Ubuntu 完全相同的真实路径。

But the following version, can handle corner cases best answer can't and satisfy my daily needs on macbook.但是以下版本,可以处理极端情况的最佳答案不能满足我在 macbook 上的日常需求。 Put this code into your ~/.bashrc and remember:将此代码放入您的 ~/.bashrc 并记住:

  • arg can only be 1 file or dir, no wildcard arg 只能是 1 个文件或目录,没有通配符
  • no spaces in the dir or file name目录或文件名中没有空格
  • at least the file or dir's parent dir exists至少文件或目录的父目录存在
  • feel free to use .随意使用。 .. / thing, these are safe ../东西,这些是安全的

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM