#!/usr/bin/perl # /usr/local/bin/perl # # ccclog - Summarizes Carbon Copy Cloner's log file. # This will handle either the statistics file or the log file. # # Revision History: # 1.0 Initial version 2010 # Parses the CCC log files. # 2.0 Stats version 2013 # Parses the CCC stats file, which is in plist form. # Added -calclen for magic field-width calculations. # 2.1 Added license info. 180616 # # # Copyright 2010 Wayne Morrison # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ####################################################################### use strict; use Getopt::Long qw(:config no_ignore_case_always); use Data::Dumper; # # Version information. # my $NAME = "ccclog"; my $VERS = "$NAME version: 2.1"; #------------------------------------------------------------------------ # # Stuff for command-line options. # my %opts = (); # Filled option array. my @opts = ( "clone", # Only give cloned volume recs. "dmg", # Only give disk image records. "src=s", # Specify source volume. "dest=s", # Specify destination volume. "last", # Only give last records. "cat", # Cat the log file. "calclen", # Calculate max field widths. "headers", # Headers output flag. "verbose", # Verbose output flag. "Version", # Version flag. "help", # Give help message. ); my $cat = 0; # Cat-the-file flag. my $last = 0; # Last entries only. my $clone = 0; # Cloned volume entries only. my $dmg = 0; # Disk-image entries only. my $headers = 0; # Give column headers. my $srcopt = ''; # Source volume. my $destopt = ''; # Destination volume. my $verbose = 0; # Verbose flag. #------------------------------ my $CAT = "/bin/cat -v"; my $LOGNODE = "CCC.log"; my $STATNODE = "CCC.stats"; my $LOGDIR = "/Library/Logs/"; my $logfile; my $SEP = "================================================================================"; my @tstmps = (); # Timestamps to output. my @srcfs = (); # Source volumes to output. my @dstfs = (); # Destination volumes to output. my @retcodes = (); # Dump return codes to output. my %sources = (); # Source volume hash. my %destinations = (); # Destination volume hash. #------------------------------------------ # Flag indicating if field lengths should be calculated or if "reasonable" # hard-coded values are acceptable. # my $calclen = 0; my $DATE_LENGTH = 19; my $SOURCE_LENGTH = 25; my $DEST_LENGTH = 15; #------------------------------------------ # Data used when parsing plist statistics. my %dumprecs = (); # Dump records. my @dates = (); # Dump dates (key to %dumprecs.) #------------------------------------------ # # This is used to find the new keys for various versions. # It is only used when looking for new key sets. # my @once = (1, 1, 1, 1, 1, 1); #------------------------------------------ my %keytypes = ( 'date' => 'date', 'elapsedTime', => 'real', 'dataCopied', => 'real', 'startTime', => 'date', 'targetDisk', => 'string', 'sourceDisk', => 'string', 'taskExitStatus', => 'integer', 'exitStatus', => 'integer', 'cccTaskName' => 'string', ); #------------------------------------------ main(); exit(0); #------------------------------------------------------------------------ # Routine: main() # sub main { my $newlog = 1; # Using a new logfile. $| = 1; # # Parse our command line. # opts(); # # Make sure we've got the current file. # $logfile = "$LOGDIR/$STATNODE"; if(! -f $logfile) { $logfile = "$LOGDIR/$LOGNODE"; $newlog = 0; } # print "main: logfile - <$logfile>\n"; # # Just cat the file if -cat was given. # catter($newlog) if($cat); # # Handle either a CCC stats file or log file. # if($newlog) { handlestats($logfile); } else { handlelog(); } } #------------------------------------------------------------------------ # Routine: opts() # sub opts { # # Get the options. # GetOptions(\%opts,@opts) || usage(); # # Look for -help or -Version. # usage() if(defined($opts{'help'})); version() if(defined($opts{'Version'})); # # Look for the source and destination options. If either # are found, we'll lop off any trailing slashes. # if(defined($opts{'src'})) { $srcopt = $opts{'src'}; $srcopt =~ s/\/+$//g; } if(defined($opts{'dest'})) { $destopt = $opts{'dest'}; $destopt =~ s/\/+$//g; } # # Dig out options from the command line. # $calclen = 1 if(defined($opts{'calclen'})); $cat = 1 if(defined($opts{'cat'})); $clone = 1 if(defined($opts{'clone'})); $dmg = 1 if(defined($opts{'dmg'})); $headers = 1 if(defined($opts{'headers'})); $last = 1 if(defined($opts{'last'})); $verbose = 1 if(defined($opts{'verbose'})); # # Save the headers we'll be using. # if($headers) { push @tstmps, 'Timestamp '; push @srcfs, 'Source'; push @dstfs, 'Destination'; } } #------------------------------------------------------------------------ # Routine: catter() # sub catter { my $newlog = shift; # New log flag. if($newlog) { system("plutil -convert xml1 -o - $logfile"); } else { system("$CAT $logfile"); } exit(0); } #=============================================================================== #=============================================================================== #=============================================================================== #------------------------------------------------------------------------ # Routine: handlelog() # # Purpose: Parse a CCC log file. # sub handlelog { my @runs = (); # # If neither -clone nor -dmg were given, we'll turn them both on. # if(!$clone && !$dmg) { $clone = 1; $dmg = 1; } # # Get the log file. # @runs = log_getdata(); # # Parse the log file. # foreach my $run (@runs) { log_parse($run); } # # Print the data. # log_printdata(); } #------------------------------------------------------------------------ # Routine: log_getdata() # sub log_getdata { my $data; my @runs = (); # # Open the log file. # open(CCCLOG, "<$logfile") || die("unable to open $logfile"); # # Get the data from the log file and divide it into an # array of CCC executions. # @runs = ; $data = join "", @runs; @runs = split /$SEP/, $data; # # Close the log file and return the list of runs. # close(CCCLOG); return(@runs); } #------------------------------------------------------------------------ # Routine: log_parse() # sub log_parse { my $run = shift; # Run data to process. my @lines = (); # Data lines in run. my $tstmp; # Timestamp of run. my $src; # Source volume of run. my $dst; # Destination volume of run. # # Divide the run into its separate line groups. # @lines = split /^$/m, $run; # # Look for various data in the run's lines. # foreach my $line (@lines) { # # Delete newlines from the beginning of lines. # $line =~ s/^\n//; # # Get the timestamp. # if($line =~ /Carbon Copy Cloner/) { $line =~ /Carbon Copy Cloner \(.+?\): (.+? .+? )/m; $tstmp = $1; } # # Get the source volume. # elsif($line =~ /Source/) { $line =~ /^Source: (.+?) \(.+\)/m; $src = $1; } # # Get the destination volume. # elsif($line =~ /Target/) { # $line =~ /^Target: (.+?) \(.+\)/m; # $dst = $1; $line =~ /^Target: (.+?) \((.+) \[.*\)/m; $dst = $2; } } # # Check for an empty run. # return if(($src eq '') && ($dst eq '') && ($tstmp eq '')); # # Check for user-specified volume restrictions. # return if(($srcopt ne '') && ($src ne $srcopt)); return if(($destopt ne '') && ($dst !~ /$destopt/)); # # Save the data for this CCC execution. # push @tstmps, $tstmp; push @srcfs, $src; push @dstfs, $dst; # # Add data to the source and destination hashes. # $sources{$src}++; $destinations{$dst}++; } #------------------------------------------------------------------------ # Routine: log_printdata() # sub log_printdata { my $srcwidth = 0; # # Find the width of the widest source volume. # We'll temporarily add the header, in case we might need it. # foreach my $src (keys(%sources), 'Source') { my $len; $len = length($src); $srcwidth = $len if($len > $srcwidth); } # # Print our data. # if($last) { # # We'll give the column headers, if the user wants 'em. # if($headers) { unshift @tstmps, 'Timestamp '; unshift @srcfs, 'Source'; unshift @dstfs, 'Destination'; log_printind(0,$srcwidth); } # # Do a backwards search through the data for each source # volume we've seen. Print just the first one we find. # foreach my $src (sort(keys(%sources))) { for(my $ind = @tstmps; $ind >= 0; $ind--) { if($srcfs[$ind] eq $src) { log_printind($ind,$srcwidth); last; } } } } else { # # Print all the entries we found. # for(my $ind = 0; $ind < @tstmps; $ind++) { log_printind($ind,$srcwidth); } } } #------------------------------------------------------------------------ # Routine: log_printind() # sub log_printind { my $ind = shift; # Index to print. my $width = shift; # Width of source volume field. my $dest = $dstfs[$ind]; # Destination. # # If verbose isn't turned on, we'll remove a leading "/Volumes/" # for destination volumes. We'll leave it alone for disk images. # if(! $verbose) { $dest =~ s/^\/Volumes\/// if($dest !~ /\.dmg\/$/); } # # Lop off the trailing slash from destinations. # $dest =~ s/\/$//g; # # Handle the -dmg and -clone flags. # if($dest =~ /\.dmg$/) { return if(!$dmg); } else { return if(!$clone); } printf "$tstmps[$ind]\t%*s\t$dest\n", -$width, $srcfs[$ind]; } #=============================================================================== #=============================================================================== #=============================================================================== #------------------------------------------------------------------------ # Routine: handlestats() # # Options: # doesn't work: # nicely formatted output # sub handlestats { my $file = shift; # CCC statistics file to parse. my @versions = (); # Count of entries in each version. my $unknowns = 0; # Count of entries w/ unknown version. my $plist; # Parsed plist. my @plist; # Array of plist entries. # # -dmg doesn't work with stats, just with log files. # if($dmg) { print STDERR "obsolete option: -dmg\n"; exit(3); } # # -clone is not necessary with current version. # if($clone) { print STDERR "unnecessary option: -clone\n"; } # # Get the contents of the plist stats file. We'll also parse # it so the dump records are nicely available. # stats_getdata($file); # # Print headers, if they're desired. # if($headers) { stats_prtit('Timestamp ','Source','Destination',0); } # # Print the entries. # if($last) { # # Build a hash of source volumes and the date of their # their final entries. # foreach my $datekey (@dates) { my $dumpref; # Reference to dump record. $dumpref = $dumprecs{$datekey}; next if($dumpref == 0); $sources{$dumpref->{'sourceDisk'}} = $datekey; } # # List the most recent dump record for each volume. # They'll be sorted by date.. # foreach my $datekey (sort(values(%sources))) { my $dumpref; # Reference to dump record. my $vind; # Version index. # # Get the dump record for this entry. # $dumpref = $dumprecs{$datekey}; next if($dumpref->{'sourceDisk'} eq ''); # # Print the entry's list. # $vind = stats_prtentry($datekey); $versions[$vind]++; $unknowns++ if($vind == 0); } } else { foreach my $datekey (@dates) { my $vind; # Version index. # print "\t$$datekey\t"; $vind = stats_prtentry($datekey); $versions[$vind]++; $unknowns++ if($vind == 0); } } # # G'head and write our collected output. # stats_printer(); # # Give a warning if there were any unrecognized entries. # if($unknowns) { print "\nentries with unknown format: $unknowns\n"; } # # Print list of entries in each version. # Only used when the plist format changes. # if(0) { for(my $vind=0; $vind < @versions; $vind++) { print "$vind:\t$versions[$vind]\n"; } } } #------------------------------------------------------------------------ # Routine: stats_getdata() # sub stats_getdata { my $file = shift; # CCC statistics file to parse. my @xmllines; # XML lines from plutil. my $ind; # Loop index. # # Read the XML data. # open(DF,"plutil -convert xml1 -o - $file | "); @xmllines = ; close(DF); if(@xmllines == 0) { print STDERR "empty dump log\n"; exit(1); } # # Strip off leading tabs and trailing newlines. # for($ind=0; $ind < @xmllines; $ind++) { $xmllines[$ind] =~ s/^\t*(.*)\n/$1/; } # # Go to the start of the real data. This is marked by # an "" line. # for($ind=0; $ind < @xmllines; $ind++) { if($xmllines[$ind] =~ //) { $ind++; last; } } # # Save all the dump records into their own hashes. # for(; $ind < @xmllines; $ind++) { my %dumphash = (); # Dump record's data. my $drkey = ''; # Dump record's key. # # Make sure we're at the start of the next dump record. # If we've hit the end of the input, we'll drop out. # if($xmllines[$ind] !~ '') { last if($xmllines[$ind] =~ /<\/(array|plist)>/); print "expecting \"\", got \"$xmllines[$ind]\"\n"; next; } $ind++; # # Dig out the pieces of this dump record and save them # to a hash. # while($xmllines[$ind] !~ /<\/dict>/) { my $str = $xmllines[$ind]; # Current string. my $key; # Key field. my $val; # Value field. # # Get the value of the "key" line. # $str = $xmllines[$ind]; $str =~ /(.*)<\/key>/; $key = $1; # # Get the value of the key's value. # $ind++; $str = $xmllines[$ind]; $str =~ /<.*?>(.*)<\/.*?>/; $val = $1; # # Save the dump's date as the %dumprecs key. # if(($key eq 'date') || ($key eq 'startTime')) { $val =~ tr/TZ/ /; } # # Save the dump's date as the %dumprecs key. # $drkey = $val if($key eq 'date'); # # Save the key/value pair. # $dumphash{$key} = $val; # # Bump up our line index and make sure we # haven't reached the end of the lines. # $ind++; last if($ind > @xmllines); } # # Save this dump record and its key. # $dumprecs{$drkey} = \%dumphash; push @dates, $drkey; } } #------------------------------------------------------------------------ # Routine: stats_prtentry() # sub stats_prtentry { my $datekey = shift; # Date key to print. my $dumpref; # Reference to dump record. my $vers; # Version of entry. # # Get the dump record to print. # $dumpref = $dumprecs{$datekey}; if($dumpref == 0) { print STDERR "datekey not found: \"$datekey\"\n"; return(0); } # # Figure out which version this dump record is. # $vers = stats_getversion($dumpref); if($vers == 1) { # print "$ind: version 1\n"; stats_prtvers1($dumpref); } elsif($vers == 2) { # print "$ind: version 2\n"; stats_prtvers2($dumpref); } elsif($vers == 3) { # print "$ind: version 3\n"; stats_prtvers3($dumpref); } return($vers); } #------------------------------------------------------------------------ # Routine: stats_getversion() # # Purpose: Determine the entry's version. This is done by looking # for keys specific to each version. # # The current version doesn't distinguish between 3a and 3b. # This is likely to become an issue if a real XML parser is used. # # These are the property-list keys for each version: # # version 1: # dataCopied # date # elapsedTime # exitStatus # sourceDisk # targetDisk # # version 2: # dataCopied # date # elapsedTime # sourceDisk # startTime # targetDisk # taskExitStatus # # version 3a: # cccTaskName # dataCopied # date # elapsedTime # sourceDisk # startTime # targetDisk # taskExitStatus # # version 3b: # cccTaskName # dataCopied # date # elapsedTime # sourceDisk # startTime # targetDisk # taskExitStatus # # These are the things we check for in each version: # # version 1: # exitStatus # # version 2: # startTime # taskExitStatus # no cccTaskName # # version 3a: # cccTaskName # startTime # taskExitStatus # dataCopied integer (two integers) # # version 3b: # cccTaskName # startTime # taskExitStatus # dataCopied real (two reals) # # plist entry starting index (in tewok's plist only): # v1: 0 # v2: 309 # v3: 506 sub stats_getversion { my $dumpref = shift; # Reference to dump record. my %dumprec; # Dump record. my $rc = 0; # Return code -- plist version. %dumprec = %$dumpref; # # Look for the first version's distinctive key. # if(defined($dumprec{'exitStatus'})) { $rc = 1; } # # Look for the second version's distinctive keys and # the lack of one from the third version. # if(defined($dumprec{'startTime'}) && defined($dumprec{'taskExitStatus'}) && ! defined($dumprec{'cccTaskName'})) { $rc = 2; } # # Look for the third version's distinctive keys. # if(defined($dumprec{'startTime'}) && defined($dumprec{'taskExitStatus'}) && defined($dumprec{'cccTaskName'})) { $rc = 3; } # # Return the version number to our caller. # return($rc); } #------------------------------------------------------------------------ # Routine: stats_prtvers1() # # Purpose: Print a version-1 dump record. # # Keys for a version 1 plist: # dataCopied integer # date date # elapsedTime real # exitStatus integer # sourceDisk string # targetDisk string # # Structure of plist fields: # date scalar # integer array # key array # real scalar # string array # sub stats_prtvers1 { my $dumpref = shift; # Reference to dump record. my %dumprec; # Dump record. %dumprec = %$dumpref; my $datalen; # Amount of data dumped. my $elapsed; # Time taken for dump. my $retcode; # Return code in plist. my $source; # Source volume. my $start; # Timestamp of dump start. my $target; # Target volume. # print "stats_prtvers1: down in\n"; # # Get the pieces of our dump record. # $datalen = $dumprec{'dataCopied'}; $elapsed = $dumprec{'elapsedTime'}; $retcode = $dumprec{'exitStatus'}; $source = $dumprec{'sourceDisk'}; $start = $dumprec{'date'}; $target = $dumprec{'targetDisk'}; stats_prtit($start,$source,$target,$retcode); } #------------------------------------------------------------------------ # Routine: stats_prtvers2() # # Purpose: Print a version-2 dump record. # # Keys for a version 2 plist: # dataCopied # date # elapsedTime # sourceDisk # startTime # targetDisk # taskExitStatus # # Structure of plist fields: # date array # integer array # key array # real scalar # string array # sub stats_prtvers2 { my $dumpref = shift; # Reference to dump record. my %dumprec; # Dump record. my $datalen; # Amount of data dumped. my $elapsed; # Time taken for dump. my $end; # Timestamp of dump end. my $retcode; # Return code in plist. my $source; # Source volume. my $start; # Timestamp of dump start. my $target; # Target volume. # print "stats_prtvers2: down in\n"; %dumprec = %$dumpref; # # Get the pieces of our dump record. # $datalen = $dumprec{'dataCopied'}; $elapsed = $dumprec{'elapsedTime'}; $end = $dumprec{'date'}; $retcode = $dumprec{'taskExitStatus'}; $source = $dumprec{'sourceDisk'}; $start = $dumprec{'startTime'}; $target = $dumprec{'targetDisk'}; stats_prtit($start,$source,$target,$retcode); } #------------------------------------------------------------------------ # Routine: stats_prtvers3() # # Purpose: Print a version-3 dump record. # # Keys for a version 3 plist: # cccTaskName string # dataCopied integer # date date # elapsedTime real # sourceDisk string # startTime date # targetDisk string # taskExitStatus integer # # Structure of plist fields: # date array # integer array # key array # real scalar # string array # sub stats_prtvers3 { my $dumpref = shift; # Reference to dump record. my %dumprec; # Dump record. my $datalen; # Amount of data dumped. my $elapsed; # Time taken for dump. my $end; # Timestamp of dump end. my $retcode; # Return code in plist. my $source; # Source volume. my $start; # Timestamp of dump start. my $target; # Target volume. my $taskname; # What was done. # print "stats_prtvers3: down in\n"; %dumprec = %$dumpref; # # Get the pieces of our dump record. # $datalen = $dumprec{'dataCopied'}; $elapsed = $dumprec{'elapsedTime'}; $retcode = $dumprec{'taskExitStatus'}; $source = $dumprec{'sourceDisk'}; $end = $dumprec{'date'}; $target = $dumprec{'targetDisk'}; $taskname = $dumprec{'cccTaskName'}; $start = $dumprec{'startTime'}; stats_prtit($start,$source,$target,$retcode); } #------------------------------------------------------------------------ # Routine: stats_prtit() # sub stats_prtit { my $start = shift; # Dump start time. my $source = shift; # Dump source. my $target = shift; # Dump destination. my $retcode = shift; # Success value of dump. my $rc; # Retcode to use. # # Check for user-specified volume restrictions. # return if(($srcopt ne '') && ($source ne $srcopt)); return if(($destopt ne '') && ($target !~ /$destopt/)); # # If verbose isn't turned on, we'll remove a leading "/Volumes/" # for destination volumes. We'll leave it alone for disk images. # if(! $verbose) { $target =~ s/^\/Volumes\/// if($target !~ /\.dmg\/$/); } # # Lop off the trailing slash from destinations. # $target =~ s/\/$//g; # # Figure out the return code to use. # $rc = ($retcode != 0) ? "ERROR! $retcode" : ' '; # # Save the data we'll be printing. # push @tstmps, $start; push @srcfs, $source; push @dstfs, $target; push @retcodes, $rc; } #------------------------------------------------------------------------ # Routine: stats_printer() # sub stats_printer { my $maxstd = 0; # Maximum start date length. my $maxsrc = 0; # Maximum source volume length. my $maxdst = 0; # Maximum destination volume length. # # Get the field lengths to use. If $calclen is nonzero, then # the lengths will be calculated. If it's zero, then "reasonable" # values for the lengths will be used. # if($calclen) { # # Find the maximum lengths of our output fields. # for(my $ind=0; $ind < @tstmps; $ind++) { my $tlen = length($tstmps[$ind]); # Date length. my $slen = length($srcfs[$ind]); # Source length. my $dlen = length($dstfs[$ind]); # Dest length. $maxstd = $tlen if($tlen > $maxstd); $maxsrc = $slen if($slen > $maxsrc); $maxdst = $dlen if($dlen > $maxdst); } } else { $maxstd = $DATE_LENGTH; $maxsrc = $SOURCE_LENGTH; $maxdst = $DEST_LENGTH; } for(my $ind=0; $ind < @tstmps; $ind++) { my $start; # Dump start time. my $source; # Dump source. my $target; # Dump destination. my $retcode; # Success value of dump. printf("%-*s\t%-*s\t%-*s %-s\n", $maxstd, $tstmps[$ind], $maxsrc, $srcfs[$ind], $maxdst, $dstfs[$ind], $retcodes[$ind] ); } } #=============================================================================== #=============================================================================== #=============================================================================== #------------------------------------------------------------------------ # Routine: version() # sub version { print STDERR "$VERS\n"; exit(0); } #------------------------------------------------------------------------ # Routine: usage() # sub usage { print STDERR "usage: ccclog [options]\n"; print STDERR "\toptions:\n"; print STDERR "\t\t-calclen calculate maximum field widths\n"; print STDERR "\t\t-cat print contents of log file\n"; print STDERR "\t\t-dest specify destination volume (regexp)\n"; print STDERR "\t\t-last only display most recent record\n"; print STDERR "\t\t-src specify source volume (name)\n"; # obsolete options: # print STDERR "\t\t-clone only display cloned volumes\n"; # print STDERR "\t\t-dmg only display disk images\n"; print STDERR "\n"; print STDERR "\t\t-headers gives column headers\n"; print STDERR "\t\t-verbose gives verbose output\n"; print STDERR "\t\t-Version gives command version\n"; print STDERR "\t\t-help displays this help message\n"; exit(0); } 1; ######################################################################### =pod =head1 NAME ccclog - Summarizes log entries kept by Carbon Copy Cloner =head1 SYNOPSIS ccclog [options] =head1 DESCRIPTION B (CCC) is an excellent backup/restore application for Mac OS X. It keeps a complete log of its actions, which is available for viewing with the B. Each log entry contains a large amount of information: timestamp of backup, source volume, destination location, size and type of volumes, hardware information, etc. With all this information, it can take a fair amount of digging in order to find the basic information of what was backed up when. B is a command-line tool that summarizes log entries from the B log file. The summarized entries are condensed down to the timestamp of backup, the source volume, and the destination location. Options allow for selection of specific source volumes, specific destinations, and the most recent backups. There is a little output processing that takes place if B<-verbose> is not given. A destination that I with "/Volumes/" will have that prefix removed, unless it is a disk image. Trailing slashes in destinations will be always removed. This output massaging is intended to convey additional information in a small space, shrink the length of output lines, and assist with scripting. If it is found to be more unhelpful than it is helpful, it may be removed in a future version. This is an example execution of B: 2010-05-14 19:09:01 root-10.5 extdisk-leopard 2010-05-14 18:15:43 root-10.6 extdisk-snowleopard 2010-05-14 21:01:23 root-10.6 /Volumes/dumps/100514-snow.dmg This is an example execution of B: 2010-05-14 19:09:01 root-10.5 /Volumes/extdisk-leopard 2010-05-14 18:15:43 root-10.6 /Volumes/extdisk-snowleopard 2010-05-14 21:01:23 root-10.6 /Volumes/dumps/100514-snow.dmg Normally, B will use hard-coded values for output field widths. These values are likely to be valid for many situations. At a slight overhead cost, maximum field lengths can be calculated and actual lengths can be used. The B<-calc> option can turn on maximum-field-length calculations on a per-run basis. Setting the I<$calclen> field to a non-zero value will do this on a permanent basis. The hard-coded values may also be adjusted as needed. Carbon Copy Cloner is available from L. =head1 ASSUMPTIONS For some of the output processing, B makes the following assumptions: =over 4 =item * Only disk images will have the B<.dmg> suffix. =back =head1 OPTIONS The following options are handled by B: =over 4 =item B<-calclen> Calculate the maximum field widths for the output, rather than using the hard-coded values. =item B<-cat> Just print the full contents of the log file as stored on disk. This is essentially a command-line version of selecting the Carbon Copy Cloner log from within the B. =item B<-clone> Only displays records for the destinations which are not B<.dmg> files (disk images.) This option is obsolete. This is the default functionality for the current version of B. =item B<-dmg> Only displays records for the destinations which are B<.dmg> files (disk images.) This option is obsolete. It does not with the current version of B. =item B<-dest dst-regexp> Displays records for the specified destination volume. The specified destination is a regular expression, and not necessarily a fixed string. =item B<-src src-name> Displays records for the specified source volume. The specified source is a fixed string and not a regular expression. =item B<-last> Displays the last record for each source volume. If the I<-src> or I<-dest> option was given, then the last record for the specified source and/or destination volume will be given. The records are sorted by source volume. =item B<-headers> Each data column will have a descriptive header. =item B<-verbose> Verbose output will be given. =item B<-Version> The command version will be printed and B will exit. =item B<-help> Displays a usage message. =back =head1 FILES /Library/Logs/CCC.log Carbon Copy Cloner's log file =head1 LICENSE Copyright 2010 Wayne Morrison Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. =head1 AUTHOR Wayne Morrison, wayne@waynemorrison.com =cut