I’ve updated the Autocall Ruby script I’ve mentioned various times before by porting it to Zsh and adding a TON of features. The following posts are now obsolete:
Autocall: A Script to Watch and “Call” Programs on a Changed Target Source File
Updated Autolily Script
Auto-update/make/compile script for Lilypond: Autolily
If you haven’t read my previous posts on this topic, basically, Autocall is a script that I wrote to automate executing an arbitrary command (from the shell) any time some file is modified. In practice, it is very useful for any small/medium project that needs to compile/build something out of source code. Typical use cases are for LilyPond files, LaTeX/XeTeX, and C/C++ source code.
Anyway, the script is now very robust and much more intelligent. It now parses options and flags instead of stupidly looking at ARGV one at a time. Perhaps the best new feature is the ability to kill processes that hang. Also, you can now specify up to 9 different commands with 9 instances of the “-c” flag (additional instances are ignored). Commands 2-9 are accessible manually via the numeric keys 2-9 (command 1 is the default). This is useful if you have, for instance, different build targets in a makefile. E.g., you could do
$ autocall -c "make" -c "make -B all" -c "make clean" -l file_list
to make things a bit easier.
I use this script all the time — mostly when working with XeTeX files or compiling source code. It works best in a situation where you have to do something X whenever files/directories Y changes in any way. Again, the command to be executed is arbitrary, so you could use it to call some script X whenever a change is detected in a file/directory. If you use it with LaTeX/XeTeX/TeX, use the “-halt-on-error” option so that you don’t have to have autocall kill it (only available with the -k flag). The copious comments should help you get started. Like all my stuff, it is not licensed at all — it’s released into the PUBLIC DOMAIN, without ANY warranties whatsoever in any jurisdiction (use at your own risk!).
#!/bin/zsh
# PROGRAM: autocall
# AUTHOR: Shinobu (http://zuttobenkyou.wordpress.com)
# LICENSE: PUBLIC DOMAIN
#
#
# DESCRIPTION:
#
# Autocall watches (1) a single file, (2) directory, and/or (3) a text file
# containing a list of files/directories, and if the watched files and/or
# directories become modified, runs the (first) given command string. Multiple
# commands can be provided (a total of 9 command strings are recognized) to
# manually execute different commands.
#
#
# USAGE:
#
# See msg("help") function below -- read that portion first!
#
#
# USER INTERACTION:
#
# Press "h" for help.
# Pressing a SPACE, ENTER, or "1" key forces execution of COMMAND immediately.
# Keys 2-9 are hotkeys to extra commands, if there are any.
# Press "c" for the command list.
# To exit autocall gracefully, press "q".
#
#
# DEFAULT SETTINGS:
#
# (-w) DELAY = 5
# (-x) FACTOR = 4
#
#
# EXAMPLES:
#
# Execute "pdflatex -halt-on-error report.tex" every time "report.tex" or "ch1.tex" is
# modified (if line count changes in either file; modification checked every 5
# seconds by default):
# autocall -c "pdflatex -halt-on-error report.tex" -F report.tex -f ch1.tex
#
# Same, but only look at "ch1.tex" (useful, assuming that report.tex includes
# ch1.tex), and automatically execute every 4 seconds:
# autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -w 1 -x 4
# (-x 0 or -x 1 here would also work)
#
# Same, but also automatically execute every 20 (5 * 4) seconds:
# autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -x 4
#
# Same, but automatically execute every 5 (5 * 1) seconds (-w is 5 by default):
# autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -x 1
#
# Same, but automatically execute every 1 (1 * 1) second:
# autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -w 1 -x 1
#
# Same, but automatically execute every 17 (1 * 17) seconds:
# autocall -c "pdflatex -halt-on-error report.tex" -F ch1.tex -w 1 -x 17
#
# Same, but for "ch1.tex", watch its byte size, not line count:
# autocall -c "pdflatex -halt-on-error report.tex" -b ch1.tex -w 1 -x 17
#
# Same, but for "ch1.tex", watch its timestamp instead (i.e., every time
# this file is saved, the modification timestamp will be different):
# autocall -c "pdflatex -halt-on-error report.tex" -f ch1.tex -w 1 -x 17
#
# Same, but also look at the contents of directory "images/ocean":
# autocall -c "pdflatex -halt-on-error report.tex" -f ch1.tex -d images/ocean -w 1 -x 17
#
# Same, but also look at the contents of directory "other" recursively:
# autocall -c "pdflatex -halt-on-error report.tex" -f ch1.tex -d images/ocean -D other -w 1 -x 17
#
# Same, but look at all files and/or directories (recursively) listed in file
# "watchlist" instead:
# autocall -c "pdflatex -halt-on-error report.tex" -l watchlist -w 1 -x 17
#
# Same, but also look at "newfile.tex":
# autocall -c "pdflatex -halt-on-error report.tex" -l watchlist -f newfile.tex -w 1 -x 17
#
# Same, but also allow manual execution of "make clean" with hotkey "2":
# autocall -c "pdflatex -halt-on-error report.tex" -c "make clean" -l watchlist -f newfile.tex -w 1 -x 17
#
###############################################################################
###############################################################################
#-----------------#
# Local functions #
#-----------------#
msg () {
case $1 in
"help")
echo "
autocall: Usage:
autocall [OPTIONS]
Required parameter:
-c COMMAND The command to be executed (put COMMAND in quotes). Note that
COMMAND can be a set of multiple commands, e.g. \"make clean;
make\". You can also specify multiple commands by invoking
-c COMMAND multiple times -- the first 9 of these are set to
hotkeys 1 through 9, if present. This is useful if you want to
have a separate command that is available and can only be
executed manually.
One or more required parameters (but see -x below):
-f FILE File to be watched. Modification detected by time.
-F FILE File to be watched. Modification detected by line-size.
-b FILE File to be watched. Modification detected by bytes.
-d DIRECTORY Directory to be watched. Modification detected by time.
-D DIRECTORY Directory to be watched, recursively. Modification
detected by time.
-l FILE Text file containing a list of files/directories (each on
its own line) to be watched (directories listed here are
watched recursively). Modification is detected with 'ls'.
Optional parameters:
-w DELAY Wait DELAY seconds before checking on the watched
files/directories for modification; default 5.
-t TIMEOUT If COMMAND does not finish execution after TIMEOUT seconds,
send a SIGTERM signal to it (but do nothing else afterwards).
-k KDELAY If COMMAND does not finish execution after TIMEOUT,
then wait KDELAY seconds and send SIGKILL to it if COMMAND is
still running. If only -k is given without -t, then -t is
automatically set to the same value as TIMEOUT.
-x FACTOR Automatically execute the command repeatedly every DELAY *
FACTOR seconds, regardless of whether the watched
files/directories were modified. If FACTOR is zero, it is set
to 1. If -x is set, then -f, -d, and -l are not required (i.e.,
if only the -c and -x options are specified, autocall will
simply act as a while loop executing COMMAND every 20 (or
more if FACTOR is greater than 1) seconds). Since the
formula is (DELAY * FACTOR) seconds, if DELAY is 1,
FACTOR's face value itself, if greater than 0, is the seconds
amount.
-a Same as \`-x 1'
-h Show this page and exit (regardless of other parameters).
-v Show version number and exit (regardless of other parameters).
"
exit 0
;;
"version")
echo "autocall version 1.0"
exit 0
;;
*)
echo "autocall: $1"
exit 1
;;
esac
}
is_number () {
if [[ $(echo $1 | sed 's/^[0-9]\+//' | wc -c) -eq 1 ]]; then
true
else
false
fi
}
autocall_exec () {
timeout=$2
killdelay=$3
col=""
case $4 in
1) col=$c1 ;;
2) col=$c2 ;;
3) col=$c3 ;;
4) col=$c4 ;;
5) col=$c5 ;;
6) col=$c6 ;;
*) col=$c1 ;;
esac
echo "\nautocall:$c2 [$(date --rfc-3339=ns)]$ce$col $5$ce"
if [[ $# -eq 7 ]]; then
diff -u0 -B -d <(echo "$6") <(echo "$7") | tail -n +4 | sed -e "/^[@-].\+/d" -e "s/\(\S\+\s\+\S\+\s\+\S\+\s\+\S\+\s\+\)\(\S\+\s\+\)\(\S\+\s\+\S\+\s\+\S\+\s\+\)/\1$c1\2$ce$c2\3$ce/" -e "s/^/ $c1>$ce /"
echo
fi
echo "autocall: calling command \`$c4$1$ce'..."
# see the "z" flag under PARAMTER EXPANSION under "man ZSHEXPN" for more info
if [[ $tflag == true || $kflag == true ]]; then
# the 'timeout' command gives nice exit statuses -- it gives 124 if
# command times out, but if the command exits with an error of its own,
# it gives that error number (so if the command doesn't time out, but
# exits with 4 or 255 or whatever, it (the timeout command) will exit
# with that number instead)
# note: if kflag is true, then tflag is always true
com_exit_status=0
if [[ $kflag == true ]]; then
eval timeout -k $killdelay $timeout $1 2>&1 | sed "s/^/ $col>$ce /"
com_exit_status=$pipestatus[1]
else
eval timeout $timeout $1 2>&1 | sed "s/^/ $col>$ce /"
com_exit_status=$pipestatus[1]
fi
if [[ $com_exit_status -eq 124 ]]; then
echo "\n${c6}autocall: command timed out$ce"
elif [[ $com_exit_status -ne 0 ]]; then
echo "\n${c6}autocall: command exited with error status $com_exit_status$ce"
else
echo "\n${c1}autocall: command executed successfully$ce"
fi
else
eval $1 2>&1 | sed "s/^/ $col>$ce /"
com_exit_status=$pipestatus[1]
if [[ $com_exit_status -ne 0 ]]; then
echo "\n${c6}autocall: command exited with error status $com_exit_status$ce"
else
echo "\n${c1}autocall: command executed successfully$ce"
fi
fi
}
#------------------#
# Global variables #
#------------------#
# colors
c1="\x1b[1;32m" # bright green
c2="\x1b[1;33m" # bright yellow
c3="\x1b[1;34m" # bright blue
c4="\x1b[1;36m" # bright cyan
c5="\x1b[1;35m" # bright purple
c6="\x1b[1;31m" # bright red
ce="\x1b[0m"
coms=()
delay=5
xdelay_factor=4
f=()
F=()
b=()
d=()
D=()
l=()
l_targets=()
wflag=false
xflag=false
tflag=false
kflag=false
timeout=0
killdelay=0
tstampf="" # used to DISPLAY modification only for -f flag
linestamp="" # used to DETECT modification only for -f flag
tstampF="" # used to detect AND display modifications for -F flag
tstampb="" # used to DISPLAY modification only for -b flag
bytestamp="" # used to DETECT modification only for -b flag
tstampd="" # used to detect AND display modifications for -d flag
tstampD="" # used to detect AND display modifications for -D flag
tstampl="" # used to detect AND display modifications for -l flag
tstampf_new=""
linestamp_new=""
tstampF_new=""
tstampb_new=""
bytestamp_new=""
tstampd_new=""
tstampD_new=""
tstampl_new=""
#----------------#
# PROGRAM START! #
#----------------#
#---------------#
# Parse options #
#---------------#
# the leading ":" in the opstring silences getopts's own error messages;
# the colon after a single letter indicates that that letter requires an
# argument
# first parse for the presence of any -h and -v flags (while silently ignoring
# the other recognized options)
while getopts ":c:w:f:F:b:d:D:l:t:k:x:ahv" opt; do
case "$opt" in
h) msg "help" ;;
v) msg "version" ;;
*) ;;
esac
done
# re-parse from the beginning again if there were no -h or -v flags
OPTIND=1
while getopts ":c:w:f:F:b:d:D:l:t:k:x:a" opt; do
case "$opt" in
c)
com_binary=$(echo "$OPTARG" | sed 's/ \+/ /g' | sed 's/;/ /g' | cut -d " " -f1)
if [[ $(which $com_binary) == "$com_binary not found" ]]; then
msg "invalid command \`$com_binary'"
else
coms+=("$OPTARG")
fi
;;
w)
if $(is_number "$OPTARG"); then
if [[ $OPTARG -gt 0 ]]; then
wflag=true
delay=$OPTARG
else
msg "DELAY must be greater than 0"
fi
else
msg "invalid DELAY \`$OPTARG'"
fi
;;
f)
if [[ ! -f "$OPTARG" ]]; then
msg "file \`$OPTARG' does not exist"
else
f+=("$OPTARG")
fi
;;
F)
if [[ ! -f "$OPTARG" ]]; then
msg "file \`$OPTARG' does not exist"
else
F+=("$OPTARG")
fi
;;
b)
if [[ ! -f "$OPTARG" ]]; then
msg "file \`$OPTARG' does not exist"
else
b+=("$OPTARG")
fi
;;
d)
if [[ ! -d "$OPTARG" ]]; then
msg "directory \`$OPTARG' does not exist"
else
d+=("$OPTARG")
fi
;;
D)
if [[ ! -d "$OPTARG" ]]; then
msg "directory \`$OPTARG' does not exist"
else
D+=("$OPTARG")
fi
;;
l)
if [[ ! -f $OPTARG ]]; then
msg "file \`$OPTARG' does not exist"
else
l+=("$OPTARG")
fi
;;
t)
tflag=true
if $(is_number "$OPTARG"); then
if [[ $OPTARG -gt 0 ]]; then
timeout=$OPTARG
else
msg "TIMEOUT must be greater than 0"
fi
else
msg "invalid TIMEOUT \`$OPTARG'"
fi
;;
k)
kflag=true
if $(is_number "$OPTARG"); then
if [[ $OPTARG -gt 0 ]]; then
killdelay=$OPTARG
else
msg "TIMEOUT must be greater than 0"
fi
else
msg "invalid KDELAY \`$OPTARG'"
fi
;;
x)
xflag=true
if $(is_number "$OPTARG"); then
if [[ $OPTARG -gt 0 ]]; then
xdelay_factor=$OPTARG
elif [[ $OPTARG -eq 0 ]]; then
xdelay_factor=1
else
msg "invalid FACTOR \`$OPTARG'"
fi
fi
;;
a) xflag=true ;;
msg "missing argument for option \`$OPTARG'"
;;
*)
msg "unrecognized option \`$OPTARG'"
;;
esac
done
#-----------------#
# Set misc values #
#-----------------#
if [[ $kflag == true && $tflag == false ]]; then
tflag=true
timeout=$killdelay
fi
#------------------#
# Check for errors #
#------------------#
# check that the given options are in good working order
if [[ -z $coms[1] ]]; then
msg "help"
elif [[ (-n $f && -n $d && -n $D && -n $l) && $xflag == false ]]; then
echo "autocall: see help with -h"
msg "at least one or more of the (1) -f, -d, -D, or -l paramters, or (2) the -x parameter, required"
fi
#-------------------------------#
# Record state of watched files #
#-------------------------------#
if [[ -n $F ]]; then
if [[ $#F -eq 1 ]]; then
linestamp=$(wc -l $F)
else
linestamp=$(wc -l $F | head -n -1) # remove the last "total" line
fi
tstampF=$(ls --full-time $F)
fi
if [[ -n $f ]]; then
tstampf=$(ls --full-time $f)
fi
if [[ -n $b ]]; then
if [[ $#b -eq 1 ]]; then
bytestamp=$(wc -c $b)
else
bytestamp=$(wc -c $b | head -n -1) # remove the last "total" line
fi
tstampb=$(ls --full-time $b)
fi
if [[ -n $d ]]; then
tstampd=$(ls --full-time $d)
fi
if [[ -n $D ]]; then
tstampD=$(ls --full-time -R $D)
fi
if [[ -n $l ]]; then
for listfile in $l; do
if [[ ! -f $listfile ]]; then
msg "file \`$listfile ' does not exist"
else
while read line; do
if [[ ! -e "$line" ]]; then
msg "\`$listfile': file/path \`$line' does not exist"
else
l_targets+=("$line")
fi
done < $listfile # read contents of $listfile!
fi
done
tstampl=$(ls --full-time -R $l_targets)
fi
#----------------------#
# Begin execution loop #
#----------------------#
# This is like Russian Roulette (where "firing" is executing the command),
# except that all the chambers are loaded, and that on every new turn, instead
# of picking the chamber randomly, we look at the very next chamber. After
# every chamber is given a turn, we reload the gun and start over.
#
# If we detect file/directory modification, we pull the trigger. We can also
# pull the trigger by pressing SPACE or ENTER. If the -x option is provided,
# the last chamber will be set to "always shoot" and will always fire (if the
# trigger hasn't been pulled by the above methods yet).
if [[ $xflag == true && $xdelay_factor -le 1 ]]; then
xdelay_factor=1
fi
com_num=1
for c in $coms; do
echo "autocall: command slot $com_num set to \`$c4$coms[$com_num]$ce'"
let com_num+=1
done
echo "autocall: press keys 1-$#coms to execute a specific command"
if [[ $wflag == true ]]; then
echo "autocall: modification check interval set to $delay sec"
else
echo "autocall: modification check interval set to $delay sec (default)"
fi
if [[ $xflag == true ]]; then
echo "autocall: auto-execution interval set to ($delay * $xdelay_factor) = $(($delay*$xdelay_factor)) sec"
fi
if [[ $tflag == true ]]; then
echo "autocall: TIMEOUT set to $timeout"
if [[ $kflag == true ]]; then
echo "autocall: KDELAY set to $killdelay"
fi
fi
echo "autocall: press ENTER or SPACE to execute manually"
echo "autocall: press \`c' for command list"
echo "autocall: press \`h' for help"
echo "autocall: press \`q' to quit"
key=""
while true; do
for i in {1..$xdelay_factor}; do
#------------------------------------------#
# Case 1: the user forces manual execution #
#------------------------------------------#
# read a single key from the user
read -s -t $delay -k key
case $key in
# note the special notation $'\n' to detect an ENTER key
$'\n'|" "|1)
autocall_exec $coms[1] $timeout $killdelay 4 "manual execution"
key=""
continue
;;
2|3|4|5|6|7|8|9)
if [[ -n $coms[$key] ]]; then
autocall_exec $coms[$key] $timeout $killdelay 4 "manual execution"
key=""
continue
else
echo "autocall: command slot $key is not set"
key=""
continue
fi
;;
c)
com_num=1
echo ""
for c in $coms; do
echo "autocall: command slot $com_num set to \`$c4$coms[$com_num]$ce'"
let com_num+=1
done
key=""
continue
;;
h)
echo "\nautocall: press \`c' for command list"
echo "autocall: press \`h' for help"
echo "autocall: press \`q' to exit"
com_num=1
for c in $coms; do
echo "autocall: command slot $com_num set to \`$c4$coms[$com_num]$ce'"
let com_num+=1
done
echo "autocall: press keys 1-$#coms to execute a specific command"
echo "autocall: press ENTER or SPACE or \`1' to execute first command manually"
key=""
continue
;;
q)
echo "\nautocall: exiting..."
exit 0
;;
*) ;;
esac
#------------------------------------------------------------------#
# Case 2: modification is detected among watched files/directories #
#------------------------------------------------------------------#
if [[ -n $f ]]; then
tstampf_new=$(ls --full-time $f)
fi
if [[ -n $F ]]; then
if [[ $#F -eq 1 ]]; then
linestamp_new=$(wc -l $F)
else
linestamp_new=$(wc -l $F | head -n -1) # remove the last "total" line
fi
tstampF_new=$(ls --full-time $F)
fi
if [[ -n $b ]]; then
if [[ $#b -eq 1 ]]; then
bytestamp_new=$(wc -c $b)
else
bytestamp_new=$(wc -c $b | head -n -1) # remove the last "total" line
fi
tstampb_new=$(ls --full-time $b)
fi
if [[ -n $d ]]; then
tstampd_new=$(ls --full-time $d)
fi
if [[ -n $D ]]; then
tstampD_new=$(ls --full-time -R $D)
fi
if [[ -n $l ]]; then
tstampl_new=$(ls --full-time -R $l_targets)
fi
if [[ -n $f && "$tstampf" != "$tstampf_new" ]]; then
autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampf" "$tstampf_new"
tstampf=$tstampf_new
continue
elif [[ -n $F && "$linestamp" != "$linestamp_new" ]]; then
autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampF" "$tstampF_new"
linestamp=$linestamp_new
tstampF=$tstampF_new
continue
elif [[ -n $b && "$bytestamp" != "$bytestamp_new" ]]; then
autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampb" "$tstampb_new"
bytestamp=$bytestamp_new
tstampb=$tstampb_new
continue
elif [[ -n $d && "$tstampd" != "$tstampd_new" ]]; then
autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampd" "$tstampd_new"
tstampd=$tstampd_new
continue
elif [[ -n $D && "$tstampD" != "$tstampD_new" ]]; then
autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampD" "$tstampD_new"
tstampD=$tstampD_new
continue
elif [[ -n $l && "$tstampl" != "$tstampl_new" ]]; then
autocall_exec $coms[1] $timeout $killdelay 1 "change detected" "$tstampl" "$tstampl_new"
tstampl=$tstampl_new
continue
fi
#-----------------------------------------------------#
# Case 3: periodic, automatic execution was requested #
#-----------------------------------------------------#
if [[ $xflag == true && $i -eq $xdelay_factor ]]; then
autocall_exec $coms[1] $timeout $killdelay 3 "commencing auto-execution ($(($delay*$xdelay_factor)) sec)"
fi
done
done
# vim:syntax=zsh