在 bash 中展开一个可能的相对路径

作为脚本的参数,有一些文件路径。当然,它们可以是相对的(或者包含 ~)。但是对于我编写的函数,我需要绝对的路径,但是不能解析它们的符号链接。

这个有用吗?

67828 次浏览

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

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.

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

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.

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

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
}

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

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.

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

export FULLPATH=`readlink -f ./`

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
)

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.