From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#!perl
use strict;
use List::MoreUtils qw/ any /;
use 5.008008;
our $VERSION=$App::watcher::VERSION;
my @dir;
my @exclude_dir;
# process does not die when received SIGTERM, on win32.
my $signal=$^O eq 'MSWin32' ? 'KILL' : 'TERM';
GetOptions(
'dir=s@' => \@dir,
'exclude=s@' => \@exclude_dir,
'signal=s' => \$signal,
'send_only' => \my $send_only,
'h|help' => \my $help,
'v|version' => \my $version,
'filter=s@' => \my @filters,
) or pod2usage;
$version and do { print "watcher: $VERSION\n"; exit 0 };
pod2usage(1) if $help;
pod2usage(1) unless @ARGV;
@dir = ('.') unless @dir;
$_ = qr/$_/ for @filters;
# default filter
push @filters, qr!^\.[^\.]|[/\\][\._][^\.]|\.bak$|~$|_flymake\.(?:p[lm]|t)!
unless @filters;
if (@exclude_dir) {
@exclude_dir = map { File::Spec->abs2rel($_) } @exclude_dir;
}
sub info {
my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
localtime(time);
my $time = sprintf(
"%04d-%02d-%02dT%02d:%02d:%02d",
$year + 1900,
$mon + 1, $mday, $hour, $min, $sec
);
print "[$time] ", join(' ', @_), "\n";
}
my $pid;
sub fork_and_start {
undef $pid;
$pid = fork;
die "Can't fork: $!" unless defined $pid;
if ( $pid == 0 ) { # child
$SIG{INT} = $SIG{HUP} = $SIG{TERM} = 'DEFAULT';
exec @ARGV;
die "Cannot exec: @ARGV";
} else {
info("Forked process: @ARGV");
}
}
sub kill_pid {
$pid or return;
info("Killing the existing process by $signal (pid:$pid)");
kill $signal => $pid;
waitpid( $pid, 0 );
}
sub send_signal {
info("Sending $signal to the existing process (pid:$pid)");
kill $signal => $pid;
}
info("watching: @dir");
fork_and_start();
exit(0) unless $pid;
for my $sig (qw(TERM HUP INT)) {
$SIG{$sig} = sub {
info("SIG$sig received");
finalize();
};
}
my $watcher = Filesys::Notify::Simple->new(\@dir);
while (1) {
my @restart;
$watcher->wait(sub {
my @events = @_;
@events = grep { valid_file($_) } map { $_->{path} } @events;
@restart = @events;
});
next unless @restart;
info("-- $_") for @restart;
if ($send_only) {
send_signal();
} else {
kill_pid();
info("Successfully killed! Restarting the new process.");
fork_and_start();
unless ($pid) {
exit(0);
}
}
}
sub finalize {
my $self = shift;
if ($pid) {
info("Terminate process: $pid");
kill 'TERM' => $pid;
waitpid( $pid, 0 );
}
exit 0;
}
sub valid_file {
my ($file) = @_;
my $rel = File::Spec->abs2rel($file);
# default filter
return if any { $rel =~ $_ } @filters;
# exclude path filter
return if any { index($rel, $_) == 0 } @exclude_dir;
return 1;
}
__END__
=encoding utf8
=head1 NAME
watcher - watch the file updates
=head1 SYNOPSIS
% watcher --dir . -- osascript -e 'tell application "Google Chrome" to reload active tab of window 1'
--dir=. Directory to watch.
--exclude Directory to ignore.
--filter Regex of files to ignore
--signal=HUP Sending signal to restart(Default: TERM)(EXPERIMENTAL)
--send_only Sending signal without fork/exec(EXPERIMENTAL)
-h --help show this help
=head1 DESCRIPTION
This command watches the directory updates, and run the commands.
If no filter is provided via the C<--filter> option, a default
filter will be used. This default filter ignores files and
directories prefixed with a dot, F<.bak> files, and files
ending with a F<~>.
=head1 Sending SIGHUP without restart process
(EXPERIMENTAL)
watcher can send SIGHUP without process restarting.
% watcher --signal=HUP --send_only -- ...
=head1 AUTHOR
Tokuhiro Matsuno E<lt>tokuhirom AAJKLFJEF@ GMAIL COME<gt>
=head1 SEE ALSO
L<Filesys::Notify::Simple>
=head1 LICENSE
Copyright (C) Tokuhiro Matsuno
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.