446 lines
14 KiB
Perl
Executable file
446 lines
14 KiB
Perl
Executable file
#!/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 <https://www.gnu.org/licenses/>.
|
|
#
|
|
# 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 <https://www.gnu.org/licenses/>.\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();
|
|
|