(I need readlink's function on AIX where it doesn't seem to be available).
jon.
Try this:
ls -l /path/to/some/link | awk '{print$NF}'
Sure it doesn't handle whitespace in filenames but what classic AIX
Unix symlink would have whitespace in it? :-)
Bob
readlink -f will fully resolve links in the path itself (rather than
link at the end of the path), which was the behaviour I needed.
It seems cd -P does most of what I need for directories and so
handling things other than directories is a small tweak on that.
Anyway, thanks for that!
jon.
Ah, yes, well, as you could tell that was just a partial solution
anyway.
> It seems cd -P does most of what I need for directories and so
> handling things other than directories is a small tweak on that.
You might try cd'ing there and then using pwd -P to get the canonical
directory name. I am thinking something like this:
#!/bin/sh
p="$1"
dir=$(dirname "$p")
base=$(basename "$p")
physdir=$(cd "$dir"; pwd -P)
realpath=$(cd "$dir"; ls -l "$base" | awk '{print$NF}')
echo "$physdir/$realpath" | sed 's|//*|/|g'
exit 0
Again, another very quick and partial solution. But perhaps something
good enough just the same.
Bob
I always use sed for this purpose, so:
$(cd "$dir"; ls -l "$base" | sed "s/.*->//")
But, with pathological linking structures, this isn't quite enough -
particularly if the target of the link itself contains paths, some of
which may contain links :-)
jon.
Agreed! Symlinks with arbitrary data, such as holding small shopping
lists in the target value, are so much fun. I am more concerned that
arbitrary data such as "->" might exist in there more so than
whitespace. That is why I usually don't use a pattern expression.
But I agree it is another way to go. But it is easier to say
whitespace is bad in filenames than to say whitespace is bad and oh
yes you can't have "->" in there either. :-)
Bob
Ok, I think this does it...
readlink_f()
{
local path="$1"
test -z "$path" && echo "usage: readlink_f path" 1>&2 && exit 1;
local dir
if test -L "$path"
then
local link=$(ls -l "$path" | sed "s/.*-> //")
if test "$link" = "${link#/}"
then
# relative link
dir="$(dirname "$path")"
readlink_f "${dir%/}/$link"
else
# absolute link
readlink_f "$link"
fi
elif test -d "$path"
then
(cd "$path"; pwd -P) # normalize it
else
dir="$(cd $(dirname "$path"); pwd -P)"
base="$(basename "$path")"
echo "${dir%/}/${base}"
fi
}
And I make no claims whatsoever about whether this is vulnerable to
infinite recursion!
jon.
An extra ';' there that doesn't hurt but isn't needed.
> local dir
>
> if test -L "$path"
> then
> local link=$(ls -l "$path" | sed "s/.*-> //")
I would be inclined to also look for a space before the " -> " too.
Because it just is slightly more paranoid.
local link=$(ls -l "$path" | sed "s/.* -> //")
> if test "$link" = "${link#/}"
> then
> # relative link
> dir="$(dirname "$path")"
As an aside you don't need to quote assignments. They exist inside
the shell and no word splitting will occur. It is okay to assign
without quotes here and I think it reads slightly better without.
dir=$(dirname "$path")
> readlink_f "${dir%/}/$link"
> else
> # absolute link
> readlink_f "$link"
> fi
> elif test -d "$path"
> then
> (cd "$path"; pwd -P) # normalize it
> else
> dir="$(cd $(dirname "$path"); pwd -P)"
> base="$(basename "$path")"
Same comment here about over-quoting. If nothing else it means that
syntax highlighting is different.
dir=$(cd $(dirname "$path"); pwd -P)
base=$(basename "$path")
> echo "${dir%/}/${base}"
> fi
> }
And of course those are just suggestions and nothing more. Feel free
to ignore.
Note that there is a recent movement to change that dash greater-than
combination into a true unicode arrow graphic emited by 'ls'. I think
things paused when there were several different bike shed suggestions
about which unicode arrow symbol people wanted there. I haven't seen
any actual movement for a while and I think that is a good thing.
Bob
Tips appreciated, thanks.
jon.
> Same comment here about over-quoting. If nothing else it means that
> syntax highlighting is different.
>
> dir=$(cd $(dirname "$path"); pwd -P)
You are missing a pair of quotes here. :-)
Andreas.
--
Andreas Schwab, sch...@linux-m68k.org
GPG Key fingerprint = 58CA 54C7 6D53 942B 1756 01D3 44D5 214B 8276 4ED5
"And now for something completely different."
What about:
readlink_f() (
link=$1 max_iterations=40
while [ "$max_iterations" -gt 0 ]; do
max_iterations=$(($max_iterations - 1))
dir=$(dirname -- "$link") || exit
base=$(basename -- "$link") || exit
dir=$(cd -P -- "$dir" && pwd -P) || exit
link=${dir%/}/$base
if [ ! -L "$link" ]; then
printf '%s\n' "$link"
exit
fi
link=$(ls -ld -- "$link") || exit
link=${link#* -> }
done
printf >&2 'Loop detected\n'
exit 1
)
--
Stephane
You can find my version here:
http://sudrala.de/en_d/shell-getlink.html
As it contains some corrections from Greg Wooledge, it should handle
even pathological situations. ;)
Bernd
> function getlink # ([-l] path)
Why use the ksh syntax instead of the standard one?
> {
> # Path of the file a symbolic link is pointing to.
> # -l: follow link chain, print last target
> # no option: print 1st target unchanged
>
> typeset dir file last link opt oldPWD=$PWD ret=0
>
> (( OPTIND = 1 ))
>
> while getopts "l" opt
> do
> case $opt in
> (l) last=1
> ;;
> esac
> done
>
> shift $(( OPTIND - 1 ))
> file=$1
>
> if [[ $last ]] # last link
> then
> while true
> do
> dir=$(dirname "$file")
What if $file starts with "-"?
> [[ ! -d $dir ]] &&
> {
> ret=1
> break
> }
>
> # remove slashes at end
>
> while [[ $file == */ ]]
> do
> file=${file%/}
> done
What if $file is "/"
> file=${file##*/} # file name
> command cd -P "$dir"
What if that command fails?
> [[ ! -h $file ]] && break
What if there are symlinks in the path components?
> link=$(command ls -l -- "$file"; printf x)
Good point about command substitution discarding trailing
newlines. I forgot that in the solution I gave.
> link=${link%$'\nx'}
> remove="$file -> "
> file=${link#*"$remove"}
> done
>
> printf "%s\n" "$PWD/$file"
> command cd $oldPWD
What if $oldPWD has blanks or wildcards?
> elif [[ ! -h $file ]] # 1st link
> then
> printf "%s\n" "$file"
> else
> link=$(ls -l "$file")
> printf "%s\n" "${link##*-> }"
What about trailing newlines here and links with "-> " in their
path.
> fi
>
> return $ret
What if $IFS contains 0 or 1?
> }
>
> declare -fc getlink
What's the -c about?
--
Stephane
Sorry, it's wrong if there are relative paths in symlinks (or
trailing newlines).
fixed_cmd_subst() {
eval '
'"$1"'=$('"$2"'; ret=$?; echo .; exit "$ret")
set -- "$1" "$?"
'"$1"'=${'"$1"'%??}
'
return "$2"
}
readlink_f() (
link=$1 max_iterations=40
while [ "$max_iterations" -gt 0 ]; do
max_iterations=$(($max_iterations - 1))
fixed_cmd_subst dir 'dirname -- "$link"' || exit
fixed_cmd_subst base 'basename -- "$link"' || exit
cd -P -- "$dir" || exit
link=${PWD%/}/$base
if [ ! -L "$link" ]; then
printf '%s\n' "$link"
exit
fi
fixed_cmd_subst link 'ls -ld -- "$link"' || exit
I'd just like to make a couple of suggestions for your script (I hope these
are welcome):
*) You reset OPTIND to 1 but you didn't declare it local. This will cause any
caller of getlink which uses getopts to reset its variable to 1. (I mention
this because it cost me a couple of hours a while back.)
When calling getopts, especially from a function that is intended to not be
used at a top level for processing command line options, you should declare
local copies of OPTIND, OPTARG and OPTERR.
*) To remove the trailing slashes, instead of
while [[ $file == */ ]]
do
file=${file%/}
done
file=${file##*/} # file name
just say
file="${file%${file##*[!/]}}"
*) Instead of
[[ ! -d $dir ]] &&
{
ret=1
break
}
how about this for slightly cleaner?
[[ -d $dir ]] ||
{
ret=1
break
}
--
Time flies like the wind. Fruit flies like a banana. Stranger things have .0.
happened but none stranger than this. Does your driver's license say Organ ..0
Donor?Black holes are where God divided by zero. Listen to me! We are all- 000
individuals! What if this weren't a hypothetical question?
steveo at syslang.net
Thanks for that. ${link##*-> } is a neater way to extract the link.
It does seem that a link create like so: ln -sf "a -> b" c is going to
create problems for both your script and mine [ not that I actually
care about such a perverse case :-) ]
jon.
file=${file%"${file##*[!/]}"}
Same problem with "/" being changed to "" though.
--
Stephane
You are welcome!
> *) You reset OPTIND to 1 but you didn't declare it local. This will
> cause any caller of getlink which uses getopts to reset its variable
> to 1. (I mention this because it cost me a couple of hours a while
> back.)
The reason I didn't declare OPTIND local is that OPTIND is handled
specially by the shell; there is always exactly _one_ instance of this
variable. In other words, OPTIND is always global, even if declared
local (which is indeed pretty weird). Try this:
-------------------------------
function f
{
local OPTIND=1
echo "\$1=$1"
}
while getopts "abcdefg" opt
do
echo "opt=$opt"
f $opt
done
--------------------------------
Calling the sript like this works fine:
script -a -b -c
But calling it like this leads to an endless loop:
script -abc
One could of course save and restore the original:
-------------------------------
function f
{
local oldind=$OPTIND
OPTIND=1
echo "\$1=$1"
OPTIND=$oldind
}
-------------------------------
However, this also loops endlessly. The reason is most likely that bash
maintains an additional internal variable holding the index of the
current character, relative to the current word. While this variable is
not directly accessible by the user, it is set to 0 whenever OPTIND is
assigned a value.
So the only safe way is to _never_ use getopts within another getopts
block, but always wait until the first one has finished.
> When calling getopts, especially from a function that is intended to
> not be used at a top level for processing command line options, you
> should declare local copies of OPTIND, OPTARG and OPTERR.
>
> *) To remove the trailing slashes, instead of
>
> while [[ $file == */ ]] do file=${file%/} done
>
> file=${file##*/} # file name
>
> just say file="${file%${file##*[!/]}}"
Yes, you can do that, but I find my version a bit more legible. Also,
for file=/ it returns a single slash, while yours returns an empty
string. (Hmm... the next statement in my script also creates an empty
string, but this is a bug and will be fixed).
> *) Instead of
>
> [[ ! -d $dir ]] && { ret=1 break }
>
> how about this for slightly cleaner?
>
> [[ -d $dir ]] || { ret=1 break }
I think that's just a matter of taste.
Greetings,
Bernd
That would be a bug in bash in my opinion. If OPTIND is marked
local to the function, it shouldn't affect the behavior of
parent contexts.
Note that that bug is also in ksh93, pdksh, mksh and posh
(though slightly different in that one), but not in ash nor zsh.
Note that if you set OPTIND local, you probably want to do the
same for OPTARG (and maybe OPTERR).
--
Stephane
> On 09.08.2011 15:50, Steven W. Orr wrote:
>
> *) You reset OPTIND to 1 but you didn't declare it local. This will
>> cause any caller of getlink which uses getopts to reset its variable
>> to 1. (I mention this because it cost me a couple of hours a while
>> back.)
>>
>
> The reason I didn't declare OPTIND local is that OPTIND is handled
> specially by the shell; there is always exactly _one_ instance of this
> variable. In other words, OPTIND is always global, even if declared local
> (which is indeed pretty weird). Try this:
>
>
I always declare OPTIND as local. I didn't know it does not work at all.
Bug?
> ------------------------------**-
> function f
> {
> local OPTIND=1
>
> echo "\$1=$1"
> }
>
> while getopts "abcdefg" opt
> do
> echo "opt=$opt"
> f $opt
> done
> ------------------------------**--
>
> Calling the sript like this works fine:
> script -a -b -c
>
> But calling it like this leads to an endless loop:
> script -abc
>
> One could of course save and restore the original:
>
> ------------------------------**-
> function f
> {
> local oldind=$OPTIND
>
> OPTIND=1
> echo "\$1=$1"
> OPTIND=$oldind
> }
> ------------------------------**-
>
> However, this also loops endlessly. The reason is most likely that bash
> maintains an additional internal variable holding the index of the current
> character, relative to the current word. While this variable is not directly
> accessible by the user, it is set to 0 whenever OPTIND is assigned a value.
>
What about:
readlink_f() (
link=$1 max_iterations=40
while [ "$max_iterations" -gt 0 ]; do
max_iterations=$(($max_iterations - 1))
dir=$(dirname -- "$link") || exit
base=$(basename -- "$link") || exit
dir=$(cd -P -- "$dir" && pwd -P) || exit
link=${dir%/}/$base
if [ ! -L "$link" ]; then
printf '%s\n' "$link"
exit
fi
link=$(ls -ld -- "$link") || exit
link=${link#* -> }
done
printf >&2 'Loop detected\n'
exit 1
)
--
Stephane
Sorry, it's wrong if there are relative paths in symlinks (or
trailing newlines).
fixed_cmd_subst() {
eval '
'"$1"'=$('"$2"'; ret=$?; echo .; exit "$ret")
set -- "$1" "$?"
'"$1"'=${'"$1"'%??}
'
return "$2"
}
readlink_f() (
link=$1 max_iterations=40
while [ "$max_iterations" -gt 0 ]; do
max_iterations=$(($max_iterations - 1))
fixed_cmd_subst dir 'dirname -- "$link"' || exit
fixed_cmd_subst base 'basename -- "$link"' || exit
cd -P -- "$dir" || exit
link=${PWD%/}/$base
if [ ! -L "$link" ]; then
printf '%s\n' "$link"
exit
fi
fixed_cmd_subst link 'ls -ld -- "$link"' || exit
> function getlink # ([-l] path)
Why use the ksh syntax instead of the standard one?
> {
> # Path of the file a symbolic link is pointing to.
> # -l: follow link chain, print last target
> # no option: print 1st target unchanged
>
> typeset dir file last link opt oldPWD=$PWD ret=0
>
> (( OPTIND = 1 ))
>
> while getopts "l" opt
> do
> case $opt in
> (l) last=1
> ;;
> esac
> done
>
> shift $(( OPTIND - 1 ))
> file=$1
>
> if [[ $last ]] # last link
> then
> while true
> do
> dir=$(dirname "$file")
What if $file starts with "-"?
> [[ ! -d $dir ]] &&
> {
> ret=1
> break
> }
>
> # remove slashes at end
>
> while [[ $file == */ ]]
> do
> file=${file%/}
> done
What if $file is "/"
> file=${file##*/} # file name
file=${file%"${file##*[!/]}"}
Same problem with "/" being changed to "" though.
--
Stephane
This has been fixed. Also getlink() now outputs paths always in
normalized form. Any feedback appreciated!
Bernd