# Latex: Saner Source Code Listings: No Starch Press Style

I’ve lately gotten in the habit of writing aesthetically pleasing ebooks with LaTeX for myself on new subjects that I come across. This way, I can (1) learn more about TeX (always a huge bonus) and (2) create documents that are stable, portable (PDF output), beautiful, and printer-friendly. (I also put all the .tex and accompanying makefiles into version control (git) and sync it across all of my computers, to ensure that they last forever.)

One of those new subjects for me right now is the Haskell programming language. I started copying down small code snippets from various free resources on the web, with the help of the Listings package (actually named the lstlisting package internally).

## The Problem

Unfortunately, I got tired of referencing line numbers manually to explain the code snippets, like this:

\documentclass[twoside]{article}
\usepackage[x11names]{xcolor} % for a set of predefined color names, like LemonChiffon1

\lstnewenvironment{csource}[1][]
{\lstset{basicstyle=\ttfamily,language=C,numberstyle=\numold,numbers=left,frame=lines,framexleftmargin=0.5em,framexrightmargin=0.5em,backgroundcolor=\color{LemonChiffon1},showstringspaces=false,escapeinside={(*@}{@*)},#1}}
{}

\begin{document}
\section{Hello, world!}
The following program \texttt{hello.c} simply prints Hello, world!'':

\begin{csource}
#include <stdio.h>

int main()
{
printf("Hello, world!\n");
return 0;
}
\end{csource}

At line 1, we include the \texttt{stdio.h} header file. We begin the \texttt{main} function at line 3. We print Hello, world!'' to standard output (a.k.a., \textit{STDOUT}) at line 5. At line 6, we return value 0 to let the caller of this program know that we exited safely without any errors.
\end{document}


Output (compiled with the xelatex command):

This is horrible. Any time I add or delete a single line of code, I have to manually go back and change all of the numbers. On the other hand, I could instead use the \label{labelname} command inside the lstlisting environment on any particular line, and get the line number of that label with \ref{comment}, like so:

\section{Hello, world!}
The following program \texttt{hello.c} simply prints Hello, world!'':

\begin{csource}
#include <stdio.h>(*@\label{include}@*)

int main()(*@\label{main}@*)
{
printf("Hello, world!\n");(*@\label{world}@*)
return 0;(*@\label{return}@*)
}
\end{csource}
At line \ref{include}, we include the \texttt{stdio.h} header file. We begin the \texttt{main} function at line \ref{main}. We print Hello, world!'' to standard output (a.k.a., \textit{STDOUT}) at line \ref{world}. At line \ref{return}, we return value 0 to let the caller of this program know that we exited safely without any errors.


Output:

(The only difference is that the line number references are now red. This is because I also used the hyperref package with the colorlinks option.)

But I felt like this was not a good solution at all (it was suggested by the official lstlisting package’s manual, Section 7 “How tos”). For one thing, I have to think of a new label name for every line I want to address. Furthermore, I have to compile the document twice, because that’s how the \label command works. Yuck.

## Enlightenment

So again, with my internet-research hat on, I tried to figure out a better solution. The first thing that came to mind was how the San Francisco-based publisher No Starch Press displayed source code listings in their newer books. Their stylistic approach to source code listings is probably the most sane (and beautiful!) one I’ve come across.

After many hours of tinkering, trial-and-error approaches, etc., I finally got everything working smoothly. Woohoo! From what I can tell, my rendition looks picture-perfect, and dare I say, even better than their version (because I use colors, too). Check it out!

\documentclass{article}
\usepackage[x11names]{xcolor} % for a set of predefined color names, like LemonChiffon1
\usepackage{libertine} % for the pretty dark-circle-enclosed numbers

% Allow "No Starch Press"-like custom line numbers (essentially, bulleted line numbers for only those lines the author will address)
\newcounter{lstNoteCounter}
\newcommand{\lnnum}[1]
{\ifthenelse{#1 =  1}{\libertineGlyph{uni2776}}
{\ifthenelse{#1 =  2}{\libertineGlyph{uni2777}}
{\ifthenelse{#1 =  3}{\libertineGlyph{uni2778}}
{\ifthenelse{#1 =  4}{\libertineGlyph{uni2779}}
{\ifthenelse{#1 =  5}{\libertineGlyph{uni277A}}
{\ifthenelse{#1 =  6}{\libertineGlyph{uni277B}}
{\ifthenelse{#1 =  7}{\libertineGlyph{uni277C}}
{\ifthenelse{#1 =  8}{\libertineGlyph{uni277D}}
{\ifthenelse{#1 =  9}{\libertineGlyph{uni277E}}
{\ifthenelse{#1 = 10}{\libertineGlyph{uni277F}}
{\ifthenelse{#1 = 11}{\libertineGlyph{uni24EB}}
{\ifthenelse{#1 = 12}{\libertineGlyph{uni24EC}}
{\ifthenelse{#1 = 13}{\libertineGlyph{uni24ED}}
{\ifthenelse{#1 = 14}{\libertineGlyph{uni24EE}}
{\ifthenelse{#1 = 15}{\libertineGlyph{uni24EF}}
{\ifthenelse{#1 = 16}{\libertineGlyph{uni24F0}}
{\ifthenelse{#1 = 17}{\libertineGlyph{uni24F1}}
{\ifthenelse{#1 = 18}{\libertineGlyph{uni24F2}}
{\ifthenelse{#1 = 19}{\libertineGlyph{uni24F3}}
{\ifthenelse{#1 = 20}{\libertineGlyph{uni24F4}}
{NUM TOO HIGH}}}}}}}}}}}}}}}}}}}}}
\newcommand*{\lnote}{\stepcounter{lstNoteCounter}\vbox{\llap{{\lnnum{\thelstNoteCounter}}\hskip 1em}}}
\lstnewenvironment{csource2}[1][]
{
\setcounter{lstNoteCounter}{0}
\lstset{basicstyle=\ttfamily,language=C,numberstyle=\numold,numbers=right,frame=lines,framexleftmargin=0.5em,framexrightmargin=0.5em,backgroundcolor=\color{LemonChiffon1},showstringspaces=false,escapeinside={(*@}{@*)},#1}
}
{}

\begin{document}
\section{Hello, world!}
The following program \texttt{hello.c} simply prints Hello, world!'':

\begin{csource2}
(*@\lnote@*)#include <stdio.h>

/* This is a comment. */
(*@\lnote@*)int main()
{
(*@\lnote@*)    printf("Hello, world!\n");
(*@\lnote@*)    return 0;
}
\end{csource2}

We first include the \texttt{stdio.h} header file \lnnum{1}. We then declare the \texttt{main} function \lnnum{2}. We then print Hello, world!'' to standard output (a.k.a., \textit{STDOUT}) \lnnum{3}. Finally, we return value 0 to let the caller of this program know that we exited safely without any errors \lnnum{4}.
\end{document}


Output (compiled with the xelatex command):

Ah, much better! This is pretty much how No Starch Press does it, except that we’ve added two things: the line number count on the right hand side, and also a colored background to make the code stand out a bit from the page (easier on the eyes when quickly skimming). These options are easily adjustable/removable to suit your needs (see line 33). For a direct comparison, download some of their sample chapter offerings from Land of Lisp (2010) and Network Flow Analysis (2010) and see for yourself.

## Explanation

If you look at the code, you can see that the only real thing that changed in the listing code itself is the use of a new \lnote command. The \lnote command basically spits out a symbol, in this case whatever the \lnnum command produces with the value of the given counter, lstNoteCounter. The \lnnum command is basically a long if/else statement chain (I tried using the switch statement construct from here, but then extra spaces would get added in), and can produce a nice glyph, but only up to 20 (for anything higher, it just displays the string “NUM TOO HIGH.” This is because it uses the Linux Libertine font to create the glyph (with \libertineGlyph), and Libertine’s black-circle-enclosed numerals only go up to 20). A caveat: Linux Libertine’s LaTeX commands are subject to change without notice, as it is currently in “alpha” stage of development (e.g., the \libertineGlyph command actually used to be called the \Lglyph command, if I recall correctly).

The real star of the show is the \llap command. I did not know about this command until I stumbled on this page yesterday. For something like an hour I toiled over trying to use \marginpar or \marginnote (the marginnote package) to get the same effect, without success (for one thing, it is impossible to create margin notes on one side (e.g., always on the left, or always on the right) if your document class is using the twoside option.

The custom \numold command (used here to typeset the line numbers with old style figures) is actually a resurrection of a command of the same name from an older, deprecated Linux Libertine package. The cool thing about how it’s defined is that you can use it with or without an argument. Because of this \numold command and how it’s defined, you have to use the XeTeX engine (i.e., compile with the xelatex command).

In all of my examples above, the serif font is Linux Libertine, and the monospaced font is DejaVu Sans Mono.

## Other Thoughts

You may have noticed that I have chosen to use my own custom lstlisting environment (with the \lstnewenvironment command). The only reason I did this is because I can specify a custom command that starts up each listing environment. In my case, it’s \setcounter{lstNoteCounter}{0}, which resets the notes back to 0 for the listing.

Feel free to use my code. If you make improvements to it, please let me know! By the way, Linux Libertine’s latex package supports other enumerated glyphs, too (e.g., white-circle-enclosed numbers, or even circle-enclosed alphabet characters). Or, you could even use plain letters enclosed inside a \colorbox command, if you want. You can also put the line numbers right next to the \lnote numbers (we just add numbers=left to the lstnewenvironment options, and change \lnote’s hskip to \2.7em; I also changed the line numbers to basic italics):

\lstnewenvironment{csource2}[1][]
{\lstset{basicstyle=\ttfamily,language=C,numberstyle=\itshape,numbers=left,frame=lines,framexleftmargin=0.5em,framexrightmargin=0.5em,backgroundcolor=\color{LemonChiffon1},showstringspaces=false,escapeinside={(*@}{@*)},#1}}
{\stepcounter{lstCounter}}

\newcommand*{\lnote}{\stepcounter{lstNoteCounter}\llap{{\lnnum{\thelstNoteCounter}}\hskip 2.7em}}


Output:

Or we can swap their positions (by using the numbersep= command to the lstnewenvironment declaration, and leaving \lnnote as-is with just 1em of \hskip):

\lstnewenvironment{csource2}[1][]
{\lstset{basicstyle=\ttfamily,language=C,numberstyle=\itshape,numbers=left,numbersep=2.7em,frame=lines,framexleftmargin=0.5em,framexrightmargin=0.5em,backgroundcolor=\color{LemonChiffon1},showstringspaces=false,escapeinside={(*@}{@*)},#1}}
{\stepcounter{lstCounter}}


Output:

The possibilities are yours to choose.

In case you’re wondering, the unusual indenting of the section number into the left margin in my examples is achieved as follows:

\usepackage{titlesec}
\titleformat{\section}
{\Large\bfseries} % formatting
{\llap{
{\thesection}
\hskip 0.5em
}}
{0em}% horizontal sep
{}% before


It’s a simplified version of the code posted on the StackOverflow link above.

NB: The \contentsname command (used to render the “Contents” text if you have a Table of Contents) inherits the formatting from the \chapter command (i.e., if you use \titleformat{\chapter}). Thus, if you format the \chapter command like the \section command above with \titleformat, the only way to prevent this inheritance (if it is not desired) is by doing this:

\renewcommand\contentsname{\normalfont Contents}


This way, your Table of Content’s title will stay untouched by any \titleformat command.

UPDATE December 7, 2010: Some more variations and screenshots.

# Improved Autocall Script

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:

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'
-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
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  # Autocall: A Script to Watch and “Call” Programs on a Changed Target Source File UPDATE July 24, 2010: This post is now totally obsolete. See this post instead. Recently, I’ve realized that the Autolily script I made was just one solution to a larger class of problems — that of calling a specific program on a target source/text file repeatedly every time you change the source. So, I’ve modified it slightly to make it accept any program name, so that the general format is: autocall [program] [file]. The source code is below: #!/usr/bin/ruby #===============================================================================================================# # Program name: Autocall # # Author: Shinobu (zuttobenkyou.wordpress.com) # # Date: March 2010 # # LICENSE: PUBLIC DOMAIN # # # # This program takes 2 or 3 arguments; the first 2 is the command and file, while the third optional arg is the # # delay b/n each possible execution of the command. By default this delay is 1 second (it checks if the file has# # been modified every second) # # # # Place this script somewhere, like in ~/scripts # # Then, open up a terminal and call it like so: ~/scripts/autocall.rb [program] [file] # # # # You might want to do a "sudo ln -s" of autocall.rb to one of your system's$PATH directories (e.g., /usr/bin) #
# to avoid typing out the path to autocall.rb every time you use it. Continuing the example from above,         #
# something like "sudo ln -s ~/scripts/autocall.rb /usr/bin/autocall" should do (make sure that                 #
# /usr/bin/autocall does not exist already, as the above comman will overwrite that file if it exists).         #
#                                                                                                               #
# Now you can just do:                                                                                          #
#                                                                                                               #
#     autocall [command] [file]                                                                                 #
#                                                                                                               #
# from anywhere in your system!                                                                                 #
#                                                                                                               #
# To exit, press CTRL-C.                                                                                        #
#===============================================================================================================#

if ARGV.size > 1
file_data_orig = ""
call = ARGV.shift
file = ARGV.shift
delay = 1
if ARGV.size > 0
delay = ARGV.shift.to_i
end
pathsize = file.split("/").size
ls_call = "ls --full-time"

# make sure that the "file" variable is a filename, and not mixed with its path
if pathsize > 1
path_to_file = file.split("/").first(pathsize - 1).join("/")
file = file.split("/").last
ls_call << " #{path_to_file}" # modify our ls command to reflect relative location of file
end

#{ls_call}.split("\n").each do |line|
if line.split(/\s/).last == file
file_data_orig = line
break
end
end
file_data_new = ""

# enter infinite loop -- keep compiling the given file if it has changed in the past 1 second
while true
# detect the file size and also timestamp
lsarr = #{ls_call}.split("\n")
lsarr.shift # get rid of the first line, since that is the size of all the files in the directory

# find our file from ls's output!
lsarr.each do |line|
if line.split(/\s/).last == file
file_data_new = line
break
end
end

# if there is any change detected, run given command on it
if file_data_orig != file_data_new
puts "\n\e[1;38;5;226mautocall: change detected @ #{Time.now.ctime} in file #{file}'; invoking #{call}'...\e[0m\n"
if pathsize > 1
#{call} "#{path_to_file}/#{file}"
else
#{call} "#{file}"
end
file_data_orig = file_data_new
end
sleep delay
end
else
puts "Usage: autocall [command] [file]\n"
end


I can think of at least 1 other time you would want to use this script aside from editing LilyPond files — when editing LaTeX files. For me, I use currently use autocall to call a program that converts text files intelligently to HTML files. You could further edit the source to let it pass along command line options to autocall as well, and not just the program name (I will probably do this myself if the situation presents itself in the future).