Version 1 of Relative File Paths

Updated 2007-07-26 08:25:42 by colin

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

If I have two file paths a and b, how can I 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 Dec 14th 2004 - 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 indefinately; 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}]]..";
     }

 };# relTo

 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 Dec 16th 2004 - 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]
 }