#!/usr/bin/env wish-f # # usage: wtkdiff [diff opts] f1 f2 # wtkdiff - # # "weak" tkdiff, or "wayne's" tkdiff, # an attempt at a minimal version of John Klassa's tkdiff processor # # version history: # 960619: smooth tk v4 compatibility, revamp state machine in parsediff # 960616: initial release # # goals: - display neutral; the diffmarks are characters, so they # show up on b&w xterms, and can be cut-pasted into # mail/news or other plain text # (they are not, however, actual diff-style # context diff marks, a possible future improvement) # - fast; the diff processing is simpler, and only reads # the common lines once each (though it processes # non-different lines, so eventually a clever enough approach # to inserting diffs and reading twice would outpace it) # (though see perldodiff prototyped below) # - simple; the screen is simpler, with fewer widgets, and hence # might display over narrow-bandwidth X windows connections # a bit better, plus being in TNF (throop normal form), # with quit, tcl, and the other usual button panel regulars # Also, subwindow resizing and a small default startup # size in an attempt to deal with smaller displays # (eg, my14in home system) # - distributable; the actual diff can be rshed, and wtkdiff # doesn't have to have access to the remote file # namespace; it simply reads diff output only) # - pipeable; "-" can be used for either diff argument, so it can # be used at the end of pipelines, as in # sed 's/foo/bar' f | wtkdiff f - # - multiple-file browsing; can be used to browse diffs across files. # The usage "wtkdiff -" takes diff argument sets from stdin. # settings (can be reset in $env(HOME)/.wtkdiffrc) set opt(init) {} set opt(diffcmd) {diff -D--wtkdiffmarker-- %s} # if you have gnu diff, tabs can be handled thus: # set opt(diffcmd) {diff --expand-tabs -D--wtkdiffmarker-- %s} # otherwise some untabify operation can be added as a pipe fitting # set opt(diffcmd) {diff -D--wtkdiffmarker-- %s | expand} set opt(font) fixed set opt(height) 30 set opt(width) 40 set opt(listwidth) 200 set opt(listheight) 300 set opt(diffdiv) 20 catch {source $env(HOME)/.wtkdiffrc} # dance of the error routines, tk v3 vs v4 proc poperr {err} {if [catch {bgerror $err}] {tkerror $err}} # sugaring on the hard stuff proc rediff {} { # restart with (possibly rewritten) arguments from .argv dodiff [.argv get @1,1 @1,1lineend] } proc dodiff {cmd} { # set .argv, and catch diff errors (so arguments can be retyped & retried) # dance of the error routines, v3 vs v4 if [catch {parsediff $cmd} err] {poperr $err} } # the hard stuff proc parsediff {cmd} { global d l r opt .argv delete 1.0 end .argv insert 1.0 $cmd .left.text configure -state normal .right.text configure -state normal .left.text delete 1.0 end .right.text delete 1.0 end # d($n) -> diffnum of left recnum n # l($n) -> left recnum at top of diffnum n # r($n) -> right recnum at top of diffnum n catch {unset d} catch {unset l} catch {unset r} # d(c) -> diff count # d(.) -> diff number of current diff set d(c) 0 set d(.) 0 # process each line of diff -D output, # recording diff->line and line->diff pointers in d($p,$d(c)) and d($i($p)) set il 0 set ir 0 set dc 0 # note: state machine based on $x2 and $k; # presumes else only under ifndef, # and no null #if branch set k "=" set c(regexp) \ {[regexp {^#(ifndef|ifdef|else|endif) .*--wtkdiffmarker--} $x x1 x2]} set c(ifndef) { incr dc set dcl [format %3d $dc] set dcr [format %3d $dc] set l($dc) $il set r($dc) $ir set k "<1" } set c(ifdef) { incr dc set dcl [format %3d $dc] set dcr [format %3d $dc] set l($dc) $il set r($dc) $ir set k ">1" } set c(else) { set k ">1" } set c(endif) { set k "=1" } set c(<1) { incr il set d($il) $dc .left.text insert end "$dcl< $il $x\n" set dcl " " set k "<" } set c(<) { incr il set d($il) $dc .left.text insert end " < $il $x\n" } set c(>1) { incr ir .right.text insert end "$dcr> $ir $x\n" set dcr " " set k ">" } set c(>) { incr ir .right.text insert end " > $ir $x\n" } set c(=1) { incr il incr ir set d($il) $dc .left.text insert end "$dcl $il $x\n" .right.text insert end "$dcr $ir $x\n" set k "=" } set c(=) {incr il;incr ir;set d($il) $dc; .left.text insert end " $il $x\n" .right.text insert end " $ir $x\n" } # NOTE: if users configure the $opt(diffcmd), # they have to ensure it outputs in -D--wtkdiffmarker format # foreach x [split [exec sh -c "[format $opt(diffcmd) $cmd] ;exit 0"] \n] { # if $c(regexp) {eval $c($x2)} $c($k) # } foreach x [split [exec -- /bin/sh -c "[format $opt(diffcmd) $cmd];exit 0"] \n] {if [regexp {^#(ifndef|ifdef|else|endif) .*--wtkdiffmarker--} $x x1 x2] {eval $c($x2)} $c($k)} set d(c) $dc .panel.goto configure -text "goto(1,$d(c))" .left.text configure -state disabled .right.text configure -state disabled destroy .panel.goto.menu menu .panel.goto.menu if $d(c)<=$opt(diffdiv) { for {set i 1} {$i<=$d(c)} {incr i} { .panel.goto.menu add command -label $i -command "gotodiff $i" } } { for {set i 1} {$i<=$d(c)} {incr i $opt(diffdiv)} { .panel.goto.menu add cascade -menu .panel.goto.menu.c$i -label $i menu .panel.goto.menu.c$i for {set j 0} {$j<$opt(diffdiv)&&($i+$j)<=$d(c)} {incr j} { .panel.goto.menu.c$i add command -label [expr $i+$j] \ -command "gotodiff [expr $i+$j]" } } } } # possibly-faster parsing of diff -D output... # (for test only; currently disabled, header and trailer code snipped) proc perldodiff {cmd} { # process each line of diff -D output, # recording diff->line and line->diff pointers in d($p,$d(c)) and d($i($p)) eval [exec sh -c "$cmd ;exit 0" | perl -e { $oleft=0; $oright=1; $oboth=2; @k=("<",">"," "); $il=0; $ir=0; $dc=0; $ds=" "; $o=$oboth; while(<>) { s/([{}])/\\$1/g; if (/^#(ifndef |ifdef |else .*|endif .*)--wtkdiffmarker--/) { if (/^#ifndef --wtkdiffmarker--/) { $dc += 1; print "set l($dc) $il\n"; print "set r($dc) $ir\n"; $ds=sprintf("%3d",$dc); $o=$oleft; } elsif (/^#ifdef --wtkdiffmarker--/) { $dc += 1; print "set l($dc) $il\n"; print "set r($dc) $ir\n"; $ds=sprintf("%3d",$dc); $o=$oright; } elsif (/^#else .*--wtkdiffmarker--/) { $ds=sprintf("%3d",$dc); $o=$oright; } elsif (/^#endif .*--wtkdiffmarker--/) { $ds=" "; $o=$oboth; } } else { if ($o!=$oright) { $il += 1; print "set d($il) $dc\n"; print ".left.text insert insert {$ds$k[$o] $il $_} \n"; } if ($o!=$oleft) { $ir += 1; print ".right.text insert insert {$ds$k[$o] $ir $_} \n"; } $ds=" "; } print "set d(c) $dc\n"; } } ] } proc gotodiff {n} { global d l r if {$d(c)<$n} return if {$n<1} return .left.text yview $l($n).0 .right.text yview $r($n).0 set d(.) $n } proc nextdiff {} { global d if {$d(.)>=$d(c)} return incr d(.) gotodiff $d(.) } proc prevdiff {} { global d if {$d(.)<=1} return incr d(.) -1 gotodiff $d(.) } # linked scrolling (whew) proc bothset {args} { # left text controls left & center bars eval .scroll set $args eval .left.scroll set $args } proc bothview {args} { global d l r # center scroll controls left and right text eval .left.text yview $args set i [expr int([.left.text index @1,1])] if [catch {set d($i)}] {set d($i) $d(c)} set j [expr $d($i)+1] # adjust right window to align next difference (or end-of-file) .right.text yview [if $j>$d(c) { expr int($i-[.left.text index end]+[.right.text index end]) } { expr $i-$l($j)+$r($j) }].0 } proc hscroll-balance {args} { global opt # horizontal scroll to regulate relative left/right sizes # (this version 3.6 compatible... 4.0 needs more research) .left.text configure -width $args .right.text configure -width [expr $opt(width)*2-$args] .hscroll set [expr $opt(width)*2+1] 5 $args $args } # widget definitions # button panel pack [frame .panel] -side top -fill x pack [button .panel.quit -text quit -command {destroy .}] -side left pack [button .panel.rediff -text rediff -command rediff] -side left pack [button .panel.tcl -text tcl -command {eval [selection get]}] -side left pack [button .panel.prev -text prev -command prevdiff] -side left pack [button .panel.next -text next -command nextdiff] -side left pack [menubutton .panel.goto -menu .panel.goto.menu \ -relief raised -text "goto(1,0)" ] -side left menu .panel.goto.menu pack [button .panel.help -text help -command dohelp] -side right # argv display pack [text .argv -height 1 -width 10 -font fixed -borderwidth 2 -relief ridge ] -side top -fill x # left/right balance scrolling pack [scrollbar .hscroll -orient horizontal -command hscroll-balance -width 10 ] -side top -fill x # text display panes and scroll bars pack [scrollbar [frame .left].scroll -command ".left.text yview" ] -side left -fill y pack [text .left.text -font $opt(font) -yscroll bothset \ -wrap none -width $opt(width) -height $opt(height) ] -side left -fill both -expand y pack [text [frame .right].text -font $opt(font) \ -yscroll ".right.scroll set" \ -wrap none -width $opt(width) -height $opt(height) ] -side left -fill both -expand y pack [scrollbar .right.scroll -command ".right.text yview" ] -side left -fill y scrollbar .scroll -command bothview pack .left -side left -fill both -expand y pack .scroll -side left -fill y -anchor n pack .right -side left -fill both -expand y # allow for resizing (and set the balance scrollbar) hscroll-balance $opt(width) wm minsize . 100 100 # handle the two usage possibilities if [string match "-" $argv] { # extra widgets toplevel .wtkdiff-files pack [scrollbar .wtkdiff-files.scroll \ -command {.wtkdiff-files.text yview} ] -anchor w -side left -fill y -expand y pack [text .wtkdiff-files.text \ -font fixed -width 256 -yscroll {.wtkdiff-files.scroll set} ] -anchor w -side left -fill both -expand y wm minsize .wtkdiff-files 50 100 wm geometry .wtkdiff-files $opt(listwidth)x$opt(listheight) bind .wtkdiff-files.text { dodiff [%W get "@%x,%y linestart" "@%x,%y lineend"] } # read stdin for diff invocations to select if [gets stdin argv]>0 { .wtkdiff-files.text insert end $argv\n while {[gets stdin line]>0} { .wtkdiff-files.text insert end $line\n } } } # eval the users's post-widget init text, and proc the diff eval $opt(init) dodiff $argv # define the action for the help button (expects xd text displayer on $PATH) proc dohelp {} { exec xd -t wtkdiff-help << { usage: wtkdiff [diffopts] f1 f2 wtkdiff - The diff options and files are passed through to diff. If the argument list is "-", stdin is read for a set of option/file settings, one per line, and these can be browsed in random order. The wtkdiff screen has left and right display panes, and three scroll bars. The left scroll bar controls the left window independently, and similarly on the right. The central scroll bar is for synchronized scrolling, keeping corresponding text segments in the two windows aligned. Each line is annotated with a line number, and a differences key, "<" meaning the line is unique to the lefthand file, ">" to the righthand file, and each set of adjacent changes numbered. The button panel has quit, tcl, prev, next, goto and help buttons. - quit terminates wtkdiff - rediff reruns the diff (eg, to respond to edits in progress) - tcl execute the current X selection as a tcl script (eg, for on-the-fly customization or other settings) - prev jump both display panes to the previous difference - next jump both display panes to the next difference - goto jmp both display panes to the selected difference - help this message When started with usage "wtkdiff -", another toplevel window is opened, with diff invocations one per line, in a scrollable region. Rightclick on any of the lines in this window makes that diff the current diff, and executes it, setting the display panes. All the display panes are ordinary tk text widgets, so any of the displayed information can be selected, and copied into other windows. Or, for example, the "wtkdiff-files" window can be edited as ordinary text, so that new files to browse can be entered while running. Optional settings are organized like so set opt(init) {} set opt(diffcmd) {diff -D--wtkdiffmarker-- %s} set opt(font) fixed set opt(height) 30 set opt(width) 40 set opt(listwidth) 200 set opt(listheight) 300 set opt(diffdiv) 20 Note that if the $opt(diffcmd) is customized, the $argv will be substituted in the %s format. Further, whatever replacement is made, it must produce output in -D--wtkdiffmarker-- form, so that option must be either left intact, or simulated by whatever command is used to compute the diffs. Some of the many possible uses for the diffcmd setting... set opt(diffcmd) {diff --expand-tabs -D--wtkdiffmarker-- %s} set opt(diffcmd) {diff -D--wtkdiffmarker-- %s | expand} set opt(diffcmd) {rsh host "cd `pwd`;diff -D--wtkdiffmarker-- %s"} set opt(diffcmd) {$DIFFCMD %s} set opt(diffcmd) {rsh $DIFFHOST "cd $DIFFDIR; diff -D--wtkdiffmarker-- %s"} This last can be invoked with the options coming from the current environment, via eg wtkdiff foo.c bar.c or with the options specified for the single invocationi, via eg env DIFFHOST=wonky DIFFDIR=/usr/local/wonkysrc wtkdiff foo.c bar.c The opt settings can be altered in $HOME/.wtkdiffrc. It is read before any widgets are defined, so the $opt(x) settings can take effect. The $opt(init) will be executed as a tcl code fragment when all the widgets have been defined, for any additional/unanticipated customization desired. The widgets are .argv .panel.{quit,rediff,prev,next,goto{,.menu},help} .hscroll .left.{scroll,text} .scroll .right.{scroll,text} and in "wtkdiff -" usage .wtkdiff-files.{scroll,text} Examples: wtkdiff f1 f2 sed 's/foo/bar' file | wtkdiff file - co -p foo.c | wtkdiff - foo.c wtkdiff -B file - ls *.c | perl -ne ' chop; $p="old/$_"; print "$_ $p\n" if (stat($_))[8] != (stat("$p"))[8]; ' | wtkdiff - cleartool lsco -fmt "%En %En@@%PVn\n" | wtkdiff - } & }