Updated 2012-06-03 14:09:51 by AMG

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]
 }

[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]
}