105

As arguments to my script there are some file paths. Those can, of course, be relative (or contain ~). But for the functions I've written I need paths that are absolute, but do not have their symlinks resolved.

Is there any function for this?

6

11 Answers 11

89

MY_PATH=$(readlink -f $YOUR_ARG) will resolve relative paths like "./" and "../"

Consider this as well (source):

#!/bin/bash
dir_resolve()
{
cd "$1" 2>/dev/null || return $?  # cd to desired directory; if fail, quell any error messages but return exit status
echo "`pwd -P`" # output full, link-resolved path
}

# sample usage
if abs_path="`dir_resolve \"$1\"`"
then
echo "$1 resolves to $abs_path"
echo pwd: `pwd` # function forks subshell, so working directory outside function is not affected
else
echo "Could not reach $1"
fi
3
  • that certainly nice, but the problem is that simlinks are resolved, and that would lead to some confusion with the users.
    – laar
    Commented Aug 19, 2011 at 20:18
  • man pwd: "-P Display the physical current working directory (all symbolic links resolved)", just remove the -P
    – andsens
    Commented Jun 12, 2013 at 22:01
  • 9
    To get the readlink -f functionality on OS X, you can use Homebrew and then run: $ brew install coreutils You can then use greadlink -f .
    – jgpawletko
    Commented Jun 3, 2015 at 16:40
52

http://www.linuxquestions.org/questions/programming-9/bash-script-return-full-path-and-filename-680368/page3.html has the following

function abspath {
    if [[ -d "$1" ]]
    then
        pushd "$1" >/dev/null
        pwd
        popd >/dev/null
    elif [[ -e "$1" ]]
    then
        pushd "$(dirname "$1")" >/dev/null
        echo "$(pwd)/$(basename "$1")"
        popd >/dev/null
    else
        echo "$1" does not exist! >&2
        return 127
    fi
}

which uses pushd/popd to get into a state where pwd is useful.

2
  • 2
    Sadly, this is really the best answer on here and it's not voted up to where it belongs. +1 for accurately answering the OP's question. He wants symlinks not resolved, which is really a question of bash's local DIRSTACK and nothing else. Nice solution! Commented Mar 14, 2013 at 4:29
  • 7
    When aiming for standard sh compatibility, pushd; cd <dir>; <cmd>; popd can be replaced with (cd <dir>; <cmd>).
    – Abbafei
    Commented Jun 27, 2013 at 9:46
20

If your OS supports it, use:

realpath -s "./some/dir"

And using it in a variable:

some_path="$(realpath -s "./some/dir")"

Which will expand your path. Tested on Ubuntu and CentOS, might not be available on yours. Some recommend readlink, but documentation for readlink says:

Note realpath(1) is the preferred command to use for canonicalization functionality.

In case people wonder why I quote my variables, it's to preserve spaces in paths. Like doing realpath some path will give you two different path results. But realpath "some path" will return one. Quoted parameters ftw :)

Thanks to NyanPasu64 for the heads up. You'll want to add -s if you don't want it to follow the symlinks.

3
  • The best answer, by far. All the rest either ignore the requirement of not following symlinks, or are way too complicated.
    – swineone
    Commented Aug 22, 2021 at 3:55
  • On Linux, realpath follows symlinks, which OP specifically requested to not happen.
    – nyanpasu64
    Commented Feb 1, 2022 at 9:34
  • 1
    Thanks for the heads up @nyanpasu64 , I edited the answer to include not following symlinks.
    – Raid
    Commented Feb 1, 2022 at 18:08
18

Simple one-liner:

function abs_path {
  (cd "$(dirname '$1')" &>/dev/null && printf "%s/%s" "$PWD" "${1##*/}")
}

Usage:

function do_something {
    local file=$(abs_path $1)
    printf "Absolute path to %s: %s\n" "$1" "$file"
}
do_something $HOME/path/to/some\ where

I am still trying to figure out how I can get it to be completely oblivious to whether the path exists or not (so it can be used when creating files as well).

14
  • 1
    Won't this change the current working directory? Should I cd - afterward to reset it?
    – devios1
    Commented Nov 20, 2013 at 18:26
  • 4
    No need. Note the parens, they cause the commands inside to be run in a subshell. The state (e.g. the working directory) of a subshell does not leak to its parent shell. Read more here: tldp.org/LDP/abs/html/subshells.html
    – andsens
    Commented Nov 21, 2013 at 20:22
  • 1
    Nice one-liner. There's an issue, though: if the expanded value of $1 does not have any slash (i.e. it is a file name in the current directory), then ${1%/*} expands to the filename itself and the cd command fails. You may want to use $(dirname $1) instead, which will expand to '.' in that case.
    – Paulo
    Commented Sep 1, 2014 at 12:43
  • 1
    @BryanP I actually ended up going completely overboard for the dotfile synchronizer that I'm maintaining. It has a clean_path() function that removes unneeded path parts, if you stick a relative path at the end of $PWD and run it through that, it should work quite well: github.com/andsens/homeshick/blob/…
    – andsens
    Commented Oct 7, 2018 at 16:25
  • 1
    If you don't want to use functions and want a quick easy way, you can use realpath -s "./some/dir" as per stackoverflow.com/a/64706902/2024228
    – Raid
    Commented Apr 19, 2023 at 1:04
9

