root/juggler/trunk/release/scripts/mtree.pl

Revision 20974, 29.2 kB (checked in by patrick, 1 year ago)

Copyright update.

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1 #!/usr/bin/env perl
2
3 # ************** <auto-copyright.pl BEGIN do not edit this line> **************
4 #
5 # VR Juggler is (C) Copyright 1998-2008 by Iowa State University
6 #
7 # Original Authors:
8 #   Allen Bierbaum, Christopher Just,
9 #   Patrick Hartling, Kevin Meinert,
10 #   Carolina Cruz-Neira, Albert Baker
11 #
12 # This library is free software; you can redistribute it and/or
13 # modify it under the terms of the GNU Library General Public
14 # License as published by the Free Software Foundation; either
15 # version 2 of the License, or (at your option) any later version.
16 #
17 # This library is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20 # Library General Public License for more details.
21 #
22 # You should have received a copy of the GNU Library General Public
23 # License along with this library; if not, write to the
24 # Free Software Foundation, Inc., 59 Temple Place - Suite 330,
25 # Boston, MA 02111-1307, USA.
26 #
27 # *************** <auto-copyright.pl END do not edit this line> ***************
28
29 # -----------------------------------------------------------------------------
30 # Cross-platform mtree based on the FreeBSD 3.0 version of mtree(8) which is
31 # based on the 4.3 BSD-Reno version of the utility.
32 #
33 #     Patrick Hartling <patrick@vrac.iastate.edu>
34 #     October/November 1998
35 # -----------------------------------------------------------------------------
36 # This file contains a modified version of the wrap() function included with
37 # Perl 5 in the Text::Wrap module.  It was written by David Muir Sharnoff
38 # <muir@idiom.com> with help from Tim Pierce and others including Jacqui
39 # Caren.
40 # -----------------------------------------------------------------------------
41
42 require 5.003;
43
44 use Cwd;
45 use File::Path;
46 use Getopt::Std;
47 use Text::Tabs qw(expand unexpand);
48 use Time::localtime;
49
50 # Require that all global variables be defined here.  Otherwise, they must be
51 # lexically scoped (my) variables.
52 use strict 'vars';
53 use vars qw(%opts $name_printed);
54
55 require "hostname.pl";
56
57 # Subroutine prototypes.
58 sub nonfatalError($;$);
59 sub fatalError($;$);
60 sub getCurPath();
61 sub printDirStack();
62 sub top(@);
63 sub getStats($);
64 sub printTree(;$);
65 sub checkPrint(%%);
66 sub parseSpecFile();
67 sub processFile($$$$$$$$$$$$$$$);
68 sub checkKeyVal($$);
69 sub printName($);
70 sub getType($);
71 sub checkType($$);
72 sub wrap($$$@);
73
74 my $Win32 = 1 if $ENV{'OS'} =~ /Windows/;
75
76 # Parse command-line options.
77 getopts('cdeif:np:Uu', \%opts);
78
79 # Initialize variables scoped to this package.
80 my(%KEYWORDS) = ();
81 my(%SET_DEFAULTS) = ();
82 my(@dirstack) = ();
83 my($space_count, $exit_status) = (0, 0);
84
85 # Passing -U enables -u.
86 #$opts{'u'} = 1 if $opts{'U'};
87
88 # If -c was given, print out the directory tree specification rooted at the
89 # current directory or at the directory given with -p if that argument was
90 # passed as well.
91 if ( $opts{'c'} )
92 {
93    if ( $opts{'p'} )
94    {
95       chdir("$opts{'p'}") or die "$0: $opts{'p'}: $!\n";
96    }
97
98    print "#           user: ", (getpwuid($<))[0], "\n";
99    print "#        machine: ", hostname(), "\n";
100    print "#           tree: ", cwd(), "\n";
101    print "#           date: ", ctime(), "\n";
102
103    printTree(".");
104 }
105 # Otherwise, parse the specification file and compare it against the tree
106 # rooted at the current directory or at the directory given with the -p option
107 # if that argument was passed.
108 else
109 {
110    parseSpecFile();
111 }
112
113 # $exit_status may have been modified if parseSpecFile() was run.
114 exit $exit_status;
115
116 # =========================================================================== #
117 # Subroutines follow                                                          #
118 # =========================================================================== #
119
120 # -----------------------------------------------------------------------------
121 # Print a non-fatal error message to STDERR.  This is similar to the
122 # functionality provided by warn().
123 #
124 # PRE: None.
125 # POST: An error is printed to STDERR that contains the script name, a path
126 #       if specified and an error message.
127 #
128 # ARGS: $msg  - The error message to print.
129 #       $path - The path to which the error corresponds.  Optional.
130 # -----------------------------------------------------------------------------
131 sub nonfatalError($;$)
132 {
133    my $msg = shift;
134    my $path = shift || "";
135
136    print STDERR "$0: ";
137    print STDERR "$path: " if $path;
138    print STDERR "$msg\n";
139 }
140
141 # -----------------------------------------------------------------------------
142 # Print a fatal error message to STDERR and exit with status 1.  This is
143 # similar to the functionality provided by die().
144 #
145 # PRE: None.
146 # POST: An error is printed to STDERR that contains the script name, a path
147 #       if specified and an error message.  The script exits after the error
148 #       is printed.
149 #
150 # ARGS: $msg  - The error message to print.
151 #       $path - The path to which the error corresponds.  Optional.
152 # -----------------------------------------------------------------------------
153 sub fatalError($;$)
154 {
155    my $msg = shift;
156    my $path = shift || "";
157
158    nonfatalError("$msg", "$path");
159    exit 1;
160 }
161
162 # -----------------------------------------------------------------------------
163 # Return the current path as a scalar string.
164 #
165 # PRE: None.
166 # POST: A scalar string is generated from @dirstack.
167 #
168 # RETURNS: A scalar string containing the current path in @dirstack.
169 # -----------------------------------------------------------------------------
170 sub getCurPath()
171 {
172    return join("/", @dirstack);
173 }
174
175 # -----------------------------------------------------------------------------
176 # Print the directory stack.  This will have a trailing "/" if @dirstack is
177 # not empty.
178 #
179 # PRE: None.
180 # POST: The current directory stack is printed to STDOUT.
181 # -----------------------------------------------------------------------------
182 sub printDirStack()
183 {
184    foreach ( @dirstack )
185    {
186       print "$_/";
187    }
188 }
189
190 # -----------------------------------------------------------------------------
191 # Return the top of the stack (array) passed.
192 #
193 # PRE: None.
194 # POST: The top element of the stack passed is returned.
195 #
196 # ARGS: One argument of type array.
197 #
198 # RETURNS: The top element of the stack passed if the stack is not empty.
199 #          undef is returned if the stack is empty.
200 # -----------------------------------------------------------------------------
201 sub top(@)
202 {
203    return ( $#_ >= 0 ) ? $_[$#_] : undef;
204 }
205
206 # -----------------------------------------------------------------------------
207 # Generate a hash containing all the information about the given file (or
208 # directory) and return it to the caller.  The information returned will have
209 # keys 'name', 'mode', 'nlink', 'uid', 'gid', 'size' and 'time'.
210 #
211 # PRE: The file named by the single argument must exist.
212 # POST: The stat() info for the named file or directory is read and stored in
213 #       a hash.
214 #
215 # ARGS: $filename - A scalar argument naming a file or directory.
216 #
217 # RETURNS: A hash containing the stat() information for the named file or
218 #          directory.
219 # -----------------------------------------------------------------------------
220 sub getStats($)
221 {
222    my $filename = shift;
223
224    my(%info) = ();
225    my(@fileinfo) = stat("$filename");
226
227    # This is a (terrible?) hack to get the file permissions unmangled.
228    $fileinfo[2] = sprintf("%o", $fileinfo[2]);
229    $fileinfo[2] =~ s/^\d+(\d{4})$/\1/;
230
231    $info{'name'}  = "$filename";
232    $info{'mode'}  = "$fileinfo[2]";
233    $info{'nlink'} = "$fileinfo[3]";
234    $info{'uid'}   = "$fileinfo[4]";
235    $info{'gid'}   = "$fileinfo[5]";
236    $info{'size'}  = "$fileinfo[7]";
237    $info{'time'}  = "$fileinfo[9]";
238    $info{'type'}  = getType("$filename");
239
240    return %info;
241 }
242
243 # -----------------------------------------------------------------------------
244 # Print the specification for the directory tree (and all its files) rooted
245 # at the named directory.
246 #
247 # PRE: None.
248 # POST: The mtree-readable tree specification is printed to STDOUT.
249 #
250 # ARGS: $cur_dir - The base directory of the tree to print.  If no name is
251 #                  passed, it defaults to the current directory.
252 # -----------------------------------------------------------------------------
253 sub printTree(;$)
254 {
255    my $cur_dir = shift || ".";
256
257    print "\n" unless $opts{'d'};
258
259    chdir("$cur_dir") or fatalError("$!", getCurPath());
260    push(@dirstack, "$cur_dir");
261
262    opendir(DIR, ".") or fatalError("$!", getCurPath());
263    my(@files) = readdir(DIR);
264    closedir(DIR);
265
266    my $file;
267    my(@contents, $i) = ((), 0);
268
269    # Get the stat() info for all the files/directories read.
270    foreach $file ( @files )
271    {
272       # If -d was specified, we skip everything that is not a directory.
273       # This saves a lot of time later.
274       next if $opts{'d'} && ! -d "$file";
275
276       my %file_info = getStats("$file");
277
278       # Store a reference to the hash in @contents.  This keeps everything
279       # ordered as it was read from the directory.
280       $contents[$i++] = \%file_info;
281
282       if ( "$file" eq "." )
283       {
284          # This is the starting directory if $cur_dir is '.'.
285          if ( "$cur_dir" eq "." )
286          {
287             %SET_DEFAULTS = %file_info;
288             delete($SET_DEFAULTS{'name'});
289             delete($SET_DEFAULTS{'type'});
290          }
291
292          foreach ( keys(%SET_DEFAULTS) )
293          {
294             if ( "$SET_DEFAULTS{$_}" ne "$file_info{$_}" )
295             {
296                $SET_DEFAULTS{$_} = "$file_info{$_}";
297             }
298          }
299       }
300    }
301
302    my(%cur_defaults) = ();
303    my(%max_hash) = ();
304    my(%counts) = ();
305    my($key, $value);
306
307    foreach ( @contents )
308    {
309       while ( ($key, $value) = each(%$_) )
310       {
311          next if "$key" eq "name" || "$key" eq "size" ||
312                  "$key" eq "time" || "$key" eq "type";
313
314          $counts{"$key"}{"$value"}++;
315       }
316    }
317
318    foreach $key ( keys(%counts) )
319    {
320       foreach $value ( keys(%{$counts{"$key"}}) ) {
321          if ( $counts{"$key"}{"$value"} > $max_hash{"$key"} )
322          {
323             $max_hash{"$key"} = $counts{"$key"}{"$value"};
324             $cur_defaults{"$key"} = $value;
325          }
326       }
327    }
328
329    my(@dirs) = ();
330
331    foreach $file ( @contents )
332    {
333       print "# ", getCurPath(), "\n" if "$$file{'name'}" eq "." && ! $opts{'n'};
334
335       if ( "$$file{'type'}" eq 'dir' && "$$file{'name'}" !~ /^\.\.?$/ )
336       {
337          push(@dirs, $file);
338       }
339       else
340       {
341          my $name = "$$file{'name'}";
342          next if "$name" eq "..";
343
344          # If this is the '.' directory, then we may need to print out a
345          # new batch of settings if anything is different between the
346          # base default values (%SET_DEFAULTS) and the defaults for this
347          # directory (%cur_defaults).
348          if ( "$name" eq "." && checkPrint(%SET_DEFAULTS, %cur_defaults) )
349          {
350             %SET_DEFAULTS = %cur_defaults;
351
352             # Only print the type as a directory if -d was given on the
353             # command line.  In this case, only directories will be in
354             # @contents, so this check is safe.
355             if ( $opts{'d'} )
356             {
357                print "/set type=dir ";
358             }
359             else
360             {
361                print "/set type=file ";
362             }
363
364             foreach ( keys(%SET_DEFAULTS) )
365             {
366                print "$_=$SET_DEFAULTS{$_} ";
367             }
368
369             print "\n";
370          }
371
372          my $output = "";
373
374          if ( "$$file{'type'}" eq 'dir' )
375          {
376             my $dirname = top(@dirstack);
377
378             if ( length($dirname) >= 16 )
379             {
380                print " " x $space_count, "$dirname \\\n";
381                $output = " " x (16 + $space_count);
382             }
383             else
384             {
385                $output .= " " x $space_count . "$dirname" .
386                           " " x (16 - length($dirname));
387             }
388          }
389          else
390          {
391             if ( length($name) >= 12 )
392             {
393                print " " x ($space_count + 4), "$name \\\n";
394                $output = " " x (16 + $space_count);
395             }
396             else
397             {
398                $output .= " " x ($space_count + 4) . "$name" .
399                           " " x (12 - length($name));
400             }
401          }
402
403          $output .= "type=$$file{'type'} "
404             unless "$$file{'type'}" eq 'file' || $opts{'d'};
405
406          $output .= "uid=$$file{'uid'} "
407             unless $cur_defaults{'uid'} eq $$file{'uid'};
408
409          $output .= "gid=$$file{'gid'} "
410             unless $cur_defaults{'gid'} eq $$file{'gid'};
411
412          $output .= "mode=$$file{'mode'} "
413             unless $cur_defaults{'mode'} eq $$file{'mode'};
414
415          $output .= "nlink=$$file{'nlink'} "
416             unless $cur_defaults{'nlink'} eq $$file{'nlink'};
417
418          # These two are always printed.
419          $output .= "size=$$file{'size'} time=$$file{'time'}";
420
421          if ( length("$output") >= 70 )
422          {
423             $output =~ s/^(\s*)(\S.+)$/\2/;
424             my(@lines) = wrap(70, "$1", " " x (16 + $space_count), "$output");
425
426             my $i;
427             for ( $i = 0; $i < $#lines - 1; $i++ )
428             {
429                print "$lines[$i] \\\n";
430             }
431
432             print "$lines[$#lines]\n";
433          }
434          else
435          {
436             print "$output\n";
437          }
438       }
439    }
440
441    foreach ( @dirs )
442    {
443       $space_count += 4 if $opts{'i'};
444       printTree("$$_{'name'}");
445
446       print " " x $space_count, "# ", getCurPath(), "\n" if ! $opts{'n'};
447       print " " x $space_count, "..\n";
448       print "\n" unless $opts{'d'};
449
450       chdir("..");
451       pop(@dirstack);
452       $space_count -= 4 if $opts{'i'};
453    }
454
455    # This is always the LAST thing printed (i.e., after every tree has been
456    # traversed).
457    print "..\n" if top(@dirstack) eq ".";
458 }
459
460 # -----------------------------------------------------------------------------
461 # Determine if the contents of the base hash differ in any way from those of
462 # the local hash.
463 #
464 # PRE: None.
465 # POST: The two hashes are compared, and a value appropriate for testing in
466 #       conditionals is returned.
467 #
468 # ARGS: %base  - A basic set of keys and values (which in this case has
469 #                global scope).
470 #       %local - A more tightly scoped set of keys and values (which in this
471 #                case is used to override what is in %base).
472 #
473 # RETURNS: 0 - There are no differences between the two hashes.
474 #          1 - At least one difference between the two hashes.
475 # -----------------------------------------------------------------------------
476 sub checkPrint(%%)
477 {
478    my (%base, %local) = @_;
479
480    my $diff_count = 0;
481
482    foreach ( keys(%base) )
483    {
484       return 1 if "$base{$_}" ne "$local{$_}";
485    }
486
487    foreach ( keys(%local) )
488    {
489       return 1 if "$base{$_}" ne "$local{$_}";
490    }
491
492    return 0;
493 }
494
495 # -----------------------------------------------------------------------------
496 # Read the specification file and compare it against the directory tree rooted
497 # at the current directory or at the tree rooted at the directory named by the
498 # -p argument if it was passed.
499 #
500 # PRE: None.
501 # POST: The specification file (read either from STDIN or from a file named
502 #       with the -f option) is read.  If -U and/or -u are given on the command
503 #       line, the directory tree is updated as necessary to match the
504 #       specification.  Otherwise, any differences between the specification
505 #       file and the tree are printed to STDOUT with no update.
506 # -----------------------------------------------------------------------------
507 sub parseSpecFile()
508 {
509    # If -f was given on the command line, read the specification file from
510    # the file named with that argument.
511    if ( $opts{'f'} )
512    {
513       open(DISTFILE, "$opts{'f'}") or fatalError("Cannot open $opts{'f'}: $!");
514    }
515    # Otherwise, read it from STDIN.
516    else
517    {
518       open(DISTFILE, "-");
519    }
520
521    # If -p was given on the command line, start the tree check from that
522    # directory (assuming that it exists).
523    if ( $opts{'p'} )
524    {
525       chdir("$opts{'p'}") or die "$0: $opts{'p'}: $!\n";
526    }
527
528    my $line;
529    while ( $line = <DISTFILE> )
530    {
531       $line =~ s/#.*$//;                # Strip out comments
532
533       next if $line =~ /^$/;            # Skip blank lines
534
535       # Join lines ending in '\'.
536       if ( $line =~ /^(.*)\\\s*$/ )
537       {
538          my $new_line = "$1";
539
540          while ( $line = <DISTFILE> )
541          {
542             if ( $line =~ /^(.*)\\\s*$/ )
543             {
544                $new_line .= "$1";
545             }
546             else
547             {
548                $new_line .= "$line";
549                last;
550             }
551          }
552
553          $line = "$new_line";
554       }
555
556       # '/set ...' line.
557       if ( $line =~ /^\/set\s+(\S.+)$/ )
558       {
559          foreach ( split(/\s+/, "$1") )
560          {
561             my($keyword, $value) = split(/=/, "$_");
562             checkKeyVal("$keyword", "$value");
563             $KEYWORDS{"$keyword"} = "$value";
564          }
565       }
566       # '/unset ...' line.
567       elsif ( $line =~ /^\/unset\s+(\S.+)$/ )
568       {
569          foreach ( split(/\s+/, "$1") )
570          {
571             delete($KEYWORDS{"$_"});
572          }
573       }
574       # '..' directory change line.
575       elsif ( $line =~ /^\s*\.\.\s*$/ )
576       {
577          pop(@dirstack);
578          chdir("..");
579       }
580       # Named file or directory (possibly with options) line.
581       elsif ( $line =~ /^\s*(\S+)\s*(\S.+)?$/ )
582       {
583          my $filename = "$1";
584
585          my(%TEMP) = ();
586
587          if ( $2 )
588          {
589             foreach ( split(/\s+/, "$2") )
590             {
591                my($keyword, $value) = split(/=/, "$_");
592                checkKeyVal("$keyword", "$value");
593                $TEMP{"$keyword"} = "$value";
594             }
595          }
596
597          processFile("$filename", "$TEMP{'ignore'}", "$TEMP{'nochange'}",
598                      "$TEMP{'cksum'}", "$TEMP{'gid'}", "$TEMP{'gname'}",
599                      "$TEMP{'md5digest'}", "$TEMP{'mode'}", "$TEMP{'nlink'}",
600                      "$TEMP{'uid'}", "$TEMP{'uname'}", "$TEMP{'size'}",
601                      "$TEMP{'link'}", "$TEMP{'time'}", "$TEMP{'type'}");
602       }
603    }
604
605    close(DISTFILE) or nonfatalError("Cannot close input file: $!");
606 }
607
608 # -----------------------------------------------------------------------------
609 # Process the named file by comparing its existence and various settings
610 # against what is expected to be found.
611 #
612 # PRE: The %KEYWORDS hash should have expected defaults set.
613 # POST: If -U and/or -u were given on the command line, the file is updated
614 #       to match its specification and the change is printed to STDOUT.
615 #       Otherwise, the information that would be changed is printed to STDOUT
616 #       with no update.
617 #
618 # ARGS: $name      - The name of the file to process.
619 #       $ignore    - Ignore any file heirarchy below this file.
620 #       $nochange  - Make sure this file exists but ignore all other
621 #                    attributes.
622 #       $cksum     - The checksum of the file using the default algorithm
623 #                    specified by the cksum(1) utility.
624 #       $gid       - The expected owner's group ID.
625 #       $gname     - The expected owner's group name.
626 #       $md5digest - The MD5 message digest of the file.
627 #       $mode      - The expected permission bits (in octal).
628 #       $nlink     - The expected number of hard links to the file.
629 #       $uid       - The expected owner's user ID.
630 #       $uname     - The expected owner's user name.
631 #       $size      - The expected size.
632 #       $link      - The file the link is expected to reference.
633 #       $time      - The expected modification time.
634 #       $type      - The expected file type.
635 # -----------------------------------------------------------------------------
636 sub processFile($$$$$$$$$$$$$$$)
637 {
638    my $name      = shift;
639    my $ignore    = $_[0] || $KEYWORDS{'ignore'};
640    my $nochange  = $_[1] || $KEYWORDS{'nochange'};
641    my $cksum     = $_[2] || $KEYWORDS{'cksum'};
642    my $gid       = $_[3] || $KEYWORDS{'gid'};
643    my $gname     = $_[4] || $KEYWORDS{'gname'};
644    my $md5digest = $_[5] || $KEYWORDS{'md5digest'};
645    my $mode      = $_[6] || $KEYWORDS{'mode'};
646    my $nlink     = $_[7] || $KEYWORDS{'nlink'};
647    my $uid       = $_[8] || $KEYWORDS{'uid'};
648    my $uname     = $_[9] || $KEYWORDS{'uname'};
649    my $size      = $_[10] || $KEYWORDS{'size'};
650    my $link      = $_[11] || $KEYWORDS{'link'};
651    my $time      = $_[12] || $KEYWORDS{'time'};
652    my $type      = $_[13] || $KEYWORDS{'type'};
653
654    if ( ! $Win32 )
655    {
656       $uname = getpwuid($<) if ! $uname;
657
658       if ( $uname && ! $uid )
659       {
660          my(@user_info) = getpwnam("$uname") or die "getpwnam($uname): $!\n";
661          $uid = $user_info[2];
662       }
663
664       if ( $gname && ! $gid )
665       {
666          my(@group_info) = getgrnam("$gname") or die "getgrnam($gname): $!\n";
667          $gid = $group_info[2];
668       }
669    }
670
671    SWITCH:
672    {
673       if ( "$type" eq 'dir' )
674       {
675          # XXX: This is kind of a hack to deal with Perl thinking that a
676          # symlink pointing to a directory is actually a directory.
677          if ( -l "$name" )
678          {
679             warn "WARNING: Symlink exists where directory expected (",
680                  printDirStack(), ") -- removing link\n";
681             unlink("$name");
682          }
683
684          if ( ! -d "$name" )
685          {
686             print "missing: ";
687             printDirStack();
688             print "$name";
689
690             $exit_status = 2 if $opts{'u'};
691
692             if ( $opts{'U'} || $opts{'u'} ) {
693                mkdir("$name", 0755) or die "ERROR: Cannot mkdir $name: $!\n";
694                chmod(oct($mode), "$name") if $mode;
695                chown($uid, $gid, "$name");
696                print " (created)\n";
697             }
698             else
699             {
700                print "\n";
701                return;
702             }
703          }
704
705          last SWITCH;
706       }
707
708       # If -d was given on the command line and execution reaches this
709       # point, the type of $name is not a directory, so it should be
710       # ignored.
711       return if $opts{'d'};
712
713       if ( "$type" eq 'file' )
714       {
715          if ( ! -f "$name" )
716          {
717             print "missing: ";
718             printDirStack();
719             print "$name ";
720          }
721
722          last SWITCH;
723       }
724
725       if ( "$type" eq 'link' )
726       {
727          if ( ! -l "$name" )
728          {
729             print "missing: ";
730             printDirStack();
731             print "$name ";
732 &