Updated 2011-01-30 23:36:36 by dkf

This is really tremendously easy, so long as you remember the behaviour of the file join command...
proc makeAbsolute {pathname} {
    file join [pwd] $pathname
}

How does this work? Well if the pathname is relative or volume-relative, the file join will pick out the missing info (namely the current volume and the current directory on that volume) and plug it in for us. However, if the pathname is already absolute all the info from the pwd will be just discarded.

Note that this does not produce canonical filenames, and I am not convinced that doing so is a particularly good idea either, since I've worked on systems where the canonical (memorable for people and always guaranteed to work) filenames were really directed through symbolic links, and the usually canonical (guaranteed symlink-free, with a well-founded directory chain above) filenames were liable to change on the whim of an automounter. (Thankfully those days are gone now!) This means that canonicalising a filename by removing symlinks is not guaranteed to be correct, and yet there is no way to know what symlinks there are that you could insert either. It isn't even necessarily right to remove dirname/.. combinations from the filename, since that will also go wrong if dirname is a symlink. Like I said, just don't bother...

DKF

On the issue of canonical filenames, note that TIP 17, approved for inclusion in a future Tcl release, proposes the new command [file normalize] to provide such a thing.

(DKF: Mind you, if you're stuck with the automounter problem of the sort hinted at above, file normalize doesn't help because it doesn't know that “non-normalized” filenames are canonical.)

But...

If you decide to ignore DKF's advice and bother anyway, you might start with the following. It returns an absolute "direct" path, one without any symlinks in it. Note that I've been using this code for a long time, and it hasn't been upgraded to use namespaces for its storage array.

The main intent of the routine is to create a one-to-one map between the string representation of the pathname and the file on the file system. The secondary intent is to purge '..' from the pathname so that one can evalute 'file dirname $pathname' and know that the result is the parent directory.
  proc DirectPathname { pathname } {
    global _DirectPathnameCache
    set canCdTo [file dirname $pathname]
    set rest [file tail $pathname]
    switch -exact -- $rest {
        .        -
        .. {
            set canCdTo [file join $canCdTo $rest]
            set rest ""
        }
    }
    set index [file join [pwd] $canCdTo]
    if {[info exists _DirectPathnameCache($index)]} {
        return [file join $_DirectPathnameCache($index) $rest]
    }
    if {[catch {pwd} savedir]} {
        return -code error "Can't determine pathname for\n\t$pathname:\n\t$savedir"
    }
    # Try to [cd] to where we can [pwd]
    while {[catch {cd $canCdTo}]} {
        switch -exact -- [file tail $canCdTo] {
            "" {
                # $canCdTo is the root directory, and we can't cd to it.
                # This means we know the direct pathname, even though we
                # can't cd to it or any of its ancestors.
                set _DirectPathnameCache($index) $canCdTo        ;# = '/'
                return [file join $_DirectPathnameCache($index) $rest]
            }
            . {
                # Do nothing.  Leave $rest unchanged
            }
            .. {
                # Don't want to shift '..' onto $rest.
                # Make recursive call instead.
                set _DirectPathnameCache($index) [file dirname \
                        [DirectPathname [file dirname $canCdTo]]]
                return [file join $_DirectPathnameCache($index) $rest]
            }
            default {
                ;# Shift one path component from $canCdTo to $rest
                set rest [file join [file tail $canCdTo] $rest]
            }
        }
        set canCdTo [file dirname $canCdTo]
        set index [file dirname $index]
    }
    # We've successfully changed the working directory to $canCdTo
    # Try to use [pwd] to get the direct pathname of the working directory
    catch {set _DirectPathnameCache($index) [pwd]}
    # Shouldn't be a problem with a [cd] back to the original working directory
    cd $savedir
    if {![info exists _DirectPathnameCache($index)]} {
        # Strange case where we could [cd] into $canCdTo, but [pwd] failed.
        # Try a recursive call to resolve matters.
        set _DirectPathnameCache($index) [DirectPathname $canCdTo]
    }
    return [file join $_DirectPathnameCache($index) $rest]
  }

DGP

However the above procedure isn't necessarily correct, since the file in the directory might be a symlink to a file in a separate directory. :^)
 proc DirectPathname {filename} {
     set savewd [pwd]
     set realFile [file join $savewd $filename]
     # Hmm.  This (unusually) looks like a job for do...while!
     cd [file dirname $realFile]
     set dir [pwd] ;# Always gives a canonical directory name
     set filename [file tail $realFile]
     while {![catch {file readlink $filename} realFile]} {
         cd [file dirname $realFile]
         set dir [pwd]
         set filename [file tail $realFile]
     }
     cd $savewd
     return [file join $dir $filename]
 }

OK, so this code doesn't handle non-existent directories very well, and circular symlinks will definitely get its (metaphorical) knickers in a twist. Detecting these error cases is quite a lot more complex.

DKF

See Determining the Applications Root Directory for a related discussion.