This does the trick for me on OS X: $(cd SOME_DIRECTORY 2> /dev/null && pwd -P)

It should work anywhere. The other solutions seemed too complicated.

5

Use readlink -f <relative-path>, e.g.

export FULLPATH=`readlink -f ./`
3

Maybe this is more readable and does not use a subshell and does not change the current dir:

dir_resolve() {
  local dir=`dirname "$1"`
  local file=`basename "$1"`
  pushd "$dir" &>/dev/null || return $? # On error, return error code
  echo "`pwd -P`/$file" # output full, link-resolved path with filename
  popd &> /dev/null
}
2

on OS X you can use

stat -f "%N" YOUR_PATH

on linux you might have realpath executable. if not, the following might work (not only for links):

readlink -c YOUR_PATH
1
  • 24
    The OS X answer does not work. stat -f "%N" PATH gives me exactly the same path I gave it. Commented Oct 16, 2014 at 1:36
2

There's another method. You can use python embedding in bash script to resolve a relative path.

abs_path=$(python3 - <<END
from pathlib import Path
path = str(Path("$1").expanduser().resolve())
print(path)
END
)
0

self edit, I just noticed the OP said he's not looking for symlinks resolved:

"But for the functions I've written I need paths that are absolute, but do not have their symlinks resolved."

So guess this isn't so apropos to his question after all. :)

Since I've run into this many times over the years, and this time around I needed a pure bash portable version that I could use on OSX and linux, I went ahead and wrote one:

The living version lives here:

https://github.com/keen99/shell-functions/tree/master/resolve_path

but for the sake of SO, here's the current version (I feel it's well tested..but I'm open to feedback!)

Might not be difficult to make it work for plain bourne shell (sh), but I didn't try...I like $FUNCNAME too much. :)

#!/bin/bash

resolve_path() {
    #I'm bash only, please!
    # usage:  resolve_path <a file or directory> 
    # follows symlinks and relative paths, returns a full real path
    #
    local owd="$PWD"
    #echo "$FUNCNAME for $1" >&2
    local opath="$1"
    local npath=""
    local obase=$(basename "$opath")
    local odir=$(dirname "$opath")
    if [[ -L "$opath" ]]
    then
    #it's a link.
    #file or directory, we want to cd into it's dir
        cd $odir
    #then extract where the link points.
        npath=$(readlink "$obase")
        #have to -L BEFORE we -f, because -f includes -L :(
        if [[ -L $npath ]]
         then
        #the link points to another symlink, so go follow that.
            resolve_path "$npath"
            #and finish out early, we're done.
            return $?
            #done
        elif [[ -f $npath ]]
        #the link points to a file.
         then
            #get the dir for the new file
            nbase=$(basename $npath)
            npath=$(dirname $npath)
            cd "$npath"
            ndir=$(pwd -P)
            retval=0
            #done
        elif [[ -d $npath ]]
         then
        #the link points to a directory.
            cd "$npath"
            ndir=$(pwd -P)
            retval=0
            #done
        else
            echo "$FUNCNAME: ERROR: unknown condition inside link!!" >&2
            echo "opath [[ $opath ]]" >&2
            echo "npath [[ $npath ]]" >&2
            return 1
        fi
    else
        if ! [[ -e "$opath" ]]
         then
            echo "$FUNCNAME: $opath: No such file or directory" >&2
            return 1
            #and break early
        elif [[ -d "$opath" ]]
         then 
            cd "$opath"
            ndir=$(pwd -P)
            retval=0
            #done
        elif [[ -f "$opath" ]]
         then
            cd $odir
            ndir=$(pwd -P)
            nbase=$(basename "$opath")
            retval=0
            #done
        else
            echo "$FUNCNAME: ERROR: unknown condition outside link!!" >&2
            echo "opath [[ $opath ]]" >&2
            return 1
        fi
    fi
    #now assemble our output
    echo -n "$ndir"
    if [[ "x${nbase:=}" != "x" ]]
     then
        echo "/$nbase"
    else 
        echo
    fi
    #now return to where we were
    cd "$owd"
    return $retval
}

here's a classic example, thanks to brew:

%% ls -l `which mvn`
lrwxr-xr-x  1 draistrick  502  29 Dec 17 10:50 /usr/local/bin/mvn@ -> ../Cellar/maven/3.2.3/bin/mvn

use this function and it will return the -real- path:

%% cat test.sh
#!/bin/bash
. resolve_path.inc
echo
echo "relative symlinked path:"
which mvn
echo
echo "and the real path:"
resolve_path `which mvn`


%% test.sh

relative symlinked path:
/usr/local/bin/mvn

and the real path:
/usr/local/Cellar/maven/3.2.3/libexec/bin/mvn
-1

Do you have to use bash exclusively? I needed to do this and got fed up with differences between Linux and OS X. So I used PHP for a quick and dirty solution.

#!/usr/bin/php <-- or wherever
<?php
{
   if($argc!=2)
      exit();
   $fname=$argv[1];
   if(!file_exists($fname))
      exit();
   echo realpath($fname)."\n";
}
?>

I know it's not a very elegant solution but it does work.

1
  • This does nothing but return the path to the file if you already know the path to the file or the file is in the current dir...does not find the file in the PATH settings
    – G-Man
    Commented Jan 11, 2018 at 16:19

Not the answer you're looking for? Browse other questions tagged or ask your own question.