#!/usr/bin/perl ############################################################################### # modfilegen.pl # ############################################################################### # Generates a mod file from two YaBB source code trees # # Date started: August 6th, 2005 # # Version: 1.2 [2005-12-01] # # Released by AK108 for use by the YaBB community # ############################################################################### ############################################################################### # See bottom of file for POD. # ############################################################################### our($version) = 'modfilegen v1.2'; # Version ID use strict; use warnings; ### Declare globals: our(%opt, %arguments, @changes, $outputhead, $generatortag, $difftempfile); # Globals that are used all over our($option, $data, $j, $i); # Globals for the argument parsing routine our($prevfile, $stepnum, $modoutput, $item); # Globals for the modfile output routine ### Output and errors. sub error { ### Recoverable error print STDERR 'modfilegen.pl: ', @_, "\n"; } sub fatal_error { ### Error that we can't continue from my($errorcode); if($_[0] =~ /\A\d+\Z/) {$errorcode = shift @_;} print STDERR 'modfilegen.pl: ', @_, "\n"; if($errorcode) {exit $errorcode;} else {exit 1;} } sub usage_error { ### Fatal error caused by lack of an argument or something similar &fatal_error(@_, q~ Usage: ./modfilegen.pl [OPTIONS]... See "./modfilegen.pl --help" for help Also use "./modfilegen.pl --pod" or "perldoc modfilegen.pl" for detailed help ~); } ### Routines for output at different verbose levels sub verboseout { if($opt{'verbose'}) {print @_;} } sub veryverboseout { if($opt{'veryverbose'}) {print @_;} } sub normalout { unless($opt{'quiet'}) {print @_;} } ### Find out the arguments %arguments = (); for($i = 0; $i < @ARGV; $i++) { if($ARGV[$i] =~ /\A\-/) { ### Allows a trash option. Scary, huh? $option = $ARGV[$i]; $option =~ s/\A\-\-?//; if($option ne 'V') {$option = lc($option);} $arguments{$option} = ''; $j = $i; while(++$j && $ARGV[$j] && $ARGV[$j] =~ /\A[^-]/) { ### This one had some data with it if($arguments{$option}) {$arguments{$option} .= '|';} # Use pipes cause you can't have files with pipes in them $arguments{$option} .= &urldecode($ARGV[$j]); } if(!$arguments{$option}) {$arguments{$option} = 1;} } } ### Other methods if($arguments{'url-encode'}) {shift @ARGV; print &urlencode(join(' ', @ARGV)), "\n"; exit 0;} if($arguments{'url-decode'}) {shift @ARGV; print &urldecode(join(' ', @ARGV)), "\n"; exit 0;} if($arguments{'url-full-encode'}) {shift @ARGV; print &urlencode(join(' ', @ARGV), 1), "\n"; exit 0;} if($arguments{'?'} || $arguments{'help'}) {&help; exit 0;} if($arguments{'pod'}) {system("perldoc $0") || &fatal_error('Couldn\'t read POD documentation'); exit 0;} ### Turn the argument hash into something a little more useful. Also test these options for correctness. if($arguments{'s'} || $arguments{'stock-dir'}) { $opt{'stock'} = $arguments{'s'} || $arguments{'stock-dir'}; $opt{'stock'} =~ s~\/\Z~~; } else {&usage_error('-s or --stock-dir required');} if($arguments{'m'} || $arguments{'modded-dir'}) { $opt{'modded'} = $arguments{'m'} || $arguments{'modded-dir'}; $opt{'modded'} =~ s~\/\Z~~; } else {&usage_error('-m or --modded-dir required');} if($arguments{'o'} || $arguments{'output-file'}) { $opt{'output'} = $arguments{'o'} || $arguments{'output'} || ''; } else {&error('No file given, assuming STDOUT');} if($arguments{'v'} || $arguments{'verbose'}) { $opt{'verbose'} = 1; } if($arguments{'V'} || $arguments{'very-verbose'}) { $opt{'verbose'} = 1; $opt{'veryverbose'} = 1; } if($arguments{'q'} || $arguments{'quiet'}) { undef $opt{'verbose'}; undef $opt{'veryverbose'}; $opt{'quiet'} = 1; } if($arguments{'t'} || $arguments{'temp-dir'}) { $opt{'tempdir'} = $arguments{'temp-dir'} || $arguments{'t'}; if(!-d $opt{'tempdir'}) {$opt{'tempdir'} = '/tmp';} if(-d $opt{'tempdir'} && $opt{'tempdir'} eq '1') {&error('Your temporary directory, 1, exists. This may not be what you want, though.');} $opt{'tempdir'} =~ s~\/\Z~~; # Chop trailing / off if needed $difftempfile = "$opt{'tempdir'}/modfilegen.diffoutput." . time; } if($arguments{'fast'}) { $opt{'fast'} = 1; undef $opt{'tempdir'}; } if($arguments{'c'} || $arguments{'perl-check'}) { $opt{'perlcheck'} = 1; } if($arguments{'mark-changes'}) { $outputhead = qq~\#\#\# Modified by $version\n~; } else {$outputhead = '';} if($arguments{'smart-yabb2'}) {$opt{'smart-yabb2'} = 1;} if($arguments{'no-generator-id'}) {$generatortag = '';} else {$generatortag = "$version";} if($arguments{'d'}) {$arguments{'diff'} = $arguments{'d'};} if(!$arguments{'diff'}) {$arguments{'diff'} = 1;} if($arguments{'diff'} eq 'internal' || index($arguments{'diff'}, 'mfg') == 1) {$opt{'diffmethod'} = 'mfgdiff';} elsif($arguments{'diff'} eq 'external' || $arguments{'diff'} eq 'diff' || $arguments{'diff'} == 1) {$opt{'diffmethod'} = 'diff';} else {&usage_error("The engine $arguments{'diff'} was not recognized.");} # Default diff method @changes = (); if($arguments{'smart-yabb2'}) { ### We're doing a smart compare of YaBB 2 files. my($result, $result2); # First things first. _ALL_ mods need to change Admin/ModList.pl and AdminIndex.pl # Even YaBB upgrade mods change it (to change the version). (But they'd use the non-smart usage anyway) $result = &comparefile("$opt{'stock'}/AdminIndex.pl", "$opt{'modded'}/AdminIndex.pl") || &error('You are doing a smart compare on YaBB 2. But you didn\'t change AdminIndex.pl.'); $result2 = &comparefile("$opt{'stock'}/Admin/ModList.pl", "$opt{'modded'}/Admin/ModList.pl") || &error('You are doing a smart compare on YaBB 2. But you didn\'t change ModList.pl.'); unless($result && $result2) {&error('Your modifications do not appear to be compliant with the YaBB 2 modwriting rules.');} &comparefile("$opt{'stock'}/YaBB.pl", "$opt{'modded'}/YaBB.pl"); &comparedirectory('Sources', 0, '.pl'); &comparedirectory('Admin', 0, '.pl', '^ModList.pl'); # Usually mods don't change public_html or anything else. So we can ignore those files. } elsif($opt{'stock'} =~ /\|/ && $opt{'modded'} =~ /\|/) { ### It's a group of files that were space delimited but now are pipe delimited due to my parsing routine ### We're usually here when someone uses something like "stockdir/*" ### Let's right the files into a hash so we can easily check the existence of common files my(@filestocompare, %stockfiles, $item); %stockfiles = (); foreach (split(/\|/, $opt{'stock'})) { ### This part's hard. $opt{'stock'} is our pipe delimited list of files. ### But $opt{'stock'} should be the directory name. So we fake it here and change it later. $_ =~ s~.*\/~~; $stockfiles{$_} = 1; } foreach (split(/\|/, $opt{'modded'})) { $_ =~ s~.*\/~~; if($stockfiles{$_}) {push(@filestocompare, $_);} } ### Now fix $opt{'stock'} my($realdir, $trash) = split(/\|/, $opt{'stock'}); $realdir =~ m~(.*)\/~; $opt{'stock'} = $1 || &fatal_error('I couldn\'t fix $opt{\'stock\'}; exiting. Try using a directory name instead of a list of files.'); ($realdir, $trash) = split(/\|/, $opt{'modded'}); $realdir =~ m~(.*)\/~; $opt{'modded'} = $1 || &fatal_error('I couldn\'t fix $opt{\'modded\'}; exiting. Try using a directory name instead of a list of files.'); foreach $item (@filestocompare) { &veryverboseout("Item is $opt{'stock'}/$item\n"); if(-d "$opt{'stock'}/$item" && $item !~ /\A\.\.?\Z/) {&comparedirectory("$item", 1);} elsif(!-d "$opt{'stock'}/$item") {&comparefile("$opt{'stock'}/$item", "$opt{'modded'}/$item");} } } elsif(!-d $opt{'stock'} && !-d $opt{'modded'}) { ### One file to compare &comparefile($opt{'stock'}, $opt{'modded'}); } elsif(-d $opt{'stock'} && -d $opt{'modded'}) { ### It's a directory ### Let's write the files into a hash so we can easily check the existence of common files my(@filestocompare, %stockfiles, $item); %stockfiles = (); opendir(STOCKDIR, $opt{'stock'}) || &fatal_error("Couldn't open stock directory \"$opt{'stock'}\": $!"); while($_ = readdir(STOCKDIR)) {$stockfiles{$_} = 1;} closedir(STOCKDIR); opendir(MODDEDDIR, $opt{'modded'}) || &fatal_error("Couldn't open modded directory \"$opt{'modded'}\": $!"); while($_ = readdir(MODDEDDIR)) {if($stockfiles{$_}) {push(@filestocompare, $_);}} close(MODDEDDIR); foreach $item (@filestocompare) { if(-d "$opt{'stock'}/$item" && $item ne '..' && $item ne '.') {&comparedirectory($item, 1);} elsif(!-d "$opt{'stock'}/$item") {&comparefile("$opt{'stock'}/$item", "$opt{'modded'}/$item");} } } else {&fatal_error('Dunno quite what you want me to do.');} sub comparedirectory { ### Compares each file in a directory if($arguments{'no-recursive'}) {return 0;} my($directory, $sublimit) = ($_[0], $_[1]); my(@filestocompare, %stockfiles, $item, %filetypesallowed, %filelimits, $filesarelimited, $filetypesarelimited); $sublimit ||= 0; # This is needed so your computer can't be killed too badly by comparing the root directory. if($sublimit > 5) {return 0;} # More then 5 levels deep. if(!-d "$opt{'stock'}/$directory" || !-d "$opt{'modded'}/$directory") {return 0;} # Set up the file extension limiting if($_[2] && $_[2] ne '*') { $filetypesarelimited = 1; foreach (split(/\|/, $_[2])) { if($_) { $filetypesallowed{$_} = 1; } } } # Set up file exclusions # These are individual file names. Use extension limiting if that's a problem. if($_[3]) { $filesarelimited = 1; foreach (split(/\^/, $_[3])) { if($_) { $filelimits{$_} = 1; } } } opendir(STOCKDIR, "$opt{'stock'}/$directory") || &fatal_error("Couldn't open stock directory \"$opt{'stock'}/$directory\": $!"); while($_ = readdir(STOCKDIR)) {$stockfiles{$_} = 1;} closedir(STOCKDIR); opendir(MODDEDDIR, "$opt{'modded'}/$directory") || &fatal_error("Couldn't open modded directory \"$opt{'modded'}/$directory\": $!"); while($_ = readdir(MODDEDDIR)) {if($stockfiles{$_}) {push(@filestocompare, $_);}} close(MODDEDDIR); foreach $item (@filestocompare) { if($filesarelimited && !-d "$opt{'stock'}/$directory/$item") { # Limit by filename if($filelimits{$item}) {next;} } if($filetypesarelimited && !-d "$opt{'stock'}/$directory/$item") { # Limit by file extension $item =~ /(\..*?)\Z/; # The . is part of the extension too if($1 && !$filetypesallowed{$1}) {next;} } if(-d "$opt{'stock'}/$directory/$item" && $item ne '.' && $item ne '..') {&comparedirectory("$directory/$item", $sublimit+1);} elsif(!-d "$opt{'stock'}/$directory/$item") {&comparefile("$opt{'stock'}/$directory/$item", "$opt{'modded'}/$directory/$item");} } } sub comparefile { if($opt{'perlcheck'} && ($_[0] =~ /\.pl\Z/ || $_[0] =~ /\.cgi\Z/)) { my($result, $result2, @tempoutput); ### This may not be portable! ### Your mileage may vary. open(PERLC, "perl -c $_[0] 2>&1 |"); @tempoutput = ; close(PERLC); $result = join('', @tempoutput); open(PERLC, "perl -c $_[1] 2>&1 |"); @tempoutput = ; close(PERLC); $result2 = join('', @tempoutput); chomp $result; chomp $result2; if($result !~ /syntax OK\Z/) {&error("perl -c found errors in $_[0]");} if($result2 !~ /syntax OK\Z/) {&error("perl -c found errors in $_[1]");} undef $result; undef $result2; } if($opt{'diffmethod'} eq 'mfgdiff') {return &mfgdiff(@_);} elsif($opt{'diffmethod'} eq 'diff') {return &externaldiff(@_);} else {&fatal_error('diff engine internal choice caused problems. You shouldn\'t be able to get this error.');} } sub externaldiff { my($stockfile, $moddedfile, $errorcode, @diff, $diff, $trash, $item); ($stockfile, $moddedfile) = @_; &verboseout("Comparing $stockfile to $moddedfile\n"); # Now there are 3 ways of handling the errorcode/output problem: # The old way: Test with system(), do it again and read with open() # The old way, but more much faster: Read with open(), see if it was changed based on the data # The new slowest way: Use system() to check the errorcode and write it to a tempfile, and read from the file. if(!$arguments{'fast'}) { system("diff -U 2 --ignore-all-space --ignore-blank-lines $stockfile $moddedfile > /dev/null") || return; $errorcode = $? >> 8; if($errorcode == 2) { # diff reports trouble &fatal_error("diff didn't like the stock file \"$stockfile\" or modded file \"$moddedfile\". Try running \"diff -U 2 $stockfile $moddedfile\" to see diff's errors."); } if($errorcode == 0) {return 0;} # No changes in the file } if($opt{'tempdir'}) { system("diff -U 2 --ignore-all-space --ignore-blank-lines $stockfile $moddedfile > $difftempfile") || return; $errorcode = $? >> 8; if($errorcode == 2) { # diff reports trouble &fatal_error("diff didn't like the stock file \"$stockfile\" or modded file \"$moddedfile\". Try running \"diff -u $stockfile $moddedfile\" to see diff's errors."); } if($errorcode == 0) {return 0;} # No changes in the file open(FILE, $difftempfile) || &fatal_error("Cannot read from temporary file $difftempfile: $!"); @diff = ; close(FILE); unlink($difftempfile); } else { open(DIFF, "diff -U 2 --ignore-all-space --ignore-blank-lines $stockfile $moddedfile |") || &fatal_error("Couldn't diff stock file \"$stockfile\" to modded file \"$moddedfile\": $!"); $trash = ; $trash = ; $trash = ; @diff = ; close(DIFF); } $diff = join('', @diff); if($diff !~ /\n\+/ && $diff !~ /\n\-/) {return 0;} @diff = split(/\@\@ .*? \@\@\n/, $diff); foreach $item (@diff) { my($precontext, $postcontext, $subitem, $stockline, $moddedline, $changetype); foreach $subitem (split(/\n/, $item)) { $subitem =~ s/\A(.)//; if($1 eq ' ') { # Context $moddedline .= $subitem . "\n"; $stockline .= $subitem . "\n"; } elsif($1 eq '-') { # Stock $stockline .= $subitem . "\n"; $changetype ||= 'replace'; } elsif($1 eq '+') { # Modded $moddedline .= $subitem . "\n"; $changetype ||= 'add'; } } $changetype ||= 'Huh?'; if(!$stockline || !$moddedline) { # Handle stuff matching on nothing &veryverboseout('$stockline or $moddedline false in ', $stockfile, "\n"); next; } push(@changes, "$stockfile|$changetype|" . &urlencode($stockline) . '|' . &urlencode($moddedline)); ### Did I miss anything? No? Wow, this is scary. # If only making the algorithm was this easy... } return 1; } sub mfgdiff { my(@stock, @modded, $stockfile, $moddedfile, $file, $stocklen, $moddedlen, $stockcnt, $moddedcnt, $i, $tmpstockcnt, $tmpmoddedcnt, $foundmatch, $changetype, $stockstr, $moddedstr); ($stockfile, $moddedfile) = @_; &verboseout("Comparing $stockfile to $moddedfile\n"); ### First things first: open the files and load them open(STOCK, "<$stockfile") || &fatal_error("Couldn't open stock file \"$stockfile\": $!"); @stock = ; close(STOCK); open(MODDED, "<$moddedfile") || &fatal_error("Couldn't open modded file \"$moddedfile\": $!"); @modded = ; close(MODDED); $stockstr = join('', @stock); $moddedstr = join('', @modded); $stocklen = scalar @stock; $moddedlen = scalar @modded; $stockcnt = 0; $moddedcnt = 0; for($i = 0; ($stockcnt < $stocklen || $moddedcnt < $moddedlen); $i++) { ### Is the line the same? if($modded[$moddedcnt] && $stock[$stockcnt] && $modded[$moddedcnt] eq $stock[$stockcnt]) {$stockcnt++; $moddedcnt++;} ### Yes, continue on else { ### No. Try desperately to make up the difference &veryverboseout("Difference found at $stockcnt and $moddedcnt\n"); ### Was a line added or changed? We can determine this if we know the next point the files match at. $changetype = 0; $foundmatch = 0; $tmpstockcnt = 0; $tmpmoddedcnt = 0; for($tmpstockcnt = $stockcnt; $tmpstockcnt < $stocklen && !$foundmatch; $tmpstockcnt++) { for($tmpmoddedcnt = $moddedcnt; $tmpmoddedcnt < $moddedlen && !$foundmatch; $tmpmoddedcnt++) { if($modded[$tmpmoddedcnt] eq $stock[$tmpstockcnt] && $modded[$tmpmoddedcnt] ne "\n") { &veryverboseout('Found that modded[', $tmpmoddedcnt+1, '] is equal to stock[', $tmpstockcnt+1, "]\n"); $foundmatch = 1; if($tmpmoddedcnt == $tmpstockcnt) {$changetype = 1;} ### Line was changed elsif($tmpmoddedcnt != $tmpstockcnt) {$changetype = 2;} ### A line was added else {$changetype = 0;} ### Match wasn't found } } } #print 'Stock line ', $stockcnt, ' was changed like this: ', $changetype, "\n"; ### Ok, we know what type of change it is: Added a line, replaced a line, or changed until end of file. #&pause(); my($ambig) = ''; if($changetype == 1) { # Changed a line ### Expand the current match if we can my($stockline, $moddedline, $pos); $stockline = $stock[$stockcnt]; $moddedline = $modded[$moddedcnt]; # Does this match in the stock file occur more then once? $stockstr =~ m/\Q$stockline\E/g; $pos = pos($stockstr); $stockstr =~ m/\Q$stockline\E/g; $_ = 0; while($pos && pos($stockstr) && $pos != pos($stockstr) && ++$_) { # Ambigious case :( #print "Ambigious case loop: \#$_\n"; $ambig = 'ambig'; # Reset pos pos($stockstr) = 0; # Expand the match behind the change if($stock[$stockcnt-$_] && $modded[$moddedcnt-$_] && $stock[$stockcnt-$_] eq $modded[$moddedcnt-$_]) { $stockline = $stock[$stockcnt-$_] . $stockline; $moddedline = $modded[$moddedcnt-$_] . $moddedline; } else {last;} # Provide checking data for while loop $stockstr =~ m/\Q$stockline\E/g; $pos = pos($stockstr); $stockstr =~ m/\Q$stockline\E/g; } push(@changes, "$stockfile|change|" . &urlencode($stockline) . '|' . &urlencode($moddedline) . "|$stockcnt|$moddedcnt|$ambig"); $stockcnt++; $moddedcnt++; } elsif($changetype == 2) { # Added a line(s) #&pause("-", join('', @stock[$stockcnt..$tmpstockcnt-1]), "-"); #if(join('', @stock[$stockcnt..$tmpstockcnt-1]) =~ /\A\n*\Z/) { ### Do some magic to prevent it from matching on blank lines # my($magicnum) = 1; # while(join('', @stock[$stockcnt-$magicnum..$tmpstockcnt-1]) =~ /\A\n*\Z/s) {$magicnum++;} # push(@changes, "$stockfile|add|" . &urlencode(join('', @stock[$stockcnt-$magicnum..$tmpstockcnt-1])) . '|' . &urlencode(join('', @modded[$moddedcnt-$magicnum..$tmpmoddedcnt-1])) . "|$tmpstockcnt|$tmpmoddedcnt|magic"); # $stockcnt += $tmpstockcnt - $stockcnt; # $moddedcnt += $tmpmoddedcnt - $moddedcnt; #} #else { ### OK, we found the changed area. But let's try to make it a more unique match. my($stockline, $moddedline, $pos); $stockline = join('', @stock[$stockcnt..$tmpstockcnt-1]); $moddedline = join('', @modded[$moddedcnt..$tmpmoddedcnt-1]); # Does this match in the stock file occur more then once? $stockstr =~ m/\Q$stockline\E/g; $pos = pos($stockstr); $stockstr =~ m/\Q$stockline\E/g; $_ = 0; foreach (1..5) { #while($pos && pos($stockstr) && $pos != pos($stockstr) && ++$_ && $stock[$stockcnt-$_] ne $stock[-1]) { # Ambigious case :( $ambig = 'ambig1'; # Reset pos pos($stockstr) = 0; # Expand the match behind the change if($stock[$stockcnt-$_] && $modded[$moddedcnt-$_] && $stock[$stockcnt-$_] eq $modded[$moddedcnt-$_]) { $stockline = $stock[$stockcnt-$_] . $stockline; $moddedline = $modded[$moddedcnt-$_] . $moddedline; } else {last;} # Provide checking data for while loop $stockstr =~ m/\Q$stockline\E/g; $pos = pos($stockstr); $stockstr =~ m/\Q$stockline\E/g; } $_ = 0; pos($stockstr) = 0; $stockstr =~ m/\Q$stockline\E/g; $pos = pos($stockstr); $stockstr =~ m/\Q$stockline\E/g; foreach (0..5) { #while($pos && pos($stockstr) && $pos != pos($stockstr) && ++$_) { # Ambigious case :( if($ambig) {$ambig = 'ambig3';} else {$ambig = 'ambig2'}; # Reset pos pos($stockstr) = 0; # Expand the match after the change if($stock[$stockcnt+$_] && $modded[$moddedcnt+$_] && $stock[$stockcnt+$_] eq $modded[$moddedcnt+$_]) { $stockline .= $stock[$stockcnt+$_]; $moddedline .= $modded[$moddedcnt+$_]; } else {last;} # Provide checking data for while loop $stockstr =~ m/\Q$stockline\E/g; $pos = pos($stockstr); $stockstr =~ m/\Q$stockline\E/g; } pos($stockstr) = 0; $stockstr =~ m/\Q$stockline\E/g; $pos = pos($stockstr); $stockstr =~ m/\Q$stockline\E/g; ### Acid3 bugfix foreach (1..5) { if(!$ambig && $stockcnt > $_ && $stock[$stockcnt-$_] && $modded[$moddedcnt-$_] && $stock[$stockcnt-$_] eq $modded[$moddedcnt-$_]) { $stockline = $stock[$stockcnt-$_] . $stockline; $moddedline = $modded[$moddedcnt-$_] . $moddedline; } else {last;} } ### push(@changes, "$stockfile|add|" . &urlencode($stockline) . '|' . &urlencode($moddedline) . "|$tmpstockcnt|$tmpmoddedcnt|$ambig"); $stockcnt += $tmpstockcnt - $stockcnt - 1; $moddedcnt += $tmpmoddedcnt - $moddedcnt - 1; #} } else { # I don't think there's a match; we're done here since EOF is reached. # TODO: Find out if this needs to be implemented $stockcnt++; $moddedcnt++; } } } } chomp @changes; &normalout(qq~ Summary: Files with changes: ~); # Show modfilegen pride! *shifty eyes go here* $modoutput = qq~$generatortag ~; $stepnum = 1; foreach $item (@changes) { my($file, $action, $find, $replace, $stockline, $moddedline, $magic) = split(/\|/, $item); my($tmpfile); if(($prevfile && $file ne $prevfile) || !$prevfile) { $tmpfile = $file; ### Try to cut out the extra junk of the directory prefix. BoardMod adds that for us. if(-d $opt{'stock'}) {$tmpfile =~ s~\A$opt{'stock'}\/~~;} elsif(-f $opt{'stock'}) { my($tmpstockdir) = $opt{'stock'}; $tmpstockdir =~ s~\A(.*)\/~~; $tmpstockdir = $1 || &error("Sorry, I couldn\'t help with trimming the stock directory ($opt{'stock'}) from the file to edit ($tmpfile). Your mod may not work without minor changes."); $tmpfile =~ s~\A$tmpstockdir\/~~; } $modoutput .= qq~ $tmpfile ~; &normalout("$tmpfile\n"); } $find = &urldecode($find); chomp $find; $replace = &urldecode($replace); chomp $replace; ### Note: $stepnum stores the mod step number. Useful if you're trying to figure out what the number of a step is. ### You can uncomment the below line to include this in the mod file. It's useful for debugging ### and BoardMod ignores them too, at least on BM 2.5.4 (Linux) $modoutput .= qq~$stepnum~; ### Note: Just as $stepnum has the mod step number, $action has the type of match that was found earlier. ### Uncomment the line below to have this outputted in the mod file. #$modoutput .= qq~$action~; ### Note: This is a debug output. What this will tell you changes from time to time. #if($magic) {$modoutput .= qq~$magic~;} ### It's important not to spit more newlines then needed -- it makes things go boom in BoardMod ### Be very careful with indenting -- whitespaces matters here, unlike most other places $modoutput .= qq~ $find $outputhead$replace ~; ### NOTE: $stocknum and $moddednum include the lines of the last match in the stock files and modded files. ### You can uncomment the below line to include this in the mod file. It's useful for debugging ### and BoardMod ignores them too, at least on BM 2.5.4 (Linux) #$modoutput .= qq~$stockline~; #$modoutput .= qq~$moddedline\n\n~; $prevfile = $file; $stepnum++; } &normalout(scalar @changes, " changes made\n"); if($opt{'output'}) { open(FILE, ">$opt{'output'}") || &fatal_error("Couldn't open output file \"$opt{'output'}\": $!"); print FILE $modoutput; close(FILE); } else { print $modoutput; } exit 0; sub help { # 80 character ruler to keep things on one line. ################################################################################ print qq~This is $version Usage: ./modfilegen.pl [OPTIONS]... Generates a mod file of the differences between two YaBB source code trees. Mandatory arguments to long options are mandatory for short options too. Required: -s, --stock-dir DIR Location of the unmodded directory -m, --modded-dir DIR Location of the modded directory -o, --output-file FILE File to write the mod to Others: -d, --diff ENGINE Change which engine (internal|external) to use --mark-changes Add Perl comments before each change --smart-yabb2 Compares files likely to change in mods for YaBB 2 --no-recursive Do not compare subdirectories --fast Trade some safety for speed when using external diff -c, --perl-check Use "perl -c" to check .pl and .cgi files -v, --verbose Display more information while generating modfile --pod View this file\'s POD using perldoc --url-encode TEXT HTML URL Encodes the rest of the options -q, -V, --url-decode, and -t are covered in the POD. Check out the POD for more specific details on these options. ~; ################################################################################ ### Not implemented options. Could be some day. # -c, --perl-check Use "perl -c" to check the modded-tree for errors ### After the modding if -T # -i, --incremental-write Write each mod instruction as it is found # -T, --test-output Test the output modfile. Makes it larger/safer ### } sub urlencode { my($text, $mode) = ($_[0], $_[1]); #$text =~ s/(\W)/sprintf("%%%lx", ord($1));/eg; # Not good enough; doesn't make it 2 digits all the time ### "Real Perl Hackers Use 'pack'" (tm) - jbert on Perlmonks if(!$mode) {$text =~ s/(\W)/'%' . unpack("H*", pack("C", ord($1)))/eg;} elsif($mode) {$text =~ s/(.)/'%' . unpack("H*", pack("C", ord($1)))/eg;} return $text; } sub urldecode { my($text) = $_[0]; $text =~ s/\%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; return $text; } sub pause { print join('', @_); system("stty", '-icanon', 'eol', "\001"); print 'Press any key to continue... '; my($trash) = getc(STDIN); print ' Resuming.', "\n"; system "stty", 'icanon', 'eol', '^@'; # ASCII null } sub system { # Overwrite the system call with one that returns the real error code. CORE::system(@_); if ($? == -1) { return ''; # Failed to execute; return a null string. } elsif ($? & 127) { return 0; # Died, return 0. } else { return 1; # Success; return 1. } } exit 0; =head1 NAME modfilegen.pl - A YaBB modfile generator Version 1.2 =head1 SYNOPSIS $ ./modfilegen.pl -s stockdir -m moddir -o mymod.mod $ ./modfilegen.pl OPTIONS =head1 DESCRIPTION This script is able to compare two YaBB directories and give you a "modfile", which is a file that can be used to modify a YaBB install using the BoardMod program. It can be used on non-YaBB code as well, but you might be better off using diff. =head2 Options Options can be given in any order. To include spaces or other characters, use the HTML URL encoding. You can use the C<--url-encode TEXT> method to encode special characters in C. Arguments for long options are required for short options too. =head3 Required =over 12 =item C<-s> or C<--stock-dir> Location of the directory or file to assume as unmodded. =item C<-m> or C<--modded-dir> Location of the directory or file that has been modded. Note: If the -s and -m are files, it compares the files. If they are both directories, it compares all the files in the directory, recursively. If only one of -s and -m is a file, then it compares files, not directories. Hopefully. =item C<-o> or C<--output-file> File to output results to. Assumes STDOUT, so this is not really required if you are using quiet. =back =head3 Optional =over 12 =item C<-d> or C<--diff> Select the engine modfilegen uses to generate the modfile. The safest engine is selected by default. Engines: C or C: The internal routine that was used exclusively in the first release. It is not as tested as the others, but is much more portable. C or C: This relies on the external C command and then interpets the result from diff. C or C: This uses the same conversion as diff/external but uses the Perl module to provide an equivilent to C, which should be useful if you have Algorithm::Diff available and would rather use it instead of diff. NOT IMPLEMENTED; uses diff/external instead. C (default): Picks the safest engine out of the ones available. This is the default, and the actual engine choosen that is used depends on the version of modfilegen. This version's default engine is diff/external. =item C<--fast> When using the external diff engine: Skips the preliminary checking of diff's exit code. This prevents modfilegen from checking if diff failed. Has no effect under other engines. =item C<-t [DIR]> or C<--temp-dir [DIR]> Uses temporary files whenever it should benefit us. DIR is optional and uses /tmp if DIR is not specified. It seems like this should be faster, but may not be due to hard drive speed. =item C<-c> or C<--perl-check> Runs "perl -c" on each of the source and modded files with the extension of .pl or .cgi. This helps make sure that no files are corrupted and that no stupid errors were made. This is most likely B portable. Some files will fail "perl -c" due to not finding modules. =item C<--mark-changes> Puts "Modified by $version" at the top of each step, where $version is the version ID (to check the version ID, use C<--help> or see top of this document). IMPORTANT! This can break BoardMod's uninstall abilities. =item C<--no-recursive> Does not go into subdirectories. Normally, modfilegen will go up to 5 directories deep from the stock and modded directories trying to find changes in files. =item C<--no-generator-id> Keeps modfilegen from outputting the generator id value. =item C<--smart-yabb2> Compares files in YaBB 2 most likely to be changed by a mod. These include (all in cgi-bin/yabb2): Sources/*.pl, YaBB.pl, AdminIndex.pl, Admin/*.pl This option also tells you if you didn't modify AdminIndex.pl and Admin/ModList.pl. They must be modified for Y2 mod compliance. =item Note about verbose, very verbose, and quiet options Most of the output has not been added yet. I advise using -v by default. -q has no effect, as there is no output it would prevent. -V gives some debug level output. =item C<-v> or C<--verbose> Display information while generating a modfile. Using this could take longer then normal depending on your text output speed. =item C<-V> or C<--very-verbose> Displays even more information then -v. Implies -v. Could be considered trival or debug-level information. =item C<-q> or C<--quiet> Shows no output except for error messages. =back =head3 Other modes of operation =over 12 =item C<-?> or C<--help> Displays a condensed help message without using POD. =item C<--pod> View POD documentation (AKA this documentation). Is the same as running C. =item C<--url-encode> Encodes the rest of the arguement using the HTML URL method. You can use this to encode an option if you do not know the HTML URL method. =item C<--url-decode> Decodes the rest of the arguement using the HTML URL method. You can use this to decode an option if you do not know the HTML URL method. =back =head1 VERSIONING SYSTEM My versioning system has two different styles: =head4 Development build Example: [DEV 2005-09-14] This is a build that is a snapshot of what I am currently programming on my local copy. It is not guarenteed to be stable or useable. =head4 Milestone release Example: v1.0 Milestone releases are considered more stable and more useable then development builds. =head1 BUGS See the changelog at BoardMod for more details. Match engine has not been throughly tested. The mfgdiff engine is mostly unusable. The "add before" and "add after" functions of BoardMod are not implemented, which results in slightly larger modfiles. -v information is either too much information or too little or just the wrong information. -V information is debug-level but ideally this should contain useful information. Using a temp file with diff is (oddly enough) slower then running diff twice. Some options are not implemented that should be, such as verification of the modfile. =head1 LICENSE This is released under the Artistic License. See L. =head1 AUTHOR AK108 L Released by AK108 for use by the YaBB community L and BoardMod L =cut