A quickie to apply patches generated by diff -n (so-called rcs format) - CMcC 20041006
# patch a file given diffs in rcs (diff -n) format package require fileutil namespace eval rcs {} # convert \n delimited file to an array indexed by line name proc rcs::file2array {filename arr} { upvar 1 $arr lines set lnum 0 fileutil::foreachLine line $filename { set lines([incr lnum]) $line } } # convert \n delimited text to an array indexed by line name proc rcs::text2array {text arr} { upvar 1 $arr lines set lnum 0 foreach line [split $text \n] { set lines([incr lnum]) $line } } # Apply some rcs diff -n format patches to the text in array proc rcs::patch {patch arr} { upvar 1 $arr lines set patch [split $patch \n] while {$patch != {}} { set pc [string trim [lindex $patch 0]] puts stderr "doing $pc" set patch [lrange $patch 1 end] switch -glob -- $pc { "" {} a* { foreach {start len} [split [string range $pc 1 end]] break set adding [join [lrange $patch 0 [expr {$len - 1}]] \n] if {[info exists lines($start)]} { append lines($start) \n } append lines($start) "$adding" puts stderr "ADD: '$adding'" set patch [lrange $patch $len end] puts stderr "$pc: $lines($start)" } d* { foreach {start len} [split [string range $pc 1 end]] break while {$len > 0} { puts stderr "DEL $start: $lines($start)" unset lines($start) incr start incr len -1 } } default { error "Unknown patch: '$pc'" } } } set result "" foreach lnum [lsort -integer [array names lines]] { append result \n $lines($lnum) } return [string range $result 1 end] }
Below comes a version to apply a patch to a directory tree for the most widely used unified context diff format, generated by diff -ruN old/ new/ This performs the analogous task of patch -p<striplevel> < patch
## Apply a patch in unified diff format # @synopsis{Patch directory striplevel patch} # # # @param[in] dir root directory of the original source tree # @param[in] striplevel number of path elements to be removed from the diff header # @param[in] patch output of diff -ru proc ApplyPatch {dir striplevel patch} { set patchlines [split $patch \n] set inhunk false set oldcode {} set newcode {} for {set lineidx 0} {$lineidx<[llength $patchlines]} {incr lineidx} { set line [lindex $patchlines $lineidx] if {[string match diff* $line]} { # a diff block starts. Next two lines should be # --- oldfile date time TZ # +++ newfile date time TZ incr lineidx set in [lindex $patchlines $lineidx] incr lineidx set out [lindex $patchlines $lineidx] if {![string match ---* $in] || ![string match +++* $out]} { puts $in puts $out return -code error "Patch not in unified diff format, line $lineidx $in $out" } # the quoting is compatible with list lassign $in -> oldfile lassign $out -> newfile set fntopatch [file join $dir {*}[lrange [file split $newfile] $striplevel end]] set inhunk false #puts "Found diffline for $fntopatch" continue } # state machine for parsing the hunks set typechar [string index $line 0] set codeline [string range $line 1 end] switch $typechar { @ { if {![regexp {@@\s+\-(\d+),(\d+)\s+\+(\d+),(\d+)\s+@@} $line \ -> oldstart oldlen newstart newlen]} { return code -error "Erroneous hunk in line $lindeidx, $line" } # adjust line numbers for 0-based indexing incr oldstart -1 incr newstart -1 #puts "New hunk" set newcode {} set oldcode {} set inhunk true } - { # line only in old code if {$inhunk} { lappend oldcode $codeline } } + { # line only in new code if {$inhunk} { lappend newcode $codeline } } " " { # common line if {$inhunk} { lappend oldcode $codeline lappend newcode $codeline } } default { # puts "Junk: $codeline"; continue } } # test if the hunk is complete if {[llength $oldcode]==$oldlen && [llength $newcode]==$newlen} { set hunk [dict create \ oldcode $oldcode \ newcode $newcode \ oldstart $oldstart \ newstart $newstart] #puts "hunk complete: $hunk" set inhunk false dict lappend patchdict $fntopatch $hunk } } # now we have parsed the patch. Apply dict for {fn hunks} $patchdict { puts "Patching file $fn" if {[catch {open $fn} fd]} { set orig {} } else { set orig [split [read $fd] \n] } close $fd set patched $orig set fail false set already_applied false set hunknr 1 foreach hunk $hunks { dict with hunk { set oldend [expr {$oldstart+[llength $oldcode]-1}] set newend [expr {$newstart+[llength $newcode]-1}] # check if the hunk matches set origcode [lrange $orig $oldstart $oldend] if {$origcode ne $oldcode} { set fail true puts "Hunk #$hunknr failed" # check if the patch is already applied set origcode_applied [lrange $orig $newstart $newend] if {$origcode_applied eq $newcode} { set already_applied true puts "Patch already applied" } else { puts "Expected:\n[join $oldcode \n]" puts "Seen:\n[join $origcode \n]" } break } # apply patch set patched [list {*}[lrange $patched 0 $newstart-1] {*}$newcode {*}[lrange $patched $oldend+1 end]] } incr hunknr } if {!$fail} { # success - write the result back set fd [open $fn w] puts -nonewline $fd [join $patched \n] close $fd } } }