File:  [LON-CAPA] / loncom / ConfigFileEdit.pm
Revision 1.2: download - view: text, annotated - select for diffs
Tue Dec 2 12:06:25 2003 UTC (20 years, 5 months ago) by foxr
Branches: MAIN
CVS tags: version_1_1_X, version_1_1_3, version_1_1_2, version_1_1_1, version_1_1_0, version_1_0_99_3, version_1_0_99_2, version_1_0_99_1, HEAD
Complete debugging of all functions and documentation.

    1: #
    2: #
    3: #
    4: # Copyright Michigan State University Board of Trustees
    5: #
    6: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
    7: #
    8: # LON-CAPA is free software; you can redistribute it and/or modify
    9: # it under the terms of the GNU General Public License as published by
   10: # the Free Software Foundation; either version 2 of the License, or
   11: # (at your option) any later version.
   12: #
   13: # LON-CAPA is distributed in the hope that it will be useful,
   14: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   15: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   16: # GNU General Public License for more details.
   17: #
   18: # You should have received a copy of the GNU General Public License
   19: # along with LON-CAPA; if not, write to the Free Software
   20: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   21: #
   22: # /home/httpd/html/adm/gpl.txt
   23: #
   24: # http://www.lon-capa.org/
   25: #
   26: 
   27: package ConfigFileEdit;
   28: 
   29: use IO::File;
   30: 
   31: #
   32: #   Module to read/edit configuration files.
   33: #   See the POD at the bottom of the file for more information.
   34: 
   35: #------------------------------ internal utility functions ----------
   36: 
   37: # 
   38: # Comment 
   39: #   Returns true if the line is completely a comment.
   40: # Paramter:
   41: #    line  
   42: #        Contents of a configuration file line.
   43: #
   44: sub Comment {
   45:     my $line = shift;
   46: 
   47:     # Leading whitespace followed by a #..
   48: 
   49:     if ($line =~ /^[' ',\t]*\#/) {
   50: 	return 1;
   51:     }
   52:     # Solely whitespace or empty  line.
   53: 
   54:     $line =~ s/[' ',\t]//g;
   55:     return ($line eq "");
   56: 
   57: }
   58: 
   59: #
   60: #  Field
   61: #    Return the value of a field in the line.  Leading whitespace is trimmed
   62: #    from the first key (key 0).
   63: #  Parameters:
   64: #     line 
   65: #        Line from which to extract the field.
   66: #
   67: #     idx
   68: #        Index of the field to extract.
   69: #
   70: sub Field {
   71:     my $line = shift;
   72:     my $idx  = shift;
   73: 
   74:     $line =~ s/(^ *)|(^\t*)//;
   75: 
   76:     my @fields = split(/:/, $line);
   77: 
   78:     return $fields[$idx];
   79: }
   80: #
   81: #   Index:
   82: #      Return a reference to a hash that indexes a line array.
   83: #      The hash is keyed on a field in the line array lines
   84: #      Each hash entry is the line number of the line in which 
   85: #      that key value appears.  Note that at present, keys must be
   86: #      unique.
   87: #  Parameters:
   88: #      $array    - Reference to a line array.
   89: #      $idxfield - Field number to index on (0 is the first field).
   90: #  Returns:
   91: #    Reference to the index hash:
   92: sub Index {
   93:     my $array     = shift;
   94:     my $idxfield  = shift;
   95:    
   96:     my %hash;
   97:     for(my $l = 0; $l < scalar(@$array); $l++) {
   98: 	chomp $array->[$l];	# Ensure lines have no \n's.
   99: 	my $line = $array->[$l];
  100: 	if(!Comment($line)) {
  101: 	    my $keyvalue = Field($line, $idxfield);
  102: 	    $hash{$keyvalue} = $l;
  103: 	}
  104:     }
  105: 
  106: 
  107:     return \%hash;
  108: }
  109: 
  110: 
  111: #------------------------------- public functions --------------------
  112: #
  113: #   new
  114: #     Create a new configuration file editor object.
  115: #     configuration files are : separated fields that 
  116: #     may have comments, blank lines and trailing comments.
  117: #     comments are indicated by #"s.
  118: #   Parameters:
  119: #     filename 
  120: #            Name of file to open.
  121: #     indexfield
  122: #            Select the field to index the file by.
  123: #
  124: # 
  125: sub new {
  126:     my $class      = shift;
  127:     my $filename   = shift;
  128:     my $indexfield = shift;
  129: 
  130:     # Open the configuration file.  Failure results in the return
  131:     # of an undef.
  132:     # Note we dont' need to hold on to the file handle after the file
  133:     # is read in.
  134: 
  135:     open(CONFIGFILE, "< $filename") 
  136: 	or return undef;
  137: 
  138: 
  139:     #   Read the file into a line array:
  140: 
  141:     my @linearray = <CONFIGFILE>;
  142:     close(CONFIGFILE);
  143:     
  144:     
  145:     #  Build the key to lines hash: this hash
  146:     #  is keyed on item $indexfield of the line
  147:     #  and contains the line number of the actual line.
  148: 
  149:     my $hashref = Index(\@linearray, $indexfield);
  150: 
  151: 
  152:     #   Build the object hash, bless it and return.
  153: 
  154:     my $self       = { Filename   => $filename,
  155: 		       Indexfield => $indexfield,
  156: 		       LineArray  => \@linearray,
  157: 		       KeyToLines => $hashref};
  158: 
  159:     bless ($self, $class);
  160: 
  161:     return $self;
  162:     
  163: }
  164: #
  165: #   Append an element to the configuration file array.
  166: #   The element is placed at the end of the array. If the element is not
  167: #   a comment. The key is added to the index.
  168: #
  169: #   Parameters:
  170: #      $self     - Reference to our member hash.
  171: #      $line     - A line to add to the config file.
  172: sub Append { 
  173:     my $self    = shift;
  174:     my $line    = shift;
  175: 
  176:     #   Regardless, the line is added to the config file.
  177: 
  178:     my $linearray = ($self->{LineArray});
  179:     push(@$linearray, $line);	                     # Append the line.
  180:     my $newindex = @$linearray - 1;                  # Index of new line.
  181: 
  182:     #   If the line is not a comment, pull out the desired field and add
  183:     #   it to the line index hash.
  184: 
  185:     if(!Comment($line)) {
  186: 	my $field = Field($line, $self->{Indexfield});
  187: 	$self->{KeyToLines}->{$field} = $newindex;
  188:     }
  189: }
  190: #
  191: #   Find a non comment line by looking it up by key.  
  192: #  Parameters:
  193: #     $self  - Reference to our member hash.
  194: #     $key   - Lookup key.
  195: #  Returns:
  196: #     Contents of the line or undef if there is no match.
  197: #
  198: sub Find {
  199:     my $self    = shift;
  200:     my $key     = shift;
  201: 
  202:     my $hash    = $self->{KeyToLines};
  203:     if(defined($hash->{$key})) {
  204: 	my $lines   = $self->{LineArray};
  205: 	return $lines->[$hash->{$key}];
  206:     } else {
  207: 	return undef;
  208:     }
  209: }
  210: #
  211: #   Return the number of lines in the current configuration file.
  212: #   Note that this count includes the comment lines.  To
  213: #   Get the non comment lines the best thing is to iterate through the
  214: #   keys of the KeyToLines hash.
  215: #  Parameters:
  216: #    $self     - Reference to member data hash for the object.
  217: #
  218: sub LineCount {
  219:     my $self  = shift;
  220:     my $lines = $self->{LineArray};
  221:     my $count = @$lines;
  222:     return $count;
  223: }
  224: #
  225: #   Delete a line from the configuration file.
  226: #   Note at present, there is no support for deleting comment lines.
  227: #   The line is deleted, from the array.  All lines following are slid back
  228: #   one index and the index hash is rebuilt.
  229: # Parameters:
  230: #   $self     - Reference to the member data hash for the object.
  231: #   $key      - key value of the line to delete.
  232: # NOTE:
  233: #   If a line matching this key does not exist, this is a no-op.
  234: #
  235: sub DeleteLine {
  236:     my $self     = shift;
  237:     my $key      = shift;
  238: 
  239:     my $lines    = $self->{LineArray};
  240:     my $index    = $self->{KeyToLines};
  241:     my $lastidx  = $self->LineCount() - 1;   # Index of last item.
  242: 
  243: 
  244:     my @temp;
  245: 
  246:     if(! defined($index->{$key})) {           # bail if no match.
  247: 	return;
  248:     }
  249:     my $itemno   = $index->{$key}; # Index of item to delete.
  250: 
  251:     
  252:     if ($itemno != $lastidx) {               # need to slide and reindex.
  253: 	@temp = @$lines[0..($itemno-1)];
  254: 	@temp[$itemno..($lastidx-1)] = @$lines[($itemno+1)..$lastidx];
  255: 	    
  256: 
  257:     } else {			             # just need to truncate
  258: 	@temp = @$lines[0..$lastidx-1];	     # So take the initial slice.
  259:     }
  260: 
  261:     $self->{KeyToLines} = Index(\@temp, $self->{Indexfield});
  262:     $self->{LineArray} = \@temp;             # Replace the lines index. 
  263: 
  264: 
  265: }
  266: #
  267: #   Replace a line in the configuration file:
  268: #   The line is looked up by index.
  269: #   The line is replaced by the one passed in... note if the line
  270: #   is a comment, the index is just deleted!!
  271: #   The index for the line is replaced with the new value of the key field
  272: #  (it's possible the key field changed).
  273: # 
  274: #  Parameters:
  275: #     $self          - Reference to the object's member data hash.
  276: #     $key           - Lookup key.
  277: #     $line          - New line.
  278: # NOTE:
  279: #  If there is no line with the key $key, this reduces to an append.
  280: #
  281: sub ReplaceLine {
  282:     my $self       = shift;
  283:     my $key        = shift;
  284:     my $line       = shift;
  285: 
  286:     my $hashref  = $self->{KeyToLines};
  287:     if(!defined $hashref->{$key}) {
  288: 	$self->Append($line); 
  289:     } else {
  290: 	my $l     = $hashref->{$key};
  291: 	my $lines = $self->{LineArray};
  292: 	$lines->[$l] = $line;	          # Replace old line.
  293: 	delete $hashref->{$key};          # get rid of the old index.
  294: 	if(!Comment($line)) {	          # Index this line only if not comment!
  295: 	    my $newkey = Field($line, $self->{Indexfield});
  296: 	    $hashref->{$newkey} = $l;
  297: 	}
  298:     }
  299: }
  300: #
  301: #   Write the configuration array to a file:
  302: #   Parameters:
  303: #      $self           - Reference to the object's member data hash.
  304: #      $fh              - Name of file to write.
  305: sub Write {
  306:     my $self     = shift;
  307:     my $fh       = shift;
  308:     
  309:     my $lines    = $self->{LineArray};
  310:     my $length   = @$lines;
  311:     for (my $i = 0; $i < $length; $i++) {
  312: 	print $fh $lines->[$i]."\n";
  313:     }   
  314: }
  315: 
  316: 1;
  317: #----------------------------- Documentation --------------------------------------
  318: #
  319: 
  320: =pod
  321: 
  322: =head1 NAME
  323: 
  324: ConfigFileEdit - Lookups and edits on a configuration file.
  325: 
  326: =head1 SYNOPSIS
  327: 
  328:     use LONCAPA::ConfigFileEdit;
  329:     use IO::File;
  330: 
  331:     my $editor = ConfigFileEdit->new("file.cfg", 0);
  332:     $editor->Append("new:line:with:config:info");      
  333:     my $line   = $editor->Find("key");
  334:     my $size   = $editor->LineCount();
  335:     $editor->DeleteLine("george");
  336:     $editor->ReplaceLine("new","new:line:with:different:info");  
  337:     my $fh = new IO::File("> modified.cfg", 0);
  338:     $editor->Write($fh);
  339: 
  340: =head1 DESCRIPTION
  341: 
  342: Configuration files in LonCAPA contain lines of colon separated fields.
  343: Configuration files can also contain comments initiated by the hash (#)
  344: character that continue to the end of line.  Finally, configuration files
  345: can be made more readable by the inclusion of either empty lines or 
  346: lines entirely made up of whitespace.
  347: 
  348: ConfigFileEdit allows manipulation of configuration files in a way that
  349: preserves comments and order.  This differs from LonCAPA's 'normal' mechanism
  350: handling configuration files by throwing them up into a hash by key.
  351: 
  352: ConfigFileEdit maintains the original configuration file in an array and
  353: creates the index hash as a hash to line numbers in the array.  This allows
  354: O(k) time lookups, while preserving file order and comments (comments are
  355: lines in the array that have no indices in the associated hash.
  356: 
  357: In addition to line lookup, ConfigFileEdit supports simple editing
  358: functions such as delete, append, and replace.  At present, Insertions
  359: at arbitrary points in the file are not supported.  The modified
  360: file can also be written out.
  361: 
  362: =head1 METHODS
  363: 
  364: =head2 new ( filename, keyfield )
  365: 
  366: Creates  a new ConfigFileEdit object from an existing file.   Where:
  367: 
  368: =over 4
  369: 
  370: =item * filename - The name of the configuration file to open.
  371: 
  372: =item * keyfield - The number of the field for which the index hash is generated.
  373:     Fields are enumerated from zero.
  374: 
  375: =item * RETURNS: - undef if the file could not be open, otherwise a reference
  376:     to a hash that contains the object member data.
  377: 
  378: =back
  379: 
  380: =head2 Append ( newline )
  381: 
  382: Appends a new line to the configuration file in memory.  The file that was
  383: used to create the object is not modified by this operation.
  384: 
  385: =over 4
  386: 
  387: =item * newline - A new line to append to the end of the configurationfile.
  388: 
  389: =back
  390: 
  391: =head2 LineCount 
  392: 
  393: Returns the number of lines in the file.  This count includes the nubmer of
  394: comments.
  395: 
  396: =head2 DeleteLine ( key )
  397: 
  398: Deletes the line that matches key.  Note that if there is no matching line,
  399: this function is a no-op.
  400: 
  401: =over 4
  402: 
  403: =item * key   - The key to match, the line that is indexed by this key is deleted.
  404: 
  405: =back
  406: 
  407: =head2 ReplaceLine ( key, newcontents  )
  408: 
  409: Replaces the selected line in its entirety.  Note that the config file is re-indexed
  410: so it is legal to modify the line's key field.
  411: 
  412: =over 4
  413: 
  414: =item * key    - The key that selects which line is replaced.
  415: 
  416: =item * newcontents - The new contents of the line.
  417: 
  418: =back
  419: 
  420: =head2 Write ( fh )
  421: 
  422: Writes the contents of the configuration file's line array to file.
  423: 
  424: =over 4
  425: 
  426: =item * fh   - A file handle that is open for write on the destination file.
  427: 
  428: =back
  429: 
  430: =head2 Comment ( line )
  431: 
  432: Static member that returns true if the line passed in is a comment or blank line.
  433: 
  434: =head2 Field ( line, index )
  435: 
  436: Static member that returns the value of a particular field on a config file line.
  437: 
  438: =over 4
  439: 
  440: =item * line   - The line that's parsed.
  441: 
  442: =item * index  - The index requested (0 is the first field on the line).
  443: 
  444: =back
  445: 
  446: =head2 Index ( linearray, fieldno )
  447: 
  448: Returns a reference to a hash that indexes a line array by a particular field number.
  449: this can be used to produce secondary indices if required by the application (see
  450: MEMBER DATA below).
  451: 
  452: =over 4
  453: 
  454: =item * linearray - A reference to an array containing text in configuration file format.
  455: =item * fieldno - Number of the field to index (0 is the first field).
  456: 
  457: =item * RETURNS - A reference to a hash from field value to line array element number.
  458: 
  459: =back
  460: 
  461: =head1 MEMBER DATA
  462: 
  463: The following member data can be considered exported.
  464: 
  465: =head2 LineArray
  466: 
  467: The reference to the configuration files line array.
  468: 
  469: =cut
  470: 
  471: 
  472: __END__

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>