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 (https://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