Relative File Paths

Difference between version 4 and 5 - Previous - Next
[EMJ] Dec 14th 2004 -12-14:  I know this isn't really difficult, but if someone's got it somewhere...
If I haGiven twofile file paths a and b, how cadoes one I derive a relative path to refer to a from the location of b (the files and paths may not exist)?
[CMcC]: t There's a [tcllib] [fileutil] ''relative'' and a ''prefix'' command which facilitates this kind of file name manipulation.
[MG] Dec 14th 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 indefinaitely;. 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
            }         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}]]..";
     }
 
 
};# relTo
 
 relTo $path1 $path2
 % ..\..\..
 relTo $path2 $path1
 % .\Documents and Settings\Griffiths======
Example:

======none
% 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]======
cd D[reclTo 16$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 (the eval is to flatten the target list)
     return [eval file join {*}$prefix {*}$tt]
 }
======

----'''[AlexD] - 2012-06-03 11:12:23'''
'''[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 (the eval is to flatten the targetfile list)
  return [eval file join {*}$prefix {*}$tt]
}
======


** Page Authors **

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

<<categories>> File