Relative File Paths

EMJ 2004-12-14: I know this isn't really difficult, but if someone's got it somewhere...

Given file file paths a and b, how does one derive a relative path to refer to a from the location of b (the files and paths may not exist)?

CMcC: There's a tcllib fileutil relative and a prefix command which facilitates this kind of file name manipulation.

MG 2004-12-14: This piqued my curiousity, so I had a quick go at getting something that works. It's only very lightly tested, but the tests I did worked OK. This won't work if you try giving it two files on different drives/volumes, though, and will probably loop indefinitely. I wouldn't recommend trying it :D Someone else may well come up with something neater than this, but in the mean time...

set path1 {C:/Documents and Settings/Griffiths/leaflet.pub}
set path2 {C:/CONFIG.SYS}

proc relTo {a b} {
    set a [file dirname [file normalize $a]]
    set b [file dirname [file normalize $b]]
    set aa [file split $a]
    set bb [file split $b]
    if {[llength $aa] < [llength $bb]} {
        set tmp $aa
        set aa $bb
        set bb $tmp
        set switch 1
        unset tmp
    } else {
        set switch 0
    }
 
    if {[llength $aa] == [llength $bb]} {
        if {$aa == $bb} {
            return .
        }
        set i 0
        while {$i < [llength $aa]} {
            if {[join [lrange $aa 0 end-$i]] == [join [lrange $bb end-$i]]} {
                break
            }
            incr i
        }
        return [string repeat .. $i]; 
    }

    set i 0
    while { [lindex $aa $i] == [lindex $bb $i] } {
        incr i
    }
    set i [expr {[llength $aa] + 1 - $i}]
    set sep [file separator]
    if {$switch} {
        set string .
        for {set x 1} {$x <= $i} {incr x} {
            set string $string$sep[lindex $aa $x]
            incr i -1
        }
        return $string;
    } else {
       return [string repeat ..$sep [expr {$i-1}]]..
    }
  
}

Example:

% relTo $path1 $path2
..\..\..
% relTo $path2 $path1
.\Documents and Settings\Griffiths

You can then, for instance,

cd [file dirname $path1]
cd [relTo $path1 $path2]

to get from the directory of one file to another. And then to get back,

cd [relTo $path2 $path1]

EMJ 2004-12-16: Thanx very much. Since it won't do up the file tree and down a different branch, and also was giving funny answers on Linux, I started to play around with it, eventually ending up with the following, which does what I want:

# get relative path to target file from current file
# arguments are file names, not directory names (not checked)
proc pathTo {target current} {
    set cc [file split [file normalize $current]]
    set tt [file split [file normalize $target]]
    if {![string equal [lindex $cc 0] [lindex $tt 0]]} {
        # not on *n*x then
        return -code error "$target not on same volume as $current"
    }
    while {[string equal [lindex $cc 0] [lindex $tt 0]] && [llength $cc] > 1} {
        # discard matching components from the front (but don't
        # do the last component in case the two files are the same)
        set cc [lreplace $cc 0 0]
        set tt [lreplace $tt 0 0]
    }
    set prefix {}
    if {[llength $cc] == 1} {
        # just the file name, so target is lower down (or in same place)
        set prefix .
    }
    # step up the tree (start from 1 to avoid counting file itself
    for {set i 1} {$i < [llength $cc]} {incr i} {
        append prefix { ..}
    }
    # stick it all together
    file join {*}$prefix {*}$tt
}

AlexD 2012-06-03 11:12:23

In my case it was better to get relative path to target file from current path (not a file name). So I did some changes to the code provided by EMJ:

# Get relative path to target file from current path
# First argument is a file name, second a directory name (not checked)
proc relTo {targetfile currentpath} {
    set cc [file split [file normalize $currentpath]]
    set tt [file split [file normalize $targetfile]]
    if {![string equal [lindex $cc 0] [lindex $tt 0]]} {
        # not on *n*x then
        return -code error "$targetfile not on same volume as $currentpath"
    }
    while {[string equal [lindex $cc 0] [lindex $tt 0]] && [llength $cc] > 0} {
        # discard matching components from the front
        set cc [lreplace $cc 0 0]
        set tt [lreplace $tt 0 0]
    }
    set prefix {} 
    if {[llength $cc] == 0} {
        # just the file name, so targetfile is lower down (or in same place)
        set prefix .
    }
    # step up the tree
    for {set i 0} {$i < [llength $cc]} {incr i} {
        append prefix { ..}
    }
    # stick it all together
    file join {*}$prefix {*}$tt
}

Page Authors

PYK 2021-01-04
Fixed a typo, reworded some content, reformatted source code and page markup.