#!/usr/bin/perl # watchfile: # This script prints info about changes to the file(s) specified # as command-line arguments. # Options: # By default, the script checks on the file(s) every 10 seconds. # If you want to specify a different time interval, use the "-interval" option. # By default, the script does not report on access-time changes. # If you want to be notified of access-time changes, use the "-atime" option # If you want the script to calculate and compare an MD5 digest for each file, # use the "-md5" option. This requires the Perl module Digest::MD5 # which is not installed by default. Note that access-time changes will not # be reported if the "-md5" option is used since the file is accessed # in order to compute the MD5 digest. # By default, the script does not report on resource-fork changes. # If you want to be notified of resource-fork changes, use the "-rsrc" option. # If you want the script to make a beep when there is a change, # use the "-beep" option. # Copyright 2006, Cameron Hayne. All Rights Reserved. # This program is free software. You may copy or # redistribute it under the same terms as Perl itself. # # Created: September 2003 Cameron Hayne (macdev@hayne.net) # Modified: February 2006 to support resource forks use strict; use warnings; use File::stat; use Fcntl; # function declarations sub usage_error(); sub handle_command_line_options(); sub print_msg($$$); sub get_file_info($); sub determine_changes($$); sub calc_md5($); # global variables my $interval = 10; # time (in seconds) between checks my $report_atime = 0; # whether to report on access time changes my $report_rsrc = 0; # whether to report on resource forks as well my $want_md5 = 0; # whether an md5 digest is to be computed for each file my $want_beep = 0; # whether to make a beep when reporting changes my @filenames; # names of files to be watched $| = 1; # enable autoflushing of output so this works better when piped handle_command_line_options(); MAIN: { my %fileinfo; # file info hashed by filename foreach my $filename (@filenames) { my $info = get_file_info($filename); $fileinfo{$filename} = $info; if ($info->{seen}) { print_msg($filename, $info, "Stored info"); } else { print_msg($filename, $info, "Non-existent"); } } print "------------------------------------------------\n"; while (1) { my $changes = 0; foreach my $filename (@filenames) { my $prev_info = $fileinfo{$filename}; my $info = get_file_info($filename); my $mods = determine_changes($prev_info, $info); if ($mods) { print_msg($filename, $info, "Changed: $mods"); $changes += 1; } $fileinfo{$filename} = $info; } if ($want_beep && $changes > 0) { print "\a"; # make a beep } sleep $interval; } } #----- FUNCTIONS ----- sub usage_error() { my $options = "[-interval n] [-atime] [-md5] [-rsrc] [-beep]"; print STDERR "usage: watchfile $options filename(s)\n"; exit(1); } sub handle_command_line_options() { use Getopt::Long; GetOptions( 'interval=i' => \$interval, 'atime' => \$report_atime, 'md5' => \$want_md5, 'rsrc' => \$report_rsrc, 'beep' => \$want_beep, ) or usage_error(); usage_error() unless scalar(@ARGV) >= 1; # must give a filename @filenames = @ARGV; # check validity of option values die "interval must be positive\n" if $interval <= 0; if ($want_md5) { # check that the MD5 module is installed unless (eval "require Digest::MD5") { warn "Couldn't load Digest::MD5 - md5 digests will not be used\n"; $want_md5 = 0; } # Calculating the MD5 digest accesses the file # so there is no point in reporting on atime $report_atime = 0 if $want_md5; # doing MD5 of directories is not supported foreach my $file (@filenames) { if (-d $file) { warn "MD5 of directories is not supported\n"; last; # give the warning only once } } } } sub print_msg($$$) { my ($filename, $info, $msg) = @_; my $date = localtime(time()); print "$date: $filename: $msg\n"; if ($info->{seen}) # file exists { system("/bin/ls -ld \"$filename\""); if ($report_rsrc && $info->{rsrc}->size > 0) { my $rsrc_filename = "$filename/..namedfork/rsrc"; system("/bin/ls -ld \"$rsrc_filename\""); } } } sub get_file_info($) { my ($filename) = @_; my $info; if (-e $filename) # check that the file exists { $info->{seen} = 1; $info->{data} = stat($filename) or die "Can't stat file $filename: $!"; if ($want_md5) { $info->{data_md5} = calc_md5($filename); } if ($report_rsrc) { my $rsrc_filename = "$filename/..namedfork/rsrc"; $info->{rsrc} = stat($rsrc_filename) or die "Can't stat file $rsrc_filename: $!"; if ($want_md5) { $info->{rsrc_md5} = calc_md5($rsrc_filename); } } } else # file doesn't exist { $info->{seen} = 0; $info->{data} = undef; $info->{rsrc} = undef; } return $info; } # returns a string specifying what changed sub determine_changes($$) { my ($prev_info, $info) = @_; return 0 unless defined($prev_info); return 0 if ($prev_info->{seen} == 0 and $info->{seen} == 0); my $mods = ""; my $newly_created = ($prev_info->{seen} == 0 and $info->{seen} == 1); my $newly_deleted = ($prev_info->{seen} == 1 and $info->{seen} == 0); if ($newly_created) { $mods .= "file created"; } elsif ($newly_deleted) { $mods .= "file deleted"; } my $prev_data = $prev_info->{data}; my $data = $info->{data}; if (defined($prev_data) and defined($data)) { # see 'man stat' for the meaning of these fields $mods .= "ino " if ($data->ino != $prev_data->ino); $mods .= "mode " if ($data->mode != $prev_data->mode); $mods .= "nlink " if ($data->nlink != $prev_data->nlink); $mods .= "uid " if ($data->uid != $prev_data->uid); $mods .= "gid " if ($data->gid != $prev_data->gid); $mods .= "size " if ($data->size != $prev_data->size); if ($want_md5) { my $prev_data_md5 = $prev_info->{data_md5}; my $data_md5 = $info->{data_md5}; $mods .= "md5 " if ($data_md5 ne $prev_data_md5); } if ($report_atime) { $mods .= "atime " if ($data->atime != $prev_data->atime); } $mods .= "mtime " if ($data->mtime != $prev_data->mtime); $mods .= "ctime " if ($data->ctime != $prev_data->ctime); } my $prev_rsrc = $prev_info->{rsrc}; my $rsrc = $info->{rsrc}; if (defined($prev_rsrc) and defined($rsrc)) { $mods .= "rsrc-size " if ($rsrc->size != $prev_rsrc->size); if ($want_md5) { my $prev_rsrc_md5 = $prev_info->{rsrc_md5}; my $rsrc_md5 = $info->{rsrc_md5}; $mods .= "rsrc-md5 " if ($rsrc_md5 ne $prev_rsrc_md5); } } return $mods; } sub calc_md5($) { my ($filename) = @_; if (-d $filename) { # doing MD5 on a directory is not supported return "unsupported"; # we need to return something } # We use 'sysopen' instead of just 'open' in order to be able to handle # filenames with leading whitespace or leading "-" # The usual trick to protect against leading whitespace or "-" is to do # $filename =~ s#^(\s)#./$1#; open(FILE, "< $filename\0") # but that fails if the filename is something like "- foo" # (i.e. if there is an initial "-" followed by whitespace) sysopen(FILE, $filename, O_RDONLY) or die "Unable to open file \"$filename\": $!\n"; binmode(FILE); # just in case we're on Windows! my $md5 = Digest::MD5->new->addfile(*FILE)->hexdigest; close(FILE); return $md5; }