150

I am trying to obtain the absolute path to the currently running script on OS X.

I saw many replies going for 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).

Is there an out-of-the-box solution to this?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
The Mighty Rubber Duck
  • 4,388
  • 5
  • 28
  • 27
  • See also [How to retrieve the absolute path of an arbitrary file from the OS X - Super User](http://superuser.com/questions/205127/how-to-retrieve-the-absolute-path-of-an-arbitrary-file-from-the-os-x) – Lri Aug 21 '12 at 20:12
  • 1
    See also: [Reliable way for a bash script to get the full path to itself?](http://stackoverflow.com/questions/4774054/reliable-way-for-a-bash-script-to-get-the-full-path-to-itself/20265654) – kenorb Nov 28 '13 at 12:03
  • See also: [How can I get the behavior of GNU's readlink -f on a Mac?](https://stackoverflow.com/questions/1055671/how-can-i-get-the-behavior-of-gnus-readlink-f-on-a-mac/1116890#1116890) – Paul Wagland Jan 14 '16 at 08:10
  • 33
    `$( cd "$(dirname "$0")" ; pwd -P )` – Jason S Jul 14 '16 at 03:12
  • 6
    `brew install coreutils` – Vojtěch Feb 26 '21 at 14:05

18 Answers18

160

These three simple steps are going to solve this and many other OS X issues:

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

(3) may be changed to just realpath, see (2) output

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Oleg Mikheev
  • 17,186
  • 14
  • 73
  • 95
  • 5
    This solves the problem, but requires installing stuff you might not want or need, or that may not be on the target system, i.e. OS X – Jason S May 28 '16 at 09:09
  • 10
    @JasonS GNU Coreutils is too good *not* to use. It includes a lot of great utilities. It's so good in fact that the Linux kernel is useless without it and why some people call it GNU/Linux. Coreutils is awesome. excellent answer this should be the selected answer. –  Jul 13 '16 at 15:15
  • 16
    @omouse The question specifically mentions 'an out of the box solution' (for OS X). Regardless of how awesome coreutils is, it is not 'out of the box' on OS X so the answer is not a good one. It is not a question of how good coreutils is, or whether it is better than what is on OS X. There are 'out of the box' solutions `$( cd "$(dirname "$0")" ; pwd -P )` works fine for me. – Jason S Jul 14 '16 at 03:08
  • 2
    One of the "features" of OS X is that the directory and filenames are case insensitive. As a result if you have a directory called `XXX` and some one `cd xxx` then `pwd` will return `.../xxx`. Except for this answer, *all* of the solutions above return `xxx` when what you really want is `XXX`. Thank you! – Andrew Mar 06 '17 at 01:26
  • @Andrew yep, coreutils can make OSX feel closer to *nux but it's still no way a developer oriented OS – Oleg Mikheev Mar 06 '17 at 14:56
  • I fully agree. The number of libraries etc that apple breaks with every new release of the OS shows either astounding ignorance or incompetence on their part - or both – Andrew Mar 07 '17 at 11:16
  • 1
    I don't know how copying, pasting, and reinventing code around endlessly is better than an existing package manager solution. If you needed `realpath`, then what happens when you almost certainly will need other items from `coreutils`? Rewrite those functions in bash, too? :P – Ezekiel Victor Oct 25 '17 at 00:38
  • None of those "out of the box" solutions properly handle all cases on macOS, symlinks being a salient example for me. coreutils/realpath _does_ solve this, and the installation can be automated, see my answer below. – Clay Bridges Jun 12 '19 at 21:54
  • This should be the answer; writing a custom script for this and deploying manually is too much work for something like this! – Zorayr Jun 01 '20 at 18:56
128

There's a realpath() C function that'll do the job, but I'm not seeing anything available on the command-line. 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. The #./ part strips off ./ from the front of $1.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
45

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

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.

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; and it should also work with whitespace in the path(s).

This answer was previously edited, re-adding the local bashism. The point of this answer is a portable, POSIX solution. I have edited it to address variable scoping by changing it to a subshell function, rather than an inline one. 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.

Geoff Nixon
  • 4,697
  • 2
  • 28
  • 34
  • 3
    I would only recommend to use `local` for variables defined inside the function to not pollute the global namespace. E.g. `local OURPWD=...`. Works at least for bash. – Michael Paesold Nov 24 '18 at 09:33
  • 1
    Also, the code should not use uppercase for private variables. Uppercase variables are reserved for system use. – tripleee Jan 08 '19 at 11:58
  • Thanks for the script. In case the link(s) and the real file have different base names it would probably be a good idea to add a `BASENAME=$(basename "$LINK") ` inside the while and use that in the second LINK setter and the REALPATH setter – stroborobo Jun 07 '19 at 15:49
  • 3
    This doesn't handle symlinks and `..` parent refs quite the way `realpath` would. With homebrew `coreutils` installed, try `ln -s /var/log /tmp/linkexample` then `realpath /tmp/linkexample/../`; this prints `/private/var`. But your function produces `/tmp/linkexample/..` instead, because `..` is not a symlink. – Martijn Pieters May 12 '20 at 08:59
  • @Martin Interesting. I'll look into this. – Geoff Nixon Jul 14 '21 at 05:16
19

A more command-line-friendly variant of the Python solution:

python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' ./my/path
mattes
  • 8,936
  • 5
  • 48
  • 73
nanav yorbiz
  • 231
  • 2
  • 2
  • 4
    Just in case anyone is crazy enough to start a python interpreter for a single command… – Bachsau Jun 21 '19 at 10:22
  • 3
    I was mad enough, but used `python -c "import os; import sys; print(os.path.realpath(sys.argv[1]))"` – Alex Chamberlain Nov 26 '19 at 09:59
  • @Bachsau have you checked how many "single commands" are already just a python script sitting in `/usr/bin` for the average Linux system? You might be surprised how much you're already using it. – Philip Couling Nov 30 '21 at 12:29
  • @PhilipCouling By "single command" I did not mean the single command I type on the command line, but the contents of the script consisting of a single command. – Bachsau Nov 30 '21 at 19:01
  • 1
    @Bachsau I know, but please consider carefully what the precise difference really is between those two (not a lot). Python is efficient enough as an interpreter to be used for tiny utilities. Using it in the way described above is very little different to invoking sed or awk... or sh -c ... they are all interpreters. – Philip Couling Nov 30 '21 at 20:00
14

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:
ln -s $(pwd)/realpath /usr/local/bin/realpath

WaffleSouffle
  • 3,293
  • 2
  • 28
  • 27
14

I checked every answered, but missed the best one (IMHO) by Jason S Jul 14 '16 at 3:12, left the comment field.

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).
xpt
  • 20,363
  • 37
  • 127
  • 216
  • 1
    This is my favourite solution. Simple, _truly portable_, and does not require Homebrew. If you go down the Homebrew rabbit hole, you end up managing two software package systems on your Mac, with all the associated hassle. The benefits must be worth it -- in this simple case they definitely won't, – András Aszódi Jul 01 '23 at 14:54
12
abs_path () {    
   echo "$(cd $(dirname "$1");pwd)/$(basename "$1")"
}

dirname will give the directory name of /path/to/file, i.e. /path/to.

cd /path/to; pwd ensures that the path is absolute.

basename will give just the filename in /path/to/file, i.e.file.

Codebling
  • 10,764
  • 2
  • 38
  • 66
Logu
  • 904
  • 9
  • 15
  • 2
    While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. – Igor F. Mar 11 '20 at 09:20
9

I was looking for a solution for use in a system provision script, i.e., run before Homebrew is even installed. Lacking a proper solution I'd just offload the task to a cross-platform language, e.g., 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")
4ae1e1
  • 7,228
  • 8
  • 44
  • 77
  • Great! Perl is nice for the little overhead! You could probably simplify the first version to `FULLPATH=$(perl -e "use Cwd 'abs_path'; print abs_path('$0')")`. Any reason against? – F Pereira Jan 27 '19 at 22:48
  • 1
    @FPereira Generating program code from unescaped user-supplied string is never a good idea. `''` is not bullet-proof. It breaks if `$0` contains a single quote, for instance. A very simple example: try your version in `/tmp/'/test.sh`, and call `/tmp/'/test.sh` by its full path. – 4ae1e1 Feb 08 '19 at 09:33
  • Or even simpler, `/tmp/'.sh`. – 4ae1e1 Feb 08 '19 at 09:39
7

Use Python to get it:

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

print(os.path.realpath(sys.argv[1]))
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
acrazing
  • 1,955
  • 16
  • 24
6

realpath for 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"
Evgeny Karpov
  • 2,386
  • 26
  • 16
  • 1
    nice simple solution, i just found one caveat, when invoking it with `..` it does not produce the right answer, so I added a check if the given path is a directory: `if test -d $path ; then echo $(cd "$path"; pwd) ; else [...]` – Herbert Poul Mar 07 '18 at 10:43
  • Didn't work for me, whereas `"$(dirname $(dirname $(realpath $0)))"` does work, so need something else... – Alexander Mills Jul 13 '18 at 05:56
  • The [useless use of `echo`](http://www.iki.fi/era/unix/award.html#echo) should also not be perpetrated. – tripleee Jan 07 '19 at 04:13
  • 1
    This doesn't actually resolve symlinks quite the way realpath would. It resolves `..` parent references *before* resolving symlinks, not after; try this with homebrew `coreutils` installed, create link with `ln -s /var/log /tmp/linkexample` then run `realpath /tmp/linkexample/../`; this prints `/private/var`. But your function produces `/tmp/linkexample/..` instead, because `pwd` still shows `/tmp/linkexample` after the cd. – Martijn Pieters May 12 '20 at 09:05
2

So as you can see above, I took a shot at this about 6 months ago. 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
Geoff Nixon
  • 4,697
  • 2
  • 28
  • 34
  • 1
    The gist link seems broken. – Alex Mar 03 '17 at 20:16
  • this answer works: https://stackoverflow.com/a/46772980/1223975 .... you should just use that. – Alexander Mills Jul 13 '18 at 09:00
  • Note that your implementation does not resolve symlinks before `..` parent references; e.g. `/foo/link_to_other_directory/..` is resolved as `/foo` rather than the parent of whatever path the symlink `/foo/link_to_other_directory` points to. `readlink -f` and `realpath` resolve *each path component* starting at the root and updating the prepending link targets to the remainder still being processed. I’ve added an answer to this question reimplementing that logic. – Martijn Pieters May 13 '20 at 08:21
1

On macOS, the only solution that I've found to this that reliably handles symlinks is by using realpath. Since this requires brew install coreutils, I just automated that step. 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. I just didn't feel comfortable automating something that curls arbitrary ruby code from the net.

Note that this an automated variation on Oleg Mikheev's answer.


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
  3. run the script from that symlink

Does the solution dereference the symlink, and give you the original directory? If so, it works.

Clay Bridges
  • 11,602
  • 10
  • 68
  • 118
1

This seems to work for OSX, doesnt require any binaries, and was pulled from here

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

  # Remove dir/.. sequences.
  while [[ $path =~ ([^/][^/]*/\.\./) ]]; do
    path=${path/${BASH_REMATCH[0]}/}
  done
  echo $path
}
Brad Parks
  • 66,836
  • 64
  • 257
  • 336
1

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"
}
dcmorse
  • 1,011
  • 11
  • 15
0

I needed a realpath replacement on OS X, one that operates correctly on paths with symlinks and parent references just like readlink -f would. This includes resolving symlinks in the path before resolving parent references; e.g. if you have installed the homebrew coreutils bottle, then run:

$ 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. Of course, there is no readlink -f on Mac either.

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; this is also the function call that GNU readlink -f makes:

# 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() {
    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() {
    # 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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
0

For those nodejs developers in a mac using bash:

realpath() {
  node -p "fs.realpathSync('$1')"
}
Wilfredo Pomier
  • 1,091
  • 9
  • 12
0

Just an idea of using pushd:

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

The eval is used to expand tilde such as ~/Downloads.

Philip Tzou
  • 5,926
  • 2
  • 18
  • 27
-2

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.

But the following version, can handle corner cases best answer can't and satisfy my daily needs on macbook. Put this code into your ~/.bashrc and remember:

  • arg can only be 1 file or dir, no wildcard
  • 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
    }
occia
  • 129
  • 1
  • 10
  • You want to avoid the [useless use of `echo`](http://www.iki.fi/era/unix/award.html#echo). Just `pwd` does the same as `echo $(pwd)` without having to spawn a second copy of the shell. Also, not quoting the argument to `echo` is a bug (you will lose any leading or trailing whitespace, any adjacent internal whitespace characters, and have wildcards expanded, etc). See further https://stackoverflow.com/questions/10067266/when-to-wrap-quotes-around-a-shell-variable – tripleee Jan 07 '19 at 04:14
  • Also, behavior for nonexistent paths is buggy; but I guess that's what the "but remember" sentence is perhaps trying to say. Though the behavior on Ubuntu is certainly not to print the current directory when you request the `realpath` of a nonexistent directory. – tripleee Jan 07 '19 at 04:17
  • For consistency, probably prefer `dir=$(dirname "$1"); file=$(basename "$1")` instead of the long-obsolescent backtick syntax. Also notice the correct quoting of arguments, again. – tripleee Jan 07 '19 at 04:43
  • Your updated answer seems to fail to fix many of the bugs, and adds new ones. – tripleee Jan 07 '19 at 15:40
  • please give me some specific failed cases, because all the tests I do in ubuntu 18.04 desktop is ok, thanks. – occia Jan 08 '19 at 01:37
  • Try `realpath '*'` for a start. – tripleee Jan 08 '19 at 04:43
  • what about excluding multiple args, only focus on handling 1 target, that is only a loop if we can realpath 1 dir or file successfully. – occia Jan 08 '19 at 11:29
  • Here is a demo with some additional test cases: https://ideone.com/SA3KJA – tripleee Jan 08 '19 at 11:50