Getting canonical path with sed

Hi there,

Please read the preliminary notes:

readlink does the same ....... I know
realpath does the same ....... I know
awk can do it ................ I know
perl can do it ............... I know
You're losing time ........... I know

I wrote the following script that I think prints a canonical path without following symbolic links.
It's supposed to work with any filename including space, tabs, carriage return, etc.
Can anyone think of any path that wouldn't resolve to a canonical form with this script?

Thanks for your help.

filename=$1

# Squeeze double slashes.
filename="$(echo "$filename" | sed -r 's#/+#/#g')"

# Strip special '.' directory entries.
filename=$(echo "$filename" | sed -r ':a;s#^\./##;s#/\./#/#g;s#/\.$#/#;ta')

# Strip special '..' directory entries.
filename=$(echo "$filename" | sed -r ':a;$!N;$!ba;:b;s#^/\.\./#/#;s#/([^/.]|[^/.][^/]|[^/][^/.]|[^/]{3,})/\.\.(/|$)#/#;tb')

Not listed in your list of "I knows": I think you could do this with shell builtins, and I don't mean the bash/ksh dependent ones.

Also, if it's supposed to work with any filename "including space, tabs, etc" then it fails at line one, where you don't put double quotes around $1.

I'm using bash and I usually don't need to quote variable when doing a strict variable=$variable operation. What shell fails to do that?

My command:

a=space" "tab$'\t'newline$'\nend'; echo "$a" | cat -A; b=$a; echo "$b" | cat -A

My result:

space tab^Inewline$
end$
space tab^Inewline$
end$

I like your point about shell builtins. I foresee how to do it with the first two command and will post my result. Can you think of any way to do the third one with shell builtins?

---------- Post updated at 17:28 ---------- Previous update was at 17:25 ----------

# Squeeze double slashes.
while true; do
    new=${filename//\/\//\/}
    [[ "$new" = "$filename" ]] && break
    filename=$new
done

# Strip special '.' directory entries.
filename=${filename##./}
while true; do
    new=${filename//\/.\//\/}
    [[ "$new" = "$filename" ]] && break
    filename=$new
done
filename=${filename%%/.}

You are using shell builtins only available in very new shells.

#!/bin/sh

OLDIFS="$IFS" ; IFS="/"
VAR="$1"

# Split a//b/c/d/e into $1=a, $2="", $3=b, $4=c, ...
# Note NOT quoted, we depend on shell splitting here
set -- $VAR

# S is what gets appended to O every loop between sections.
# It starts blank unless we know the path is absolute.
S="" ; O="" ; [ -z "$1" ] && S="/"

while [ "$#" -gt 0 ]
do
        case "$1" in
        "")     ;; # Skip blanks, which mean doubled slashes
        [.])    ;; # Skip single dots, which mean same-dir
        [.][.])  # Double-dot means strip off last path segment
                TMP="$*" ; S2="" ; set -- $O ; O=""
                # Re-assemble path, up to but not including last segment
                while [ "$#" -gt 1 ] ; do O="${O}${S2}${1}" ; S2="/" ; shift ; done
                set -- $TMP     # Re-split remaining tokens
                ;;
        # Add segment to O
        *)      O="${O}${S}${1}"; S="/" ;;
        esac
        shift
done

IFS="$OLDIFS"

echo "$O"

Impressive!
Thanks Corona688
My notes:

  1. If path is / or . , it becomes empty which makes it impossible to differentiate the root directory from the current directory.
  2. If a path starts with ../ , that first element is stripped. But ../brother and brother are not the same address.

I'm working on an edit and will submit as soon as I can.