#!/usr/bin/env perl # TODO: # possibly create a man page # # listen: a simple configurable build system # created by Bryson Steck, @brysonsteck on GitHub # free and open source under the GPL Version 3 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Usage # listen [flags] [file(s) to watch] [command(s) to run on file changes] # # Flags # -a, --all if all of the files have changed, required if multiple files # -o, --any if any one of the files have changed, required if multiple files # -s to use checksum on file change instead of when the file was modified # -r, --run immediately run exec command, then start listening # --timeout=[time] change timeout time in whole seconds (DEFAULT 1 SECOND) # --help prints help # GLOBALS local $| = 1; my $VERSION = "0.1.0"; my $ARGC = scalar @ARGV; my $TIMEOUT = undef; my $START_LISTEN = 0; my $EXEC_POSITION = $ARGC - 1; my $EXEC_LISTEN = undef; my $ALL_FLAG = undef; my $ANY_FLAG = undef; my $MULTI_FILES = undef; my $CHECKSUM_FLAG = undef; my $RUN_FLAG = undef; my $black = "\033[0;90m"; my $nocolor = "\033[0m"; # comment the line below if you use Windows my $CHECKSUM_COMMAND = "/usr/bin/env cksum"; # uncomment the line below if you use Windows #my $CHECKSUM_COMMAND = "C:\Windows\System32\certutil.exe -hashfile"; # check for flags if any sub flags { # quit if no args if ($ARGC == 0) { print "listen: To what?\n"; exit 2; } my $current_arg = 0; foreach $arg (@ARGV) { # shorthand args if ($arg =~ m/^-[aosrhv]+$/) { if ($arg =~ m/a/) { $ALL_FLAG = "def"; } if ($arg =~ m/o/) { $ANY_FLAG = "def"; } if ($arg =~ m/s/) { $CHECKSUM_FLAG = "def"; } if ($arg =~ m/r/) { $RUN_FLAG = "def"; } if ($arg =~ m/h/) { print "\nlisten v$VERSION - a simple automation system\n"; print "Copyright 2022 Bryson Steck\n"; print "Free and open source under the GNU General Public License v3.\n"; print "Run 'listen --license' to view the license and warranty disclaimer.\n\n"; print "usage: listen [-v | --version] [-h | --help] [-a | --all]\n"; print " [-o | --any] [-s] [-r | --run] FILE1 [FILE2 ...]\n"; print " COMMAND\n\n"; print "informational flags:\n"; print " -h | --help -> Print this message\n"; print " -v | --version -> Print the version of listen\n"; print " --license -> Print the license/warranty disclaimer\n"; print " (GNU General Public License v3)\n\n"; print "multiple file flags:\n"; print " -a | --all -> Run COMMAND if ALL of the files have been modified\n"; print " -o | --any -> Run COMMAND if ANY of the files have been modified\n\n"; print "other flags:\n"; print " -r | --run -> Run COMMAND before starting listen\n"; print " -s -> Check for file modification based on cksum\n"; print " (as opposed to the files' modified timestamp)\n\n"; exit 1; } if ($arg =~ m/v/) { print "listen v$VERSION\n"; } # longhand args } elsif ($arg =~ m/--timeout=/) { my @timeout_split = split /\=/, $arg; if (scalar @timeout_split == 1) { print "timeout flag invalid\n"; exit 2; } elsif (int($timeout_split[1]) > 0) { $TIMEOUT = int($timeout_split[1]); print "timeout is now $TIMEOUT seconds\n"; } else { print "timeout flag invalid\n"; exit 2; } } elsif ($arg =~ m/--help/) { print "\nlisten v$VERSION - a simple automation system\n"; print "Copyright 2022 Bryson Steck\n"; print "Free and open source under the GNU General Public License v3.\n"; print "Run 'listen --license' to view the license and warranty disclaimer.\n\n"; print "usage: listen [-v | --version] [-h | --help] [-a | --all]\n"; print " [-o | --any] [-s] [-r | --run] FILE1 [FILE2 ...]\n"; print " COMMAND\n\n"; print "informational flags:\n"; print " -h | --help -> Print this message\n"; print " -v | --version -> Print the version of listen\n"; print " --license -> Print the license/warranty disclaimer\n"; print " (GNU General Public License v3)\n\n"; print "multiple file flags:\n"; print " -a | --all -> Run COMMAND if ALL of the files have been modified\n"; print " -o | --any -> Run COMMAND if ANY of the files have been modified\n\n"; print "other flags:\n"; print " -r | --run -> Run COMMAND before starting listen\n"; print " -s -> Check for file modification based on cksum\n"; print " (as opposed to the files' modified timestamp)\n\n"; exit 1; } elsif ($arg =~ m/--version/) { print "listen v$VERSION\n"; exit 1; } elsif ($arg =~ m/--license/) { print "listen is free and open source under the GNU GPL Version 3.0.\n\n"; print "This program is free software: you can redistribute it and/or modify\n"; print "it under the terms of the GNU General Public License as published by\n"; print "the Free Software Foundation, either version 3 of the License, or\n"; print "(at your option) any later version.\n\n"; print "This program is distributed in the hope that it will be useful,\n"; print "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"; print "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"; print "GNU General Public License for more details.\n\n"; print "You should have received a copy of the GNU General Public License\n"; print "along with this program. If not, see .\n"; exit 1; } elsif ($arg =~ m/--all/) { $ALL_FLAG = "def"; } elsif ($arg =~ m/--any/) { $ANY_FLAG = "def"; } elsif ($arg =~ m/--run/) { $RUN_FLAG = "def"; # at this point, either it is an unknown flag or the start of the filename(s) } elsif ($arg =~ m/^-[^aosrhv]+$/) { print "listen: Unknown flag: $arg\n"; exit 2; } elsif ($arg =~ m/^--[a-z]/) { print "listen: Unknown flag: $arg\n"; exit 2; } else { last; } $current_arg++; } # set index of the start of file(s) $START_LISTEN = $current_arg; } # get last argument as executable sub get_exec { # return early if there is no executable if ($START_LISTEN + 1 == $ARGC) { print "listen: Executable missing\n"; exit 3; } # set executable index $EXEC_LISTEN = $ARGV[-1]; } # check flags are compatible with each other sub require_flags { # if there is multiple files, check if both or no flags are found if ($EXEC_POSITION - $START_LISTEN != 1) { if (!$ALL_FLAG and !$ANY_FLAG) { print "listen: Either -a or -o must be specified with multiple files to watch\n"; exit 4; } elsif ($ALL_FLAG and $ANY_FLAG) { print "listen: You must specify either -a or -o for multiple files, not both\n"; exit 4; } $MULTI_FILES = "def"; # otherwise if there are flags found for a singular file, quit } else { if ($ALL_FLAG) { print "listen: Invalid argument -a for singular file\n"; exit 4; } elsif ($ANY_FLAG) { print "listen: Invalid argument -o for singular file\n"; exit 4; } } # if checksum is wanted, make sure timeout is not present if ($CHECKSUM_FLAG) { if ($TIMEOUT) { print "listen: Timeout cannot be used in checksum mode\n"; exit 4; } } } # check file(s) if they exist and they are accessable sub check_files { for (my $i = $START_LISTEN; $i < $EXEC_POSITION; $i++) { if (not -e $ARGV[$i]) { print "listen: $ARGV[$i]: No such file or directory\n"; exit 5; } elsif (not -r $ARGV[$i]) { # being unable to read a file does not mean the executable will fail # continue anyway, but warn the user print "listen: $ARGV[$i]: Permission denied (warning)\n"; } } } # return the checksum/time modified sub diff { my @return; # if checksum wanted, append output to return if ($CHECKSUM_FLAG) { for (my $i = $START_LISTEN; $i < $EXEC_POSITION; $i++) { push @return, `$CHECKSUM_COMMAND $ARGV[$i]`; } # else, append epoch } else { for (my $i = $START_LISTEN; $i < $EXEC_POSITION; $i++) { push @return, (stat($ARGV[$i]))[9]; } } return @return; } # main subroutine sub start { # if underscore is present in command, replace it with file listening to # only if singular file being listened to if (!$MULTI_FILES) { if ($EXEC_LISTEN =~ m/ _ /) { $EXEC_LISTEN =~ s/ _ / $ARGV[$START_LISTEN] /g; } elsif ($EXEC_LISTEN =~ m/ _/) { $EXEC_LISTEN =~ s/ _/ $ARGV[$START_LISTEN] /g; } elsif ($EXEC_LISTEN =~ m/_ /) { $EXEC_LISTEN =~ s/_ / $ARGV[$START_LISTEN] /g; } } # print starting information print "$black& listen $VERSION\n"; print "& This program is free software, and comes with ABSOLUTELY NO WARRANTY.\n"; print "& Run 'listen --license' for details.\n&\n"; print "& This shell command will run:\n"; print "& $EXEC_LISTEN\n"; # add additional text if in checksum mode my $checksum_text = ""; my $modified_text = "been modified:"; if ($CHECKSUM_FLAG) { $checksum_text = "the checksum of "; $modified_text = "changed:"; } # print different things with multiple files depending on flag if ($MULTI_FILES) { if ($ALL_FLAG) { print "& When ${checksum_text}all the files below have $modified_text\n"; for (my $i = $START_LISTEN; $i < $EXEC_POSITION; $i++) { print "& $ARGV[$i]\n"; } } elsif ($ANY_FLAG) { print "& When ${checksum_text}any of the files below have $modified_text\n"; for (my $i = $START_LISTEN; $i < $EXEC_POSITION; $i++) { print "& $ARGV[$i]\n"; } } # otherwise if singular, always print this } else { print "& When ${checksum_text}this file has $modified_text\n"; print "& $ARGV[$START_LISTEN]\n"; } # run command immediately if flag is specified, then start listening if ($RUN_FLAG) { print "& Running now, then starting listen...${nocolor}\n"; system $EXEC_LISTEN; my $status = $? >> 8; if (int($status) != 0) { print "& WARNING: Exit code is $status. Returned to listen...$nocolor\n"; } else { print "& Returned to listen...$nocolor\n"; } # otherwise, start listening } else { print "& Starting now...$nocolor\n"; } # start by initializing comparison arrays my @previous_epoch = diff(); my @current_epoch = diff(); # main loop while (1) { my $run = undef; my $file_changed = undef; my @modified_files; # initialize modified_files array, only used if all flag is present for (my $i = $START_LISTEN; $i < $EXEC_POSITION; $i++) { push @modified_files, "no"; } # get current modified info @current_epoch = diff(); # logic for multiple files if ($MULTI_FILES) { # check if all files are modified if ($ALL_FLAG) { my $counter = 0; # if output differs, mark as modified foreach $modified (@modified_files) { if (@current_epoch[$counter] != @previous_epoch[$counter]) { if ($TIMEOUT) { if (!($current_epoch[$counter] - $previous_epoch[$counter] <= $TIMEOUT)) { $modified = "yes"; } } else { $modified = "yes"; } } $counter++; } # if one file is not modified, do not run foreach $modified (@modified_files) { $run = 'def'; if ($modified !~ "yes") { $run = undef; last; } } # check if any of the files are modified } else { my $counter = 0; # if output differs, run command foreach (@current_epoch) { if ($current_epoch[$counter] != $previous_epoch[$counter]) { if ($TIMEOUT) { if (!($current_epoch[$counter] - $previous_epoch[$counter] <= $TIMEOUT)) { $run = "def"; $file_changed = $counter; last; } } else { $run = "def"; $file_changed = $counter; last; } } $counter++; } } # logic for singular files } else { if ($current_epoch[0] != $previous_epoch[0]) { if ($TIMEOUT) { if (!($current_epoch[0] - $previous_epoch[0] <= $TIMEOUT)) { $run = "def"; $file_changed = 0; } } else { $run = "def"; $file_changed = 0; } } } # if logic above allows the executable to run if ($run) { # change output if multiple files are present if ($MULTI_FILES) { if ($ANY_FLAG) { print "$black& File \"$ARGV[$file_changed + 1]\" modified. Running command...$nocolor\n"; } elsif ($ALL_FLAG) { print "$black& All files have been modified. Running command...$nocolor\n"; } } else { print "$black& File \"$ARGV[$START_LISTEN]\" modified. Running command...$nocolor\n"; } # run command system $EXEC_LISTEN; # catch return code my $status = $? >> 8; if (int($status) != 0) { print "$black& WARNING: Exit code is $status. Returned to listen...$nocolor\n"; } else { print "$black& Returned to listen...$nocolor\n"; } # overwrite current as previous @previous_epoch = @current_epoch; } sleep(1); } } # ---- START OF SCRIPT ---- # start by checking flags flags(); # get executable to run on trigger get_exec(); # check if additional flags are needed # because multiple files are being listened to require_flags(); # check if file(s) exist check_files(); # start listen start();