Provided you have execute permissions on the current directory - or on the directory from which you executed your shell script - if you want an absolute path to a directory all you need is cd
.
Step 10 of cd
's spec
If the -P
option is in effect, the $PWD
environment variable shall be set to the string that would be output by pwd -P
. If there is insufficient permission on the new directory, or on any parent of that directory, to determine the current working directory, the value of the $PWD
environment variable is unspecified.
And on pwd -P
The pathname written to standard output shall not contain any components that refer to files of type symbolic link. If there are multiple pathnames that the pwd
utility could write to standard output, one beginning with a single /slash character and one or more beginning with two /slash characters, then it shall write the pathname beginning with a single /slash character. The pathname shall not contain any unnecessary /slash characters after the leading one or two /slash characters.
It's because cd -P
has to set the current working directory to what pwd -P
should otherwise print and that cd -
has to print the $OLDPWD
that the following works:
mkdir ./dir
ln -s ./dir ./ln
cd ./ln ; cd . ; cd -
OUTPUT
/home/mikeserv/test/ln
wait for it...
cd -P . ; cd . ; cd -
OUTPUT
/home/mikeserv/test/dir
And when I print with cd -
I'm printing $OLDPWD
. cd
sets $PWD
as soon as I cd -P .
$PWD
is now an absolute path to /
- so I don't need any other variables. And actually, I shouldn't even need the trailing .
but there is a specified behavior of resetting $PWD
to $HOME
in an interactive shell when cd
is unadorned. So it's just a good habit to develop.
So just doing the above on the path in ${0%/*}
should be more than enough to verify $0
's path, but in the case that $0
is itself a soft-link, you probably cannot change directory into it, unfortunately.
Here is a function that will handle that:
zpath() { cd -P . || return
_out() { printf "%s$_zdlm\n" "$PWD/${1##*/}"; }
_cd() { cd -P "$1" ; } >/dev/null 2>&1
while [ $# -gt 0 ] && _cd .
do if _cd "$1"
then _out
elif ! [ -L "$1" ] && [ -e "$1" ]
then _cd "${1%/*}"; _out "$1"
elif [ -L "$1" ]
then ( while set -- "${1%?/}"; _cd "${1%/*}"; [ -L "${1##*/}" ]
do set " $1" "$(_cd -; ls -nd -- "$1"; echo /)"
set -- "${2#*"$1" -> }"
done; _out "$1"
); else ( PS4=ERR:\ NO_SUCH_PATH; set -x; : "$1" )
fi; _cd -; shift; done
unset -f _out _cd; unset -v _zdlm
}
It strives to do as much as it might in the current shell - without invoking a subshell - though there are subshells invoked for errors and soft links which do not point to directories. It depends on a POSIX-compatible shell and a POSIX-compatible ls
as well as a clean _function()
namespace. It will still function just fine without the latter, though it may overwrite then unset
some current shell functions in that case. In general all of these dependencies should be pretty reliably available on a Unix machine.
Called with or without arguments the first thing it does is reset $PWD
to its canonical value - it resolves any links therein to their targets as necessary. Called without arguments and that's about it; but called with them and it will resolve and canonicalize the path for each or else print a message to stderr
why not.
Because it mostly operates in the current shell it should be able to handle an argument list of any length. It also looks for the $_zdlm
variable (which it also unset
s when it is through) and prints its C-escaped value immediately to the right of each of its arguments, each of which is always followed also by a single \n
ewline character.
It does a lot of directory changing, but, other than setting it to its canonical value, it does not affect $PWD
, though $OLDPWD
cannot by any means be counted upon when it is through.
It tries to quit each of its arguments as soon as it might. It first tries to cd
into $1
. If it can it prints the argument's canonical path to stdout
. If it cannot it checks that $1
exists and is not a soft link. If true, it prints.
In this way it handles any file type argument which the shell has permissions to address unless $1
is a symbolic link that does not point to a directory. In that case it calls while
loop in a subshell.
It calls ls
to read the link. The current directory must be changed to its initial value first in order to reliably handle any referent paths and so, in the command substitution subshell the function does:
cd -...ls...echo /
It strips from the left of ls
's output as little as it must to fully contain the link's name and the string ->
. While I at first tried to avoid do this with shift
and $IFS
it turns out this is the most reliable method as near as I can figure. This is the same thing Gilles's poor_mans_readlink does - and it is well done.
It will repeat this process in a loop until the filename returned from ls
is definitely not a soft link. At that point it canonicalizes that path as before with cd
then prints.
Example usage:
zpath \
/tmp/script \ #symlink to $HOME/test/dir/script.sh
ln \ #symlink to ./dir/
ln/nl \ #symlink to ../..
/dev/fd/0 \ #currently a here-document like : dash <<\HD
/dev/fd/1 \ #(zlink) | dash
file \ #regular file
doesntexist \ #doesnt exist
/dev/disk/by-path/pci-0000:00:16.2-usb-0:3:1.0-scsi-0:0:0:0 \
/dev/./././././././null \
. ..
OUTPUT
/home/mikeserv/test/dir/script.sh
/home/mikeserv/test/dir/
/home/mikeserv/test/
/tmp/zshtpKRVx (deleted)
/proc/17420/fd/pipe:[1782312]
/home/mikeserv/test/file
ERR: NO_SUCH_PATH: doesntexist
/dev/sdd
/dev/null
/home/mikeserv/test/
/home/mikeserv/
Or possibly...
ls
dir/ file file? folder/ link@ ln@ script* script3@ script4@
zdlm=\\0 zpath * | cat -A
OUTPUT
/home/mikeserv/test/dir/^@$
/home/mikeserv/test/file^@$
/home/mikeserv/test/file$
^@$
/home/mikeserv/test/folder/^@$
/home/mikeserv/test/file$ #'link' -> 'file\n'
^@$
/home/mikeserv/test/dir/^@$ #'ln' -> './dir'
/home/mikeserv/test/script^@$
/home/mikeserv/test/dir/script.sh^@$ #'script3' -> './dir/script.sh'
/home/mikeserv/test/dir/script.sh^@$ #'script4' -> '/tmp/script' -> ...
readlink
appears to have been introduced in macOS Monterey 12.3 (March 2022). – Aeronautix Oct 30 '23 at 07:53