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 }