New version.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 15 Aug 2020 23:10:59 +0000 (00:10 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 15 Aug 2020 23:18:05 +0000 (00:18 +0100)
This is a complete rewrite, and rather more competently done.

44 files changed:
.gitignore [new file with mode: 0644]
.skelrc [new file with mode: 0644]
COPYING [new symlink]
HACKING [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
README.org [new file with mode: 0644]
bench/Makefile.am [new file with mode: 0644]
bench/interp-graph.gp [new file with mode: 0644]
bench/lisp-graph.gp [new file with mode: 0644]
bench/massage-benchmarks [new file with mode: 0755]
bench/t.c [new file with mode: 0644]
bench/t.lisp [new file with mode: 0755]
bench/t.pl [new file with mode: 0755]
bench/t.py [new file with mode: 0755]
bench/t.sh [new file with mode: 0755]
bench/timeit.c [new file with mode: 0644]
config/auto-version [new symlink]
config/confsubst [new symlink]
configure.ac [new file with mode: 0644]
doc/Makefile.am [new file with mode: 0644]
doc/bench.data [new file with mode: 0644]
doc/interp-graph.tikz [new file with mode: 0644]
doc/lisp-graph.tikz [new file with mode: 0644]
dump-runlisp-image.1 [new file with mode: 0644]
dump-runlisp-image.in [new file with mode: 0644]
eval.lisp [new file with mode: 0644]
m4/mdw-auto-version.m4 [new symlink]
m4/mdw-decl-environ.m4 [new symlink]
m4/mdw-define-paths.m4 [new symlink]
m4/mdw-dir-texmf.m4 [new symlink]
m4/mdw-libtool-version-info.m4 [new symlink]
m4/mdw-manext.m4 [new symlink]
m4/mdw-silent-rules.m4 [new symlink]
runlisp.1 [new file with mode: 0644]
runlisp.c [new file with mode: 0644]
t/Makefile.am [new file with mode: 0644]
t/atlocal.in [new file with mode: 0644]
t/autotest.am [new symlink]
t/package.m4 [new file with mode: 0644]
t/tests.m4 [new file with mode: 0644]
t/testsuite.at [new symlink]
tests.at [new file with mode: 0644]
toy-runlisp [new file with mode: 0755]
vars.am [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..767821d
--- /dev/null
@@ -0,0 +1,13 @@
+Makefile.in
+*.tex
+*.pdf
+/_inst/
+/aclocal.m4
+/autom4te.cache/
+/config/compile
+/config/config.h.in
+/config/depcomp
+/config/install-sh
+/config/missing
+/configure
+/t/testsuite
diff --git a/.skelrc b/.skelrc
new file mode 100644 (file)
index 0000000..bb13575
--- /dev/null
+++ b/.skelrc
@@ -0,0 +1,9 @@
+;;; -*-emacs-lisp-*-
+
+(setq skel-alist
+      (append
+       '((author . "Mark Wooding")
+        (full-title . "Runlisp, a tool for invoking Common Lisp scripts")
+        (program . "Runlisp")
+        (licence-text . "[[gpl-3]]"))
+       skel-alist))
diff --git a/COPYING b/COPYING
new file mode 120000 (symlink)
index 0000000..8161f30
--- /dev/null
+++ b/COPYING
@@ -0,0 +1 @@
+.ext/cfd/licence/GPL-3
\ No newline at end of file
diff --git a/HACKING b/HACKING
new file mode 100644 (file)
index 0000000..87cf2b6
--- /dev/null
+++ b/HACKING
@@ -0,0 +1,116 @@
+# -*-org-*-
+#+TITLE: Hacking on =runlisp=
+#+AUTHOR: Mark Wooding
+#+LaTeX_CLASS: strayman
+
+* Adding a new Lisp implementation
+
+When a program needs to know about a bunch of /things/, I generally try
+to arrange that there's exactly one place where you put all of the
+knowledge about each particular /thing/.  In the case of ~runlisp~, I've
+failed rather abjectly.  Sorry.
+
+So, here's the list of places which need to be modified in order to
+teach ~runlisp~ about a new Lisp system.
+
+  + The main C source file ~runlisp.c~ has a master list macro named
+    ~LISP_SYSTEMS~, which just contains an entry ~_(foo)~ for each Lisp
+    system.  Add a new entry for your new system here.  This list
+    ordered according to my personal preference -- the /opinionated
+    order/.
+
+  + There's also a function ~run_foo~ defined in ~runlisp.c~ for each
+    Lisp system ~foo~.  These are defined in a section headed `Invoking
+    Lisp systems', in the opinionated order.
+
+  + The manual page ~runlisp.1~ lists each supported Lisp system by name
+    in the section `Supported Common Lisp implementations'.  These are
+    listed in alphabetical order by command name (so GNU CLisp is
+    ~clisp~, and therefore comes before ~ecl~) -- the /command order/.
+
+  + The ~README.org~ file also has a list of supported Lisp systems,
+    again in command order.
+
+  + In ~configure.ac~, there's a line ~mdw_CHECK_LISP([FOO], [foo])~ for
+    each known Lisp system in the `Checking for Lisp implementations'
+    section, in opinionated order.
+
+  + If the Lisp system needs any additional configure-time hacking, then
+    that goes at the end of the section.  Currently only ECL needs
+    special treatment here, but these are notionally in opinionated
+    order.
+
+  + The file ~vars.am~ builds a list ~LISPS~ of the supported Lisp
+    systems in opinionated order.
+
+  + For each Lisp system that can have a custom image dumped, there's a
+    paragraph in the `Image dumping' section of ~Makefile.am~, which
+    says
+
+    : if DUMP_FOO
+    : image_DATA              += foo+asdf.dump
+    : CLEANFILES              += foo+asdf.dump
+    : foo+asdf.dump: dump-runlisp-image
+    :         (v_dump)./dump-runlisp-image -o$@ foo
+    : endif
+
+    The ~DUMP_FOO~ conditional is already set up by ~mdw_CHECK_LISP~.
+    The ~.dump~ suffix should be whatever extension your Lisp system
+    usually uses to mark its image files.  These paragraphs are in
+    opinionated order.
+
+  + For each Lisp system that can be dumped, there's a section in
+    ~dump-runlisp-image.in~ which goes
+
+    : ## Foo Common Lisp.
+    : deflisp foo foo+asdf.dump
+    : dump_foo () {
+    :   ## ...
+    : }
+
+    These sections are in opinionated order.
+
+  + The ~tests.at~ file has /five/ lists of Lisp systems.
+
+      - The first, named ~LISP_SYSTEMS~ has a pair of entries, ~foo~,
+       ~foo/noimage~ for each Lisp system, in opinionated order.
+
+      - The second is in the macro ~WHICH_LISP~, which contains an entry
+       ~#+foo "foo"~ for each system, in opinionated order.  The former
+       symbol is the Lisp system's (preferred) ~*features*~ keyword
+       name, which is usually the same as its command name, but, for
+       example, is ~cmu~ rather than ~cmucl~ for CMU CL.
+
+      - The third is a ~case~ block in the ~smoke~ test, which contains
+        an entry
+
+       : foo) initfile=.foorc ;;
+
+       naming the system's user initialization file, relative to the
+       user's home directory.  (If your Lisp doesn't have one of these,
+       then this can be anything you like.)
+
+      - The fourth is another ~case~ block in the ~smoke~ test, which
+        contains an entry
+
+       : foo) impl="Foo Common Lisp" ;;
+
+       giving the Lisp system's ~lisp-implementation-type~ string.
+
+      - The fifth is in the ~preferences~ test: there's a ~set~ line
+       which simply lists the Lisp systems' command names.  This is in
+       order of increasing startup time, because the test will be
+       running lots of trivial scripts, simply checking that the right
+       Lisp system is being run, so it's valuable to choose fast Lisps.
+
+  + The script ~bench/massage-benchmarks~ has a hash ~%LISP~ mapping
+    Lisp command names to short labels to use in graphs, in opinionated
+    order.  Add an entry
+
+    :   "foo" => "Foo CL",
+
+    to this hash.
+
+And now the actual pain: the benchmarks need to be run again, and the
+data and graphs in ~README.org~ need to be updated.  Leave this to me.
+
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..02e9b42
--- /dev/null
@@ -0,0 +1,121 @@
+### -*-makefile-*-
+###
+### Build script for `runlisp'
+###
+### (c) 2020 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+###
+### Runlisp 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.
+###
+### Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+include $(top_srcdir)/vars.am
+
+SUBDIRS                         =
+
+pkgdata_DATA            =
+image_DATA              =
+image_SCRIPTS           =
+
+SUBDIRS                        += .
+
+ACLOCAL_AMFLAGS                 = -Im4
+
+###--------------------------------------------------------------------------
+### The main driver program.
+
+bin_PROGRAMS           += runlisp
+runlisp_SOURCES                 = runlisp.c
+man_MANS               += runlisp.1
+
+###--------------------------------------------------------------------------
+### Additional machinery.
+
+pkgdata_DATA           += eval.lisp
+EXTRA_DIST             += eval.lisp
+
+###--------------------------------------------------------------------------
+### Image dumping.
+
+nodist_bin_SCRIPTS     += dump-runlisp-image
+man_MANS               += dump-runlisp-image.1
+
+v_dump                  = $(v_dump_@AM_V@)
+v_dump_                         = $(v_dump_@AM_DEFAULT_V@)
+v_dump_0                = @echo "  DUMP     $@";
+
+EXTRA_DIST             += dump-runlisp-image.in
+CLEANFILES             += dump-runlisp-image
+dump-runlisp-image: dump-runlisp-image.in
+       $(SUBST) $(srcdir)/dump-runlisp-image.in >$@.new \
+               $(SUBSTITUTIONS) && \
+               chmod +x $@.new && mv $@.new $@
+
+if DUMP_SBCL
+image_DATA             += sbcl+asdf.core
+CLEANFILES             += sbcl+asdf.core
+sbcl+asdf.core: dump-runlisp-image
+       $(v_dump)./dump-runlisp-image -o$@ sbcl
+endif
+
+if DUMP_CCL
+image_DATA             += ccl+asdf.image
+CLEANFILES             += ccl+asdf.image
+ccl+asdf.image: dump-runlisp-image
+       $(v_dump)./dump-runlisp-image -o$@ ccl
+endif
+
+if DUMP_CLISP
+image_DATA             += clisp+asdf.mem
+CLEANFILES             += clisp+asdf.mem
+clisp+asdf.mem: dump-runlisp-image
+       $(v_dump)./dump-runlisp-image -o$@ clisp
+endif
+
+if DUMP_ECL
+image_SCRIPTS          += ecl+asdf
+CLEANFILES             += ecl+asdf
+ecl+asdf: dump-runlisp-image
+       $(v_dump)./dump-runlisp-image -o$@ ecl
+endif
+
+if DUMP_CMUCL
+image_DATA             += cmucl+asdf.core
+CLEANFILES             += cmucl+asdf.core
+cmucl+asdf.core: dump-runlisp-image
+       $(v_dump)./dump-runlisp-image -o$@ cmucl
+endif
+
+###--------------------------------------------------------------------------
+### Benchmarking and testing.
+
+if BENCHMARK
+SUBDIRS                        += bench
+endif
+
+SUBDIRS                        += t
+
+###--------------------------------------------------------------------------
+### Distribution.
+
+## Release number.
+dist-hook::
+       echo $(VERSION) >$(distdir)/RELEASE
+
+## Additional build tools.
+EXTRA_DIST             += config/auto-version
+
+###----- That's all, folks --------------------------------------------------
diff --git a/README.org b/README.org
new file mode 100644 (file)
index 0000000..0465005
--- /dev/null
@@ -0,0 +1,446 @@
+# -*-org-*-
+#+TITLE: ~runlisp~ -- run scripts written in Common Lisp
+#+AUTHOR: Mark Wooding
+#+LaTeX_CLASS: strayman
+#+LaTeX_HEADER: \usepackage{tikz, gnuplot-lua-tikz}
+
+~runlisp~ is a small C program intended to be run from a script ~#!~
+line.  It selects and invokes a Common Lisp implementation, so as to run
+the script.  In this sense, ~runlisp~ is a partial replacement for
+~cl-launch~.
+
+Currently, the following Lisp implementations are supported:
+
+  + Armed Bear Common Lisp (~abcl~),
+  + Clozure Common Lisp (~ccl~),
+  + GNU CLisp (~clisp~),
+  + Carnegie--Mellon Univerity Common Lisp (~cmucl~), and
+  + Embeddable Common Lisp (~ecl~), and
+  + Steel Bank Common Lisp (~sbcl~).
+
+I'm happy to take patches to support additional free Lisp
+implementations.  I'm not interested in supporting non-free Lisp
+systems.
+
+
+* Writing scripts in Lisp
+
+** Basic use
+
+The obvious way to use ~runlisp~ is in a shebang (~#!~) line at the top
+of a script.  For example:
+
+: #! /usr/local/bin/runlisp
+: (format t "Hello from Lisp!~%")
+
+Script interpreters must be named with absolute pathnames in shebang
+lines; if your ~runlisp~ is installed somewhere other than
+~/usr/local/bin/~ then you'll need to write something different.
+Alternatively, a common hack involves abusing the ~env~ program as a
+script interpreter, because it will do a path search for the program
+it's supposed to run:
+
+: #! /usr/bin/env runlisp
+: (format t "Hello from Lisp!~%")
+
+** Specific Lisps
+
+Lisp implementations are not created equal -- for good reason.  If your
+script depends on the features of some particular Lisp implementation,
+then you can tell ~runlisp~ that it must use that implementation to run
+your script using the ~-L~ option; for example:
+
+: #! /usr/local/bin/runlisp -Lsbcl
+: (format t "Hello from Steel Bank Common Lisp!~%")
+
+If your script supports several Lisps, but not all, then list them all
+in the ~-L~ option, separated by commas:
+
+: #! /usr/local/bin/runlisp -Lsbcl,ccl
+: (format t #.(concatenate 'string
+:                          "Hello from "
+:                          #+sbcl "Steel Bank"
+:                          #+ccl "Clozure"
+:                          #-(or sbcl ccl) "an unexpected"
+:                          " Common Lisp!~%"))
+
+** Embedded options
+
+If your script requires features of particular Lisp implementations
+/and/ you don't want to hardcode an absolute path to ~runlisp~, then you
+have a problem.  Most Unix-like operating systems will parse a shebang
+line into the initial ~#!~, the pathname to the interpreter program,
+and a /single/ optional argument: any further spaces don't separate
+further arguments: they just get included in the first argument, all the
+way up to the end of the line.  So
+
+: #! /usr/bin/env runlisp -Lsbcl
+: (format t "Hello from Steel Bank Common Lisp!~%")
+
+won't work: it'll just try to run a program named ~runlisp -Lsbcl~, with
+a space in the middle of its name, and that's quite unlikely to exist.
+
+To help with this situation, ~runlisp~ reads /embedded options/ from
+your script.  Specifically, if the script's second line contains the
+token ~@RUNLISP:~ then ~runlisp~ will parse additional options from this
+line.  So the following will work properly.
+
+: #! /usr/bin/env runlisp
+: ;;; @RUNLISP: -Lsbcl
+: (format t "Hello from Steel Bank Common Lisp!~%")
+
+Embedded options are split at spaces properly.  Spaces can be escaped or
+quoted in (an approximation to) the usual shell manner, should that be
+necessary.  See the manpage for the gory details.
+
+** Common environment
+
+~runlisp~ puts some effort into making sure that Lisp scripts get the
+same view of the world regardless of which implementation is running
+them.
+
+For example:
+
+  + The ~asdf~ and ~uiop~ systems are loaded and ready for use.
+
+  + The script's command-line arguments are available in
+    ~uiop:*command-line-arguments*~.  Its name can be found by calling
+    ~(uiop:argv0)~ -- though it's probably also in ~*load-pathname*~.
+
+  + The prevailing Unix standard input, output, and error files are
+    available through the Lisp ~*standard-input*~, ~*standard-output*~,
+    and ~*error-ouptut*~ streams, respectively.  (This is, alas, not a
+    foregone conclusion.)
+
+  + The keyword ~:runlisp-script~ is added to the ~*features*~ list.
+    This means that your script can tell whether it's being run from the
+    command line, and should therefore do its thing and then quit; or
+    merely being loaded into a Lisp system, e.g., for debugging or
+    development, and should sit still and not do anything until it's
+    asked.
+
+See the manual for the complete list of guarantees.
+
+
+* Invoking Lisp implementations
+
+** Basic use
+
+A secondary use of ~runlisp~ is in build scripts for Lisp programs.  If
+the entire project is just a Lisp library, then it's possibly acceptable
+to just provide an ASDF system definition and expect users to type
+~(asdf:load-system "mumble")~ to use it.  If it's a program, or there
+are things other than Lisp which ASDF can't or shouldn't handle --
+significant pieces in other languages, or a Lisp executable image to
+make and install -- then it seems sensible to make the project's main
+build system be something language-agnostic, say Unix ~make~, and
+arrange for that to invoke ASDF at the appropriate time.
+
+But how should that be arranged?  It's relatively easy for a project'
+Lisp code to support multiple Lisp implementation; but each
+implementation wants different runes for evaluating Lisp forms from the
+command line, and some of them don't provide an ideal environment for
+integrating into a build system.  So ~runlisp~ provides a simple common
+command-line interface for evaluating Lisp forms.  For example:
+
+: $ runlisp -e '(format t "~A~%" (+ 1 2))'
+: 3
+
+If your build script needs to get information out of Lisp, then wrapping
+~format~, or even ~prin1~, around forms is annoying; so ~runlisp~ has a
+~-p~ option which prints the values of the forms it evaluates.
+
+: $ runlisp -e '(+ 1 2)'
+: 3
+
+If a form produces multiple values, then ~-p~ will print all of them
+separated by spaces, on a single line:
+
+: $ runlisp -p '(floor 5 2)'
+: 2 1
+
+In addition to evaluating forms with ~-e~, and printing their values
+with ~-p~, you can also load a file of Lisp code using ~-l~.
+
+When ~runlisp~ is acting on ~-e~, ~-p~, and/or ~-l~ options, it's said
+to be running in /eval/ mode, rather than its usual /script/ mode.  In
+script mode, it /doesn't/ set ~:runlisp-script~ in ~*features*~.
+
+You can still insist that ~runlisp~ use a particular Lisp
+implementation, or one of a subset of implementations, using the ~-L~
+option mentioned above.
+
+: $ runlisp -Lsbcl -p "(lisp-implementation-type)"
+: "SBCL"
+
+** Command-line processing
+
+When scripting a Lisp -- as opposed to running a Lisp script -- it's not
+necessarily the case that your script knows in advance exactly what it
+needs to ask Lisp to do.  For example, it might need to tell Lisp to
+install a program in a particular directory, determined by Autoconf.
+While it's certainly /possible/ to quote such data and splice them into
+Lisp forms, it's more convenient to pass them in separately.  So
+~runlisp~ ensures that the command-line options are available to Lisp
+forms via ~uiop:*command-line-arguments*~, as they are to a Lisp script.
+
+: $ runlisp -p "uiop:*command-line-arguments*" one two three
+: ("one" "two" "three")
+
+When running Lisp forms like this, ~(uiop:argv0)~ isn't very
+meaningful.  (Currently, it reveals the name of the script which
+~runlisp~ uses to implement this feature.)
+
+
+* Configuring =runlisp=
+
+** Where =runlisp= looks for configuration
+
+You can influence which Lisp implementations are chosen by ~runlisp~ by
+writing a configuration file, and/or setting an environment variable.
+
+~runlisp~ looks for configuration in ~~/.runlisprc~, and in
+~~/.config/runlisprc~.  You could put configuration in both, but that
+doesn't seem like a great idea.  A configuration file just contains
+blank lines, comments, and command-line options, just as you'd write
+them to the shell.  Simple quoting and escaping is provided: see the
+manual page for the full details.  Each line is processed independently,
+so it doesn't work to write an option on one line and then its argument
+on the next.
+
+The environment variable ~RUNLISP_OPTIONS~ is processed /after/ reading
+the configuration file(s), if any.  Again, it should contain
+command-line options, as you'd write them to the shell.
+
+** Deciding which Lisp implementation to use
+
+The most useful option to use here is ~-P~, which builds up a
+/preference list/, in order.  The argument to ~-P~ is a comma-separated
+list of Lisp implementation names, just like you'd give to ~-L~.
+
+If you provide multiple ~-P~ options (e.g., on different lines of your
+configuration file, or separately in the configuration file and
+environment variable, then the lists are concatenated.  Since the
+environment variable is processed after the configuration file, this
+means that 
+
+When deciding which Lisp implementation to use, ~runlisp~ works as
+follows.  It builds a list of /acceptable/ Lisp implementations from the
+~-L~ options, and a list of /preferred/ Lisp implementations from the
+~-P~ options.  If there aren't any ~-L~ options, then it assumes that
+/all/ Lisp implementations are acceptable; but if there are no ~-P~
+options then it assumes that /no/ Lisp implementations are preferred.
+It then works through the preferred list in order: if it finds an
+implementation which is installed and acceptable, then it uses that one.
+If that doesn't work, then it works through the acceptable
+implementations that it hasn't tried yet, in order, and if it finds one
+of those that's installed, then it runs that one.  Otherwise it reports
+an error and gives up.
+
+** Clearing the preferred list
+
+Since the environment variable is processed after the configuration
+files, it can only append more Lisp implementations to the end of the
+preferred list, which may well not be so helpful.  There's an additional
+option ~-C~, which completely clears the preferred list.  The idea is
+that you can write ~-C~ at the start of your ~RUNLISP_OPTIONS~
+environment variable to temporarily override your usual configuration
+for some special effect.
+
+
+* What's wrong with =cl-launch=?
+
+The short version is that ~cl-launch~ is slow and inconvenient.
+~cl-launch~ is a big, complicated Common Lisp/Bourne shell polyglot
+which tries to do everything but doesn't quite succeed.
+
+** It's slow.
+
+I took a trivial Lisp script:
+
+: (format t "Hello from ~A!~%~
+:            Script = `~A'~%~
+:            Arguments = (~{`~A'~^, ~})~%"
+:         (lisp-implementation-type)
+:         (uiop:argv0)
+:         uiop:*command-line-arguments*)
+
+I timed how long it took to run on all of ~runlisp~'s supported Lisp
+implementations, and compared them to how long ~cl-launch~ took: the
+results are shown in table [[tab:runlisp-vanilla]].  ~runlisp~ is /at least/
+two and half times faster at running this script than ~cl-launch~ on all
+implementations except Clozure CL[fn:slow-ccl], and approaching four and
+a half times faster on SBCL.
+
+#+CAPTION: ~cl-launch~ vs ~runlisp~ (with vanilla images)
+#+NAME: tab:runlisp-vanilla
+#+ATTR_LATEX: :float t :placement [tbp]
+|------------------+-------------------+-----------------+----------------------|
+| *Implementation* | *~cl-launch~ (s)* | *~runlisp~ (s)* | *~runlisp~ (factor)* |
+|------------------+-------------------+-----------------+----------------------|
+| ABCL             |            7.3036 |          2.6027 |                2.806 |
+| Clozure CL       |            1.2769 |          0.9678 |                1.319 |
+| GNU CLisp        |            1.2498 |          0.2659 |                4.700 |
+| CMU CL           |            0.9665 |          0.3065 |                3.153 |
+| ECL              |            0.8025 |          0.3173 |                2.529 |
+| SBCL             |            0.3266 |          0.0739 |                4.419 |
+|------------------+-------------------+-----------------+----------------------|
+#+TBLFM: $4=$2/$3;%.3f
+
+But this is using the `vanilla' Lisp images installed with the
+implementations.  ~runlisp~ by default builds custom images for most
+Lisp implementations, which improves startup performance significantly;
+see table [[tab:runlisp-custom]].  (I don't currently know how to build a
+useful custom image for ABCL.  ~runlisp~ does build a custom image for
+ECL, but it doesn't help significantly.)  These results are summarized
+in figure [[fig:lisp-graph]].
+
+#+CAPTION: ~cl-launch~ vs ~runlisp~ (with custom images)
+#+NAME: tab:runlisp-custom
+#+ATTR_LATEX: :float t :placement [tbp]
+|------------------+-------------------+-----------------+----------------------|
+| *Implementation* | *~cl-launch~ (s)* | *~runlisp~ (s)* | *~runlisp~ (factor)* |
+|------------------+-------------------+-----------------+----------------------|
+| ABCL             |            7.3036 |          2.5873 |                2.823 |
+| Clozure CL       |            1.2769 |          0.0088 |              145.102 |
+| GNU CLisp        |            1.2498 |          0.0146 |               85.603 |
+| CMU CL           |            0.9665 |          0.0063 |              153.413 |
+| ECL              |            0.8025 |          0.3185 |                2.520 |
+| SBCL             |            0.3266 |          0.0077 |               42.416 |
+|------------------+-------------------+-----------------+----------------------|
+#+TBLFM: $4=$2/$3;%.3f
+
+#+CAPTION: Comparison of ~runlisp~ and ~cl-launch~ times
+#+NAME: fig:lisp-graph
+#+ATTR_LATEX: :float t :placement [tbp]
+[[file:doc/lisp-graph.tikz]]
+
+Unlike ~cl-launch~, with some Lisp implementations at least, ~runlisp~
+startup performance is usefully comparable to other popular scripting
+language implementations.  I wrote similarly trivial scripts in a number
+of other languages, and timed them; the results are tabulated in table
+[[tab:runlisp-interp]] and graphed in figure [[fig:interp-graph]].
+
+#+CAPTION: ~runlisp~ vs other interpreters
+#+NAME: tab:runlisp-interp
+#+ATTR_LATEX: :float t :placement [tbp]
+|------------------------------+-------------|
+| *Implementation*             | *Time (ms)* |
+|------------------------------+-------------|
+| Clozure CL                   |         8.8 |
+| GNU CLisp                    |        14.6 |
+| CMU CL                       |         6.3 |
+| SBCL                         |         7.7 |
+|------------------------------+-------------|
+| Perl                         |         1.2 |
+| Python                       |        10.3 |
+|------------------------------+-------------|
+| Debian Almquist shell (dash) |         1.4 |
+| GNU Bash                     |         2.0 |
+| Z Shell                      |         4.1 |
+|------------------------------+-------------|
+| Tiny C (compile & run)       |         1.2 |
+| GCC (precompiled)            |         0.5 |
+|------------------------------+-------------|
+
+#+CAPTION: Comparison of ~runlisp~ and other script interpreters
+#+NAME: fig:interp-graph
+#+Attr_latex: :float t :placement [tbp]
+[[file:doc/interp-graph.tikz]]
+
+(All the timings in this section were performed on the same 2020 Dell
+XPS13 laptop running Debian `buster'.  The tools used to make the
+measurements are included in the source distribution, in the ~bench/~
+subdirectory.)
+
+[fn:slow-ccl] I don't know why Clozure CL shows such a small difference
+here.
+
+** It's inconvenient
+
+~cl-launch~ has this elaborate machinery which reads shell script
+fragments from various places and sets variables like ~$LISPS~, but it
+doesn't quite work.
+
+Unlike other scripting languages such as Perl or Python, Common Lisp has
+lots of implementations, and they all have various unique features (and
+bugs) which a script might rely on (or need to avoid).  Also, a user
+might have preferences about which Lisps to use.  ~cl-launch~'s approach
+to this problem is a ~system_preferred_lisps~ shell function which can
+be used in ~~/.cl-launchrc~ to select a Lisp system for a particular
+`software system', though this notion doesn't appear to be well-defined,
+but this all works by editing a single ~$LISPS~ shell variable.  By
+contrast, ~runlisp~ has a ~-L~ option with which scripts can specify the
+Lisp systems they support (in a preference order), and a ~-P~ option
+with which users can express their own preferences (e.g., in the
+environment or a configuration file): ~runlisp~ will never choose a Lisp
+system which the script can't deal with, but it will respect the user's
+relative preferences.
+
+** It doesn't establish a (useful) common environment
+
+A number of Lisp systems are annoyingly deficient in their handling of
+scripts.
+
+For example, when GNU CLisp's ~-x~ option is used, it rebinds
+~*standard-input*~ to an internal string stream holding the expression
+passed in on the command line, leaving the process's actual stdin nearly
+impossible to access.
+
+: $ date | cl-launch -l sbcl -i "(princ (read-line nil nil))" # expected
+: Sun  9 Aug 14:39:10 BST 2020
+: $ date | cl-launch -l clisp -i "(princ (read-line nil nil))" # bug!
+: NIL
+
+As another example, Armed Bear Common Lisp doesn't seem to believe in
+the stderr stream: when it starts up, ~*error-ouptut*~ is bound to the
+standard output, just like ~*standard-output*~.  Also, ~cl-launch~
+loading ASDF causes a huge number of ~style-warning~ messages to be
+written to stdout, making ABCL pretty much useless for writing filter
+scripts.
+
+: $ cl-launch -l sbcl -i '(progn
+:                           (format *standard-output* "output~%")
+:                           (format *error-output* "error~%"))' \
+:   > >(sed 's/^/stdout: /') 2> >(sed 's/^/stderr: /')
+: stdout: output
+: stderr: error
+: $ cl-launch -l abcl -i '(progn
+:                           (format *standard-output* "output~%")
+:                           (format *error-output* "error~%"))' \
+:   > >(sed 's/^/stdout: /') 2> >(sed 's/^/stderr: /')
+: [1813 lines of compiler warnings tagged `stdout:']
+: stdout: output
+: stdout: error
+
+~runlisp~ takes care of all of this, providing a basic but useful common
+level of shell integration for all its supported Lisp implementations.
+In particular:
+
+  + It ensures that the standard Unix `stdin', `stdout', and `stdarr'
+    file descriptors are hooked up to the Lisp ~*standard-input*~,
+    ~*standard-output*~, and ~*error-output*~ streams.
+
+  + It ensures that starting a script doesn't write a deluge of
+    diagnostic drivel.
+
+The complete details are given in ~runlisp~'s manpage.
+
+** Why might one prefer =cl-launch= anyway?
+
+On the other hand, ~cl-launch~ is well established and full-featured.
+
+~cl-launch~ compiles scripts before trying to run them, so they'll run
+faster on Lisps which use an interpreter by default.  It has a caching
+feature so running a script a second time doesn't need to recompile it.
+If your scripts are compute-intensive and benefit from ahead-of-time
+compilation then maybe ~cl-launch~ is preferable.
+
+~cl-launch~ supports more Lisp systems.  I only have six installed on my
+development machine at the moment, so those are the ones that ~runlisp~
+supports.  If you want your scripts to be able to run on other Lisps,
+then ~cl-launch~ is the way to do that.  Of course, I welcome patches to
+help ~runlisp~ support other free Lisp implementations.  ~cl-launch~
+also supports proprietary Lisps: I have very little interest in these,
+so if you want to run scripts using Allegro or LispWorks then
+~cl-launch~ is your only choice.
diff --git a/bench/Makefile.am b/bench/Makefile.am
new file mode 100644 (file)
index 0000000..cb5eef7
--- /dev/null
@@ -0,0 +1,135 @@
+### -*-makefile-*-
+###
+### Build script for start-up benchmarks
+###
+### (c) 2020 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+###
+### Runlisp 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.
+###
+### Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+include $(top_srcdir)/vars.am
+
+GNUPLOT                         = gnuplot
+
+FORCE                   =
+FORCE:
+.PHONY: FORCE
+
+###--------------------------------------------------------------------------
+### Preliminaries.
+
+v_bench                         = $(v_bench_@AM_V@)
+v_bench_                = $(v_bench_@AM_DEFAULT_V@)
+v_bench_0               = @echo "  BENCH    $@";
+
+BENCHES                         =
+bench: $(BENCHES)
+
+noinst_PROGRAMS                += timeit
+timeit_SOURCES          = timeit.c
+
+CLEANFILES             += *.out *.bench
+
+###--------------------------------------------------------------------------
+### Lisp systems using `runlisp'.
+
+RUNLISP                         = $(top_builddir)/runlisp -I$(top_builddir)/
+EXTRA_DIST             += t.lisp
+
+RUNLISP_BENCHES                 = $(foreach l,$(LISPS), runlisp.$l.bench)
+BENCHES                         += $(RUNLISP_BENCHES)
+$(RUNLISP_BENCHES): runlisp.%.bench: timeit $(FORCE)
+       $(v_bench)./timeit $(RUNLISP) -L$* -- $(srcdir)/t.lisp a b c >runlisp.$*.out 2>$@
+
+RUNLISP_NOIMAGE_BENCHES         = $(foreach l,$(LISPS), runlisp-noimage.$l.bench)
+BENCHES                         += $(RUNLISP_NOIMAGE_BENCHES)
+$(RUNLISP_NOIMAGE_BENCHES): runlisp-noimage.%.bench: timeit $(FORCE)
+       $(v_bench)./timeit $(RUNLISP) -D -L$* -- $(srcdir)/t.lisp a b c >runlisp-noimage.$*.out 2>$@
+
+###--------------------------------------------------------------------------
+### Lisp systems using `cl-launch'.
+
+CL_LAUNCH_BENCHES       = $(foreach l,$(LISPS), cl-launch.$l.bench)
+BENCHES                         += $(CL_LAUNCH_BENCHES)
+$(CL_LAUNCH_BENCHES): cl-launch.%.bench: timeit $(FORCE)
+       $(v_bench)./timeit cl-launch -X -l $* -- $(srcdir)/t.lisp a b c >cl-launch.$*.out 2>$@
+
+###--------------------------------------------------------------------------
+### C programs (as a baseline).
+
+BENCHES                        += c.tcc.bench
+c.tcc.bench: timeit $(FORCE)
+       $(v_bench)./timeit tcc -run $(srcdir)/t.c a b c >c.tcc.out 2>$@
+
+BENCHES                        += c.gcc.bench
+noinst_PROGRAMS                += t.c.gcc
+t_c_gcc_SOURCES                 = t.c
+c.gcc.bench: t.c.gcc timeit $(FORCE)
+       $(v_bench)./timeit ./t.c.gcc a b c >c.gcc.out 2>$@
+
+###--------------------------------------------------------------------------
+### Other scripting languages.
+
+BENCHES                        += perl.bench
+EXTRA_DIST             += t.pl
+perl.bench: timeit $(FORCE)
+       $(v_bench)./timeit perl -- $(srcdir)/t.pl a b c >perl.out 2>$@
+
+BENCHES                        += python.bench
+EXTRA_DIST             += t.py
+python.bench: timeit $(FORCE)
+       $(v_bench)./timeit python -- $(srcdir)/t.py a b c >python.out 2>$@
+
+SHELLS                  = dash bash zsh
+EXTRA_DIST             += t.sh
+SHELL_BENCHES           = $(foreach s,$(SHELLS), shell.$s.bench)
+BENCHES                         += $(SHELL_BENCHES)
+$(SHELL_BENCHES): shell.%.bench: timeit $(FORCE)
+       $(v_bench)TEST_SHELL=$* ./timeit $* -- $(srcdir)/t.sh a b c >shell.$*.out 2>$@
+
+###--------------------------------------------------------------------------
+### Reporting.
+
+GRAPHS                  =
+noinst_DATA            += $(GRAPHS)
+CLEANFILES             += $(GRAPHS)
+
+v_massage               = $(v_massage_@AM_V@)
+v_massage_              = $(v_massage_@AM_DEFAULT_V@)
+v_massage_0             = @echo "  MASSAGE  $@";
+
+v_gnuplot               = $(v_gnuplot_@AM_V@)
+v_gnuplot_              = $(v_gnuplot_@AM_DEFAULT_V@)
+v_gnuplot_0             = @echo "  GNUPLOT  $@";
+
+CLEANFILES             += bench.data
+bench.data: $(BENCHES) massage-benchmarks
+       $(v_massage)$(srcdir)/massage-benchmarks >$@.new && mv $@.new $@
+
+GRAPHS                 += lisp-graph.tikz
+lisp-graph.tikz: lisp-graph.gp bench.data
+       $(v_gnuplot)$(GNUPLOT) $< >$@.new && mv $@.new $@
+
+GRAPHS                 += interp-graph.tikz
+interp-graph.tikz: interp-graph.gp bench.data
+       $(v_gnuplot)$(GNUPLOT) $< >$@.new && mv $@.new $@
+
+graphs: $(GRAPHS)
+.PHONY: graphs
+
+###----- That's all, folks --------------------------------------------------
diff --git a/bench/interp-graph.gp b/bench/interp-graph.gp
new file mode 100644 (file)
index 0000000..1066f1f
--- /dev/null
@@ -0,0 +1,15 @@
+### -*-gnuplot-*-
+
+set terminal tikz
+
+set style data histogram
+set xtic rotate by -35 offset -1, 0 scale 0
+set style fill solid
+set style histogram cluster gap 1
+
+unset key
+set border 3
+set tics nomirror
+set ylabel "Time (ms) to run trivial script"
+
+plot "bench.data" index "> interp" using (1000*$2):xtic(1)
diff --git a/bench/lisp-graph.gp b/bench/lisp-graph.gp
new file mode 100644 (file)
index 0000000..6b9550f
--- /dev/null
@@ -0,0 +1,15 @@
+### -*-gnuplot-*-
+
+set terminal tikz
+
+set style data histogram
+set xtics rotate by -35 scale 0
+set style fill solid
+set style histogram cluster
+
+set border 3
+set tics nomirror
+set ylabel "Time (s) to run trivial script"
+
+plot for [i = 2:4] "bench.data" index "> lisp" using i:xtic(1) \
+     title columnheader(i)
diff --git a/bench/massage-benchmarks b/bench/massage-benchmarks
new file mode 100755 (executable)
index 0000000..9470b84
--- /dev/null
@@ -0,0 +1,56 @@
+#! /usr/bin/perl
+
+use autodie;
+
+my %LISP =
+  ("sbcl" => "SBCL",
+   "ccl" => "Clozure CL",
+   "ecl" => "ECL",
+   "clisp" => "GNU CLisp",
+   "cmucl" => "CMU CL",
+   "abcl" => "ABCL");
+my %LABEL =
+  ("perl" => "Perl",
+   "python" => "Python",
+   "c.tcc" => "Tiny C",
+   "c.gcc" => "GCC",
+   "shell.dash" => "dash",
+   "shell.bash" => "GNU Bash",
+   "shell.zsh" => "Z Shell");
+
+for my $l (keys %LISP) { $LABEL{"runlisp.$l"} = $LISP{$l}; }
+
+{
+  my %d;
+
+  sub timing ($) {
+    my ($f) = @_;
+    return $d{$f} if exists $d{$f};
+    open my $fh, "<", "$f.bench";
+    (my $data = readline $fh) =~ s/^.* elapsed = ([0-9.]+)s.*$/$1/;
+    return $d{$f} = $data;
+  }
+}
+
+print <<EOF;
+#> lisp
+"Lisp system" "\\\\texttt{cl-launch}" "\\\\texttt{runlisp} (vanilla image)" "\\\\texttt{runlisp} (custom image)"
+EOF
+for my $l (sort keys %LISP) {
+  printf "\"%s\" %.4f %.4f %.4f\n",
+    $LISP{$l},
+    timing("cl-launch.$l"),
+    timing("runlisp-noimage.$l"),
+    timing("runlisp.$l");
+}
+print "\n\n";
+
+print <<EOF;
+#> interp
+EOF
+for my $i
+  ("runlisp.ccl", "runlisp.clisp", "runlisp.cmucl", "runlisp.sbcl",
+   "perl", "python",
+   "shell.dash", "shell.bash", "shell.zsh",
+   "c.tcc", "c.gcc")
+  { printf "\"%s\" %.4f\n", $LABEL{$i}, timing $i; }
diff --git a/bench/t.c b/bench/t.c
new file mode 100644 (file)
index 0000000..7b06527
--- /dev/null
+++ b/bench/t.c
@@ -0,0 +1,26 @@
+#include <stdio.h>
+
+#if __clang__
+#  define IMPL "Clang"
+#elif __TINYC__
+#  define IMPL "TCC"
+#elif __GNUC__
+#  define IMPL "GCC"
+#else
+#  define IMPL "an unknown C implementation"
+#endif
+
+int main(int argc, char *argv[])
+{
+  int i;
+
+  puts("Hello from " IMPL "!");
+  printf("Script = `%s'\n", argv[0]);
+  fputs("Arguments = (", stdout);
+  for (i = 1; i < argc; i++) {
+    if (i > 1) fputs(", ", stdout);
+    printf("`%s'", argv[i]);
+  }
+  putchar(')'); putchar('\n');
+  return (0);
+}
diff --git a/bench/t.lisp b/bench/t.lisp
new file mode 100755 (executable)
index 0000000..0a9010e
--- /dev/null
@@ -0,0 +1,8 @@
+#! /usr/bin/runlisp
+
+(format t "Hello from ~A!~%~
+          Script = `~A'~%~
+          Arguments = (~{`~A'~^, ~})~%"
+       (lisp-implementation-type)
+       (uiop:argv0)
+       uiop:*command-line-arguments*)
diff --git a/bench/t.pl b/bench/t.pl
new file mode 100755 (executable)
index 0000000..8997da2
--- /dev/null
@@ -0,0 +1,7 @@
+#! /usr/bin/perl
+
+printf <<EOF, $0, join ", ", map "`$_'", @ARGV;
+Hello from Perl!
+Script = `%s'
+Arguments = (%s)
+EOF
diff --git a/bench/t.py b/bench/t.py
new file mode 100755 (executable)
index 0000000..6475c0c
--- /dev/null
@@ -0,0 +1,9 @@
+#! /usr/bin/python
+
+import sys as SYS
+
+print("""\
+Hello from Python!
+Script = `%s'
+Arguments = (%s)""" %
+      (SYS.argv[0], ", ".join("`%s'" % arg for arg in SYS.argv[1:])))
diff --git a/bench/t.sh b/bench/t.sh
new file mode 100755 (executable)
index 0000000..b644622
--- /dev/null
@@ -0,0 +1,7 @@
+#! /bin/sh
+
+cat <<EOF
+Hello from ${TEST_SHELL-an unknown shell}!
+Script = $0
+Arguments = ($*)
+EOF
diff --git a/bench/timeit.c b/bench/timeit.c
new file mode 100644 (file)
index 0000000..7d8f57f
--- /dev/null
@@ -0,0 +1,80 @@
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <unistd.h>
+#include <sys/resource.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+
+static const char *progname = "timeit";
+
+static void set_progname(const char *prog)
+{
+  const char *p;
+
+  p = strrchr(prog, '/');
+  progname = p ? p + 1 : progname;
+}
+
+static void lose(const char *msg, ...)
+{
+  va_list ap;
+
+  va_start(ap, msg);
+  fprintf(stderr, "%s: ", progname);
+  vfprintf(stderr, msg, ap);
+  fputc('\n', stderr);
+  va_end(ap);
+  exit(127);
+}
+
+static double timeval_to_float(const struct timeval *tv)
+  { return (tv->tv_sec + tv->tv_usec*1e-6); }
+
+int main(int argc, char *argv[])
+{
+  struct rusage ru;
+  struct timeval t0, t1, t;
+  pid_t kid;
+  int i, st;
+
+  set_progname(argv[0]);
+  gettimeofday(&t0, 0);
+  kid = fork(); if (kid < 0) lose("fork failed: %s", strerror(errno));
+  if (!kid) {
+    execvp(argv[1], argv + 1);
+    lose("exec (`%s') failed: %s", argv[1], strerror(errno));
+  }
+  if (wait4(kid, &st, 0, &ru) < 0) lose("wait failed: %s", strerror(errno));
+  gettimeofday(&t1, 0);
+  if (st) {
+    if (WIFSIGNALED(st))
+      lose("program killed by signal %d\n", WTERMSIG(st));
+    else if (WIFEXITED(st))
+      lose("program failed with status %d\n", WEXITSTATUS(st));
+    else
+      lose("program exited with incomprehensible status 0x%04x\n", st);
+  }
+
+  if (t0.tv_usec > t1.tv_usec) {
+    t.tv_sec = t1.tv_sec - t0.tv_sec - 1;
+    t.tv_usec = t1.tv_usec + 1000000 - t0.tv_usec;
+  } else {
+    t.tv_sec = t1.tv_sec - t0.tv_sec;
+    t.tv_usec = t1.tv_usec - t0.tv_usec;
+  }
+
+  for (i = 1; i < argc; i++) {
+    if (i > 1) fputc(' ', stderr);
+    fputs(argv[i], stderr);
+  }
+  fprintf(stderr, ": elapsed = %.4fs; user = %.4fs; system = %.4fs\n",
+         timeval_to_float(&t),
+         timeval_to_float(&ru.ru_utime),
+         timeval_to_float(&ru.ru_stime));
+  return (0);
+}
diff --git a/config/auto-version b/config/auto-version
new file mode 120000 (symlink)
index 0000000..652e105
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/build/auto-version
\ No newline at end of file
diff --git a/config/confsubst b/config/confsubst
new file mode 120000 (symlink)
index 0000000..8e7de22
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/build/confsubst
\ No newline at end of file
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..042df98
--- /dev/null
@@ -0,0 +1,118 @@
+dnl -*-autoconf-*-
+dnl
+dnl Configuration script for `runlisp'
+dnl
+dnl (c) 2020 Mark Wooding
+dnl
+
+dnl----- Licensing notice ---------------------------------------------------
+dnl
+dnl This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+dnl
+dnl Runlisp is free software: you can redistribute it and/or modify it
+dnl under the terms of the GNU General Public License as published by the
+dnl Free Software Foundation; either version 3 of the License, or (at your
+dnl option) any later version.
+dnl
+dnl Runlisp is distributed in the hope that it will be useful, but WITHOUT
+dnl ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+dnl FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+dnl for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+dnl--------------------------------------------------------------------------
+dnl Initialization.
+
+mdw_AUTO_VERSION
+AC_INIT([runlisp], AUTO_VERSION, [mdw@distorted.org.uk])
+AC_CONFIG_SRCDIR([runlisp.c])
+AC_CONFIG_AUX_DIR([config])
+AM_INIT_AUTOMAKE([foreign])
+mdw_SILENT_RULES
+
+AC_PROG_CC
+AX_CFLAGS_WARN_ALL
+
+AC_CHECK_PROGS([AUTOM4TE], [autom4te])
+
+dnl--------------------------------------------------------------------------
+dnl Checking for Lisp implementations.
+
+imagedir=$localstatedir/$PACKAGE_NAME; AC_SUBST(imagedir)
+mdw_DEFINE_PATHS([
+  mdw_DEFINE_PATH([IMAGEDIR], [$imagedir])
+  mdw_DEFINE_PATH([DATADIR], [$datadir/$PACKAGE_NAME])])
+
+AC_ARG_ENABLE([imagedump],
+  [AS_HELP_STRING([--enable-imagedump[=SYSTEMS]],
+                 [make dumps of Lisp SYSTEMS with ASDF etc. preloaded;
+                  SYSTEMS is `yes', `no', or a comma-separated list of
+                  system names])],
+  [], [enable_imagedump=yes])
+
+AC_DEFUN([mdw_CHECK_LISP],
+[AC_CHECK_PROGS([$1], [$2])
+AC_ARG_VAR([$1], [Path to the $1 Lisp system.])
+case ,$enable_imagedump, in
+  ,yes, | *,$2,*) dump=t ;;
+  *) dump=nil ;;
+esac
+AM_CONDITIONAL([DUMP_$1], [test $dump = t])])
+
+mdw_CHECK_LISP([SBCL], [sbcl])
+mdw_CHECK_LISP([CCL], [ccl])
+mdw_CHECK_LISP([CLISP], [clisp])
+mdw_CHECK_LISP([ECL], [ecl])
+mdw_CHECK_LISP([CMUCL], [cmucl])
+mdw_CHECK_LISP([ABCL], [abcl])
+
+dnl ECL is changing its command-line option syntax, because that will make
+dnl things much better or something.  So we need to figure out which version
+dnl of the syntax to use.
+mdw_ecl_opts=hunoz
+if test "x$ECL" != x; then
+  AC_MSG_CHECKING([ECL command-line option flavour])
+  ver=$($ECL --version)
+  case $ver in
+    [ECL\ [0-9].*] | [ECL\ 1[0-5].*]) mdw_ecl_opts=trad ;;
+    [ECL\ 1[6-9].*] | [ECL\ [2-9][0-9].*]) mdw_ecl_opts=gnu ;;
+    *) AC_MSG_ERROR([unsupported ECL version \`$ver']) ;;
+  esac
+  AC_MSG_RESULT([$mdw_ecl_opts])
+  case $mdw_ecl_opts in
+    gnu) AC_DEFINE([ECL_OPTIONS_GNU], [1],
+                  [Define 1 if ECL uses GNU-style `--FOO' options]) ;;
+  esac
+fi
+case $mdw_ecl_opts in
+  gnu) ECLOPT=-- ;;
+  trad) ECLOPT=- ;;
+  *) AC_MSG_ERROR([internal error: unexpected value for `$mdw_ecl_opts']) ;;
+esac
+AC_SUBST([ECLOPT])
+
+dnl--------------------------------------------------------------------------
+dnl Benchmarking support.
+
+dnl This has lots of random dependencies, and isn't really very useful.  Turn
+dnl it off unless the user is very keen.
+AC_ARG_ENABLE([benchmark],
+             [AS_HELP_STRING([--enable-benchmark],
+                             [turn on script-startup benchmark machinery])],
+             [mdw_bench=$enableval], [mdw_bench=no])
+AM_CONDITIONAL([BENCHMARK], [test "$mdw_bench" = yes])
+
+dnl--------------------------------------------------------------------------
+dnl Produce output.
+
+AC_CONFIG_HEADER([config/config.h])
+AC_CONFIG_TESTDIR([t])
+
+AC_CONFIG_FILES([Makefile]
+               [bench/Makefile doc/Makefile]
+               [t/Makefile t/atlocal])
+AC_OUTPUT
+
+dnl----- That's all, folks --------------------------------------------------
diff --git a/doc/Makefile.am b/doc/Makefile.am
new file mode 100644 (file)
index 0000000..6bab640
--- /dev/null
@@ -0,0 +1,32 @@
+### -*-makefile-*-
+###
+### Additional documentation files
+###
+### (c) 2020 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+###
+### Runlisp 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.
+###
+### Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+include $(top_srcdir)/vars.am
+
+EXTRA_DIST             += bench.data
+
+EXTRA_DIST             += lisp-graph.tikz
+EXTRA_DIST             += interp-graph.tikz
+
+###----- That's all, folks --------------------------------------------------
diff --git a/doc/bench.data b/doc/bench.data
new file mode 100644 (file)
index 0000000..25d0a83
--- /dev/null
@@ -0,0 +1,22 @@
+#> lisp
+"Lisp system" "\\texttt{cl-launch}" "\\texttt{runlisp} (vanilla image)" "\\texttt{runlisp} (custom image)"
+"ABCL" 7.3036 2.6027 2.5873 
+"Clozure CL" 1.2769 0.9678 0.0088 
+"GNU CLisp" 1.2498 0.2659 0.0146 
+"CMU CL" 0.9665 0.3065 0.0063 
+"ECL" 0.8025 0.3173 0.3185 
+"SBCL" 0.3266 0.0739 0.0077 
+
+
+#> interp
+"Clozure CL" 0.0088
+"GNU CLisp" 0.0146
+"CMU CL" 0.0063
+"SBCL" 0.0077
+"Perl" 0.0012
+"Python" 0.0103
+"dash" 0.0014
+"GNU Bash" 0.0020
+"Z Shell" 0.0041
+"Tiny C" 0.0012
+"GCC" 0.0005
diff --git a/doc/interp-graph.tikz b/doc/interp-graph.tikz
new file mode 100644 (file)
index 0000000..ac39f8a
--- /dev/null
@@ -0,0 +1,68 @@
+\begin{tikzpicture}[gnuplot]
+%% generated with GNUPLOT 5.2p6 (Lua 5.3; terminal rev. Nov 2018, script rev. 107)
+%% Sat 15 Aug 2020 14:07:28 BST
+\path (0.000,0.000) rectangle (12.500,8.750);
+\gpcolor{color=gp lt color border}
+\gpsetlinetype{gp lt border}
+\gpsetdashtype{gp dt solid}
+\gpsetlinewidth{1.00}
+\draw[gp path] (1.136,1.363)--(1.316,1.363);
+\node[gp node right] at (0.952,1.363) {$0$};
+\draw[gp path] (1.136,2.248)--(1.316,2.248);
+\node[gp node right] at (0.952,2.248) {$2$};
+\draw[gp path] (1.136,3.133)--(1.316,3.133);
+\node[gp node right] at (0.952,3.133) {$4$};
+\draw[gp path] (1.136,4.017)--(1.316,4.017);
+\node[gp node right] at (0.952,4.017) {$6$};
+\draw[gp path] (1.136,4.902)--(1.316,4.902);
+\node[gp node right] at (0.952,4.902) {$8$};
+\draw[gp path] (1.136,5.787)--(1.316,5.787);
+\node[gp node right] at (0.952,5.787) {$10$};
+\draw[gp path] (1.136,6.672)--(1.316,6.672);
+\node[gp node right] at (0.952,6.672) {$12$};
+\draw[gp path] (1.136,7.556)--(1.316,7.556);
+\node[gp node right] at (0.952,7.556) {$14$};
+\draw[gp path] (1.136,8.441)--(1.316,8.441);
+\node[gp node right] at (0.952,8.441) {$16$};
+\node[gp node left,rotate=-35] at (1.853,1.179) {Clozure CL};
+\node[gp node left,rotate=-35] at (2.754,1.179) {GNU CLisp};
+\node[gp node left,rotate=-35] at (3.655,1.179) {CMU CL};
+\node[gp node left,rotate=-35] at (4.556,1.179) {SBCL};
+\node[gp node left,rotate=-35] at (5.457,1.179) {Perl};
+\node[gp node left,rotate=-35] at (6.358,1.179) {Python};
+\node[gp node left,rotate=-35] at (7.258,1.179) {dash};
+\node[gp node left,rotate=-35] at (8.159,1.179) {GNU Bash};
+\node[gp node left,rotate=-35] at (9.060,1.179) {Z Shell};
+\node[gp node left,rotate=-35] at (9.961,1.179) {Tiny C};
+\node[gp node left,rotate=-35] at (10.862,1.179) {GCC};
+\draw[gp path] (1.136,8.441)--(1.136,1.363)--(11.947,1.363);
+\node[gp node center,rotate=-270] at (0.276,4.902) {Time (ms) to run trivial script};
+\gpfill{rgb color={0.580,0.000,0.827}} (1.812,1.363)--(2.263,1.363)--(2.263,5.257)--(1.812,5.257)--cycle;
+\gpcolor{rgb color={0.580,0.000,0.827}}
+\draw[gp path] (1.812,1.363)--(1.812,5.256)--(2.262,5.256)--(2.262,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (2.713,1.363)--(3.164,1.363)--(3.164,7.823)--(2.713,7.823)--cycle;
+\draw[gp path] (2.713,1.363)--(2.713,7.822)--(3.163,7.822)--(3.163,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (3.614,1.363)--(4.065,1.363)--(4.065,4.151)--(3.614,4.151)--cycle;
+\draw[gp path] (3.614,1.363)--(3.614,4.150)--(4.064,4.150)--(4.064,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (4.514,1.363)--(4.966,1.363)--(4.966,4.770)--(4.514,4.770)--cycle;
+\draw[gp path] (4.514,1.363)--(4.514,4.769)--(4.965,4.769)--(4.965,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (5.415,1.363)--(5.867,1.363)--(5.867,1.895)--(5.415,1.895)--cycle;
+\draw[gp path] (5.415,1.363)--(5.415,1.894)--(5.866,1.894)--(5.866,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (6.316,1.363)--(6.768,1.363)--(6.768,5.920)--(6.316,5.920)--cycle;
+\draw[gp path] (6.316,1.363)--(6.316,5.919)--(6.767,5.919)--(6.767,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (7.217,1.363)--(7.669,1.363)--(7.669,1.983)--(7.217,1.983)--cycle;
+\draw[gp path] (7.217,1.363)--(7.217,1.982)--(7.668,1.982)--(7.668,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (8.118,1.363)--(8.570,1.363)--(8.570,2.249)--(8.118,2.249)--cycle;
+\draw[gp path] (8.118,1.363)--(8.118,2.248)--(8.569,2.248)--(8.569,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (9.019,1.363)--(9.470,1.363)--(9.470,3.178)--(9.019,3.178)--cycle;
+\draw[gp path] (9.019,1.363)--(9.019,3.177)--(9.469,3.177)--(9.469,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (9.920,1.363)--(10.371,1.363)--(10.371,1.895)--(9.920,1.895)--cycle;
+\draw[gp path] (9.920,1.363)--(9.920,1.894)--(10.370,1.894)--(10.370,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (10.821,1.363)--(11.272,1.363)--(11.272,1.585)--(10.821,1.585)--cycle;
+\draw[gp path] (10.821,1.363)--(10.821,1.584)--(11.271,1.584)--(11.271,1.363)--cycle;
+\gpcolor{color=gp lt color border}
+\draw[gp path] (1.136,8.441)--(1.136,1.363)--(11.947,1.363);
+%% coordinates of the plot area
+\gpdefrectangularnode{gp plot 1}{\pgfpoint{1.136cm}{1.363cm}}{\pgfpoint{11.947cm}{8.441cm}}
+\end{tikzpicture}
+%% gnuplot variables
diff --git a/doc/lisp-graph.tikz b/doc/lisp-graph.tikz
new file mode 100644 (file)
index 0000000..2a6ea13
--- /dev/null
@@ -0,0 +1,90 @@
+\begin{tikzpicture}[gnuplot]
+%% generated with GNUPLOT 5.2p6 (Lua 5.3; terminal rev. Nov 2018, script rev. 107)
+%% Sat 15 Aug 2020 14:07:24 BST
+\path (0.000,0.000) rectangle (12.500,8.750);
+\gpcolor{color=gp lt color border}
+\gpsetlinetype{gp lt border}
+\gpsetdashtype{gp dt solid}
+\gpsetlinewidth{1.00}
+\draw[gp path] (0.952,1.363)--(1.132,1.363);
+\node[gp node right] at (0.768,1.363) {$0$};
+\draw[gp path] (0.952,2.248)--(1.132,2.248);
+\node[gp node right] at (0.768,2.248) {$1$};
+\draw[gp path] (0.952,3.133)--(1.132,3.133);
+\node[gp node right] at (0.768,3.133) {$2$};
+\draw[gp path] (0.952,4.017)--(1.132,4.017);
+\node[gp node right] at (0.768,4.017) {$3$};
+\draw[gp path] (0.952,4.902)--(1.132,4.902);
+\node[gp node right] at (0.768,4.902) {$4$};
+\draw[gp path] (0.952,5.787)--(1.132,5.787);
+\node[gp node right] at (0.768,5.787) {$5$};
+\draw[gp path] (0.952,6.672)--(1.132,6.672);
+\node[gp node right] at (0.768,6.672) {$6$};
+\draw[gp path] (0.952,7.556)--(1.132,7.556);
+\node[gp node right] at (0.768,7.556) {$7$};
+\draw[gp path] (0.952,8.441)--(1.132,8.441);
+\node[gp node right] at (0.768,8.441) {$8$};
+\node[gp node left,rotate=-35] at (2.523,1.179) {ABCL};
+\node[gp node left,rotate=-35] at (4.093,1.179) {Clozure CL};
+\node[gp node left,rotate=-35] at (5.664,1.179) {GNU CLisp};
+\node[gp node left,rotate=-35] at (7.235,1.179) {CMU CL};
+\node[gp node left,rotate=-35] at (8.806,1.179) {ECL};
+\node[gp node left,rotate=-35] at (10.376,1.179) {SBCL};
+\draw[gp path] (0.952,8.441)--(0.952,1.363)--(11.947,1.363);
+\node[gp node center,rotate=-270] at (0.276,4.902) {Time (s) to run trivial script};
+\node[gp node right] at (10.479,8.107) {\texttt{cl-launch}};
+\gpfill{rgb color={0.580,0.000,0.827}} (10.663,8.030)--(11.579,8.030)--(11.579,8.184)--(10.663,8.184)--cycle;
+\gpcolor{rgb color={0.580,0.000,0.827}}
+\draw[gp path] (10.663,8.030)--(11.579,8.030)--(11.579,8.184)--(10.663,8.184)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (2.209,1.363)--(2.524,1.363)--(2.524,7.826)--(2.209,7.826)--cycle;
+\draw[gp path] (2.209,1.363)--(2.209,7.825)--(2.523,7.825)--(2.523,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (3.779,1.363)--(4.094,1.363)--(4.094,2.494)--(3.779,2.494)--cycle;
+\draw[gp path] (3.779,1.363)--(3.779,2.493)--(4.093,2.493)--(4.093,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (5.350,1.363)--(5.665,1.363)--(5.665,2.470)--(5.350,2.470)--cycle;
+\draw[gp path] (5.350,1.363)--(5.350,2.469)--(5.664,2.469)--(5.664,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (6.921,1.363)--(7.236,1.363)--(7.236,2.219)--(6.921,2.219)--cycle;
+\draw[gp path] (6.921,1.363)--(6.921,2.218)--(7.235,2.218)--(7.235,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (8.491,1.363)--(8.807,1.363)--(8.807,2.074)--(8.491,2.074)--cycle;
+\draw[gp path] (8.491,1.363)--(8.491,2.073)--(8.806,2.073)--(8.806,1.363)--cycle;
+\gpfill{rgb color={0.580,0.000,0.827}} (10.062,1.363)--(10.377,1.363)--(10.377,1.653)--(10.062,1.653)--cycle;
+\draw[gp path] (10.062,1.363)--(10.062,1.652)--(10.376,1.652)--(10.376,1.363)--cycle;
+\gpcolor{color=gp lt color border}
+\node[gp node right] at (10.479,7.799) {\texttt{runlisp} (vanilla image)};
+\gpfill{rgb color={0.000,0.620,0.451}} (10.663,7.722)--(11.579,7.722)--(11.579,7.876)--(10.663,7.876)--cycle;
+\gpcolor{rgb color={0.000,0.620,0.451}}
+\draw[gp path] (10.663,7.722)--(11.579,7.722)--(11.579,7.876)--(10.663,7.876)--cycle;
+\gpfill{rgb color={0.000,0.620,0.451}} (2.523,1.363)--(2.838,1.363)--(2.838,3.667)--(2.523,3.667)--cycle;
+\draw[gp path] (2.523,1.363)--(2.523,3.666)--(2.837,3.666)--(2.837,1.363)--cycle;
+\gpfill{rgb color={0.000,0.620,0.451}} (4.093,1.363)--(4.409,1.363)--(4.409,2.220)--(4.093,2.220)--cycle;
+\draw[gp path] (4.093,1.363)--(4.093,2.219)--(4.408,2.219)--(4.408,1.363)--cycle;
+\gpfill{rgb color={0.000,0.620,0.451}} (5.664,1.363)--(5.979,1.363)--(5.979,1.599)--(5.664,1.599)--cycle;
+\draw[gp path] (5.664,1.363)--(5.664,1.598)--(5.978,1.598)--(5.978,1.363)--cycle;
+\gpfill{rgb color={0.000,0.620,0.451}} (7.235,1.363)--(7.550,1.363)--(7.550,1.635)--(7.235,1.635)--cycle;
+\draw[gp path] (7.235,1.363)--(7.235,1.634)--(7.549,1.634)--(7.549,1.363)--cycle;
+\gpfill{rgb color={0.000,0.620,0.451}} (8.806,1.363)--(9.121,1.363)--(9.121,1.645)--(8.806,1.645)--cycle;
+\draw[gp path] (8.806,1.363)--(8.806,1.644)--(9.120,1.644)--(9.120,1.363)--cycle;
+\gpfill{rgb color={0.000,0.620,0.451}} (10.376,1.363)--(10.691,1.363)--(10.691,1.429)--(10.376,1.429)--cycle;
+\draw[gp path] (10.376,1.363)--(10.376,1.428)--(10.690,1.428)--(10.690,1.363)--cycle;
+\gpcolor{color=gp lt color border}
+\node[gp node right] at (10.479,7.491) {\texttt{runlisp} (custom image)};
+\gpfill{rgb color={0.337,0.706,0.914}} (10.663,7.414)--(11.579,7.414)--(11.579,7.568)--(10.663,7.568)--cycle;
+\gpcolor{rgb color={0.337,0.706,0.914}}
+\draw[gp path] (10.663,7.414)--(11.579,7.414)--(11.579,7.568)--(10.663,7.568)--cycle;
+\gpfill{rgb color={0.337,0.706,0.914}} (2.837,1.363)--(3.152,1.363)--(3.152,3.653)--(2.837,3.653)--cycle;
+\draw[gp path] (2.837,1.363)--(2.837,3.652)--(3.151,3.652)--(3.151,1.363)--cycle;
+\gpfill{rgb color={0.337,0.706,0.914}} (4.408,1.363)--(4.723,1.363)--(4.723,1.372)--(4.408,1.372)--cycle;
+\draw[gp path] (4.408,1.363)--(4.408,1.371)--(4.722,1.371)--(4.722,1.363)--cycle;
+\gpfill{rgb color={0.337,0.706,0.914}} (5.978,1.363)--(6.293,1.363)--(6.293,1.377)--(5.978,1.377)--cycle;
+\draw[gp path] (5.978,1.363)--(5.978,1.376)--(6.292,1.376)--(6.292,1.363)--cycle;
+\gpfill{rgb color={0.337,0.706,0.914}} (7.549,1.363)--(7.864,1.363)--(7.864,1.370)--(7.549,1.370)--cycle;
+\draw[gp path] (7.549,1.363)--(7.549,1.369)--(7.863,1.369)--(7.863,1.363)--cycle;
+\gpfill{rgb color={0.337,0.706,0.914}} (9.120,1.363)--(9.435,1.363)--(9.435,1.646)--(9.120,1.646)--cycle;
+\draw[gp path] (9.120,1.363)--(9.120,1.645)--(9.434,1.645)--(9.434,1.363)--cycle;
+\gpfill{rgb color={0.337,0.706,0.914}} (10.690,1.363)--(11.006,1.363)--(11.006,1.371)--(10.690,1.371)--cycle;
+\draw[gp path] (10.690,1.363)--(10.690,1.370)--(11.005,1.370)--(11.005,1.363)--cycle;
+\gpcolor{color=gp lt color border}
+\draw[gp path] (0.952,8.441)--(0.952,1.363)--(11.947,1.363);
+%% coordinates of the plot area
+\gpdefrectangularnode{gp plot 1}{\pgfpoint{0.952cm}{1.363cm}}{\pgfpoint{11.947cm}{8.441cm}}
+\end{tikzpicture}
+%% gnuplot variables
diff --git a/dump-runlisp-image.1 b/dump-runlisp-image.1
new file mode 100644 (file)
index 0000000..d6bd3f5
--- /dev/null
@@ -0,0 +1,272 @@
+.\" -*-nroff-*-
+.\"
+.\" Manual for `dump-runlisp-image'
+.\"
+.\" (c) 2020 Mark Wooding
+.\"
+.
+.\"----- Licensing notice ---------------------------------------------------
+.\"
+.\" This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+.\"
+.\" Runlisp 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.
+.\"
+.\" Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+.
+.ie t \{\
+.  ds o \(bu
+.  if \n(.g \{\
+.    fam P
+.    ev an-1
+.    fam P
+.    ev
+.  \}
+.\}
+.el \{\
+.  ds o o
+.\}
+.
+.de hP
+.IP
+\h'-\w'\fB\\$1\ \fP'u'\fB\\$1\ \fP\c
+..
+.ds , \h'.16667m'
+.
+.\"--------------------------------------------------------------------------
+.TH runlisp 1 "12 August 2020" "Mark Wooding"
+.SH NAME
+dump-runlisp-image \- dump Lisp images for faster script execution
+.
+.\"--------------------------------------------------------------------------
+.SH SYNOPSIS
+.
+.B dump-runlisp-image
+.RB [ \-acluv ]
+.RB [ \-o
+.IR output ]
+.RI [ lisp
+\&...]
+.
+.\"--------------------------------------------------------------------------
+.SH DESCRIPTION
+.
+The
+.B dump-runlisp-image
+program builds custom images for use by
+.BR runlisp (1).
+For many Lisp implementation,
+a custom image,
+with ASDF already loaded,
+can start scripts much more quickly
+than the `vanilla' images installed by deafult.
+The downside is that custom images may be rather large.
+.
+.SS "Supperted Common Lisp implementations"
+The following Lisp implementations are currently supported.
+.TP
+.B "ccl"
+Clozure Common Lisp.
+The default image name is
+.BR ccl+asdf.image ;
+a typical image can be 20\(en30\*,MB in size.
+.TP
+.B "clisp"
+GNU CLisp.
+The default image name is
+.BR clisp+asdf.mem ;
+a typical image is about 10\*,MB in size.
+.TP
+.B "cmucl"
+Carnegie\(enMellon University Common Lisp.
+The default image name is
+.BR cmucl+asdf.core ;
+a typical image is about 35\*,MB in size.
+.TP
+.B "ecl"
+Embeddable Common Lisp.
+The default image name is
+.BR ecl+asdf ;
+images comparatively very small
+\(en about 4\*,MB \(en
+but, sadly, not very effective.
+.TP
+.B "sbcl"
+Steel Bank Common Lisp.
+The default image name is
+.BR sbcl+asdf.core ;
+a typical image is nearly 45\*,MB in size.
+.PP
+(Although
+.BR runlisp (3)
+also supports Armed Bear Common Lisp,
+.B dump-runlisp-image
+currently doesn't.)
+.
+.SS "Options"
+The following options are accepted on the command line.
+.
+.TP
+.B "\-h"
+Write a synopsis of
+.BR dump-runlisp-image 's
+command-line syntax
+and a description of the command-line options
+to standard output
+and immediately exit with status 0.
+.
+.TP
+.B "\-v"
+Write
+.BR dump-runlisp-image 's
+version number
+to standard output
+and immediately exit with status 0.
+.
+.TP
+.B "\-a"
+Dump images for all Lisp supported implementations
+which are installed .
+This implies
+.BR \-c ,
+described below.
+You can't set
+.B \-a
+and also list implementations explicitly on the command line.
+.
+.TP
+.B "\-c"
+Only dump images for Lisp implementations
+which are actually installed
+(and can be found).
+.
+.TP
+.B "\-l"
+List the supported implementations
+and the names of the image files for each
+to standard output,
+and immediately exit with status 0.
+.
+.TP
+.BI "\-o " output
+If
+.I output
+names a directory,
+then write images to that directory
+with their default names
+(as listed above).
+Otherwise,
+exactly one Lisp implementation may be named, and
+the image is written to a file named
+.IR output .
+By default,
+images are written to the directory which
+.BR runlisp (1)
+will look in when checking for custom images
+(shown in
+.B "runlisp \-\-help"
+or
+.BR "dump-runlisp-image \-h" ),
+unless overridden by the
+.B RUNLISP_IMAGEDIR
+environment variable.
+.
+.TP
+.BI "\-u"
+Don't create Lisp images
+if a file with the appropriate name
+already exists.
+.
+.TP
+.BI "\-v"
+Be more verbose about the process of creating images.
+Lisp implementations can be rather noisy:
+by default,
+.B dump-runlisp-image
+runs silently unless something goes wrong,
+in which case it prints the failed Lisp command line
+and its output.
+If you set
+.B \-v
+then
+.B dump-runlisp-image
+will show Lisp implementation's noise immediately,
+without waiting to see whether it succeeds or fails.
+.PP
+The
+.B dump-runlisp-image
+program will dump an image for each of the named
+.I lisp
+implementations in turn,
+or all Lisp implementations, if
+.B \-a
+is set.
+.PP
+This involves invoking the Lisp implementations.
+The
+.B dump-runlisp-image
+program expects, by default,
+to be able to run a Lisp system
+as a program with the same name,
+found by searching as directed by the
+.B PATH
+environment variable.
+This can be overridden by setting an environment variable,
+with the same name but in
+.IR "upper case" ,
+to the actual name \(en
+either a bare filename to be searched for on the
+.BR PATH ,
+or a pathname containing a
+.RB ` / ',
+relative to the working directory or absolute,
+to the program.
+Note that the entire variable value is used as the program name:
+it's not possible to provide custom arguments to a Lisp system
+using this mechanism.
+If you want to do that,
+you must write a shell script to do the necessary work,
+and point the environment variable
+(or the
+.BR PATH )
+at your script.
+(This is the same convention as
+.BR runlisp (1).)
+.PP
+If
+.B \-a
+or
+.B \-c
+is set,
+then
+.B dump-runlisp-image
+will skip Lisp implementations
+which can't actually be found
+(by searching the
+.B PATH
+for its command name).
+.
+.\"--------------------------------------------------------------------------
+.
+.SH "BUGS"
+.hP \*o
+There's no support for making images for ABCL.
+I don't really know what this would look like,
+but I suspect it wouldn't help very much.
+ABCL is terribly slow to start up anyway.
+.
+.SH "SEE ALSO"
+.BR runlisp (1).
+.
+.SH "AUTHOR"
+Mark Wooding, <mdw@distorted.org.uk>
+.
+.\"----- That's all, folks --------------------------------------------------
diff --git a/dump-runlisp-image.in b/dump-runlisp-image.in
new file mode 100644 (file)
index 0000000..a4bf87e
--- /dev/null
@@ -0,0 +1,390 @@
+#! /bin/sh -e
+###
+### Dump Lisp images for faster script execution
+###
+### (c) 2020 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+###
+### Runlisp 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.
+###
+### Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+###--------------------------------------------------------------------------
+### Build-time configuration.
+
+VERSION=@VERSION@
+imagedir=@imagedir@
+eclopt=@ECLOPT@
+
+###--------------------------------------------------------------------------
+### Random utilities.
+
+prog=${0##*/}
+
+## Report a fatal error.
+lose () { echo >&2 "$prog: $*"; exit 2; }
+
+## Quote a string so that Lisp will understand it.
+lisp_quote () { printf "%s\n" "$1" | sed 's/[\\"]/\\&/g'; }
+
+## Mention that we're running a program.
+run () { echo "$*"; $lbuf "$@"; }
+
+## Figure out whether we can force line-buffering.
+if stdbuf --version >/dev/null 2>&1; then lbuf="stdbuf -oL --"
+else lbuf=""; fi
+
+## Copy stdin to stdout, one line at a time.  This is important in the shell
+## game below, to prevent lines from two incoming streams being interleaved
+## in the log file.
+copy () { while IFS= read -r line; do printf "%s %s\n" "$1" "$line"; done; }
+
+###--------------------------------------------------------------------------
+### Lisp runes.
+
+## Load and upgrade ASDF.
+load_asdf_rune="(require \"asdf\")"
+upgrade_asdf_rune="(asdf:upgrade-asdf)"
+
+## Ignore `#!' lines.  (We force this so as to provide a uniform environment,
+## even though some Lisp implementations take special action when they know
+## they're running scripts.)
+ignore_shebang_rune="\
+(set-dispatch-macro-character
+ #\\# #\\!
+ (lambda (stream #1=#:char #2=#:arg)
+   (declare (ignore #1# #2#))
+   (values (read-line stream))))"
+
+## Push `:runlisp-script' into the `*features*' list.
+set_script_feature_rune="(pushnew :runlisp-script *features*)"
+
+## All of the above.
+common_prelude_rune="\
+(progn
+  $upgrade_asdf_rune
+  $ignore_shebang_rune
+  $set_script_feature_rune)"
+
+###--------------------------------------------------------------------------
+### Explain how to dump the various Lisp systems.
+
+## Maintain the master tables.
+unset lisps
+deflisp () { lisps=${lisps+$lisps }$1; eval ${1}_image=\$2; }
+
+## Steel Bank Common Lisp.
+deflisp sbcl sbcl+asdf.core
+dump_sbcl () {
+  image=$(lisp_quote "$1")
+  run "${SBCL-sbcl}" --noinform --no-userinit --no-sysinit \
+    --disable-debugger \
+    --eval "$load_asdf_rune" \
+    --eval "$common_prelude_rune" \
+    --eval "(sb-ext:save-lisp-and-die \"$image\")"
+}
+
+## Clozure Common Lisp.
+deflisp ccl ccl+asdf.image
+dump_ccl () {
+  image=$(lisp_quote "$1")
+  ## A snaglet occurs here.  CCL wants to use the image name as a clue to
+  ## where the rest of its installation is; but in fact the image is
+  ## nowhere near its installation.  So we must hack...
+
+  run "${CCL-ccl}" -b -n -Q \
+    -e "$load_asdf_rune" \
+    -e "$common_prelude_rune" \
+    -e "(ccl::in-development-mode
+         (let ((#1=#:real-ccl-dir (ccl::ccl-directory)))
+           (defun ccl::ccl-directory ()
+             (let* ((#2=#:dirpath (ccl:getenv \"CCL_DEFAULT_DIRECTORY\")))
+               (if (and #2# (plusp (length (namestring #2#))))
+                   (ccl::native-to-directory-pathname #2#)
+                   #1#))))
+         (compile 'ccl::ccl-directory))" \
+    -e "(ccl:save-application \"$image\"
+         :init-file nil
+         :error-handler :quit)"
+}
+
+## GNU CLisp.
+deflisp clisp clisp+asdf.mem
+dump_clisp () {
+  image=$(lisp_quote "$1")
+  run "${CLISP-clisp}" -norc -q -q \
+    -x "$load_asdf_rune" \
+    -x "$common_prelude_rune" \
+    -x "(ext:saveinitmem \"$image\"
+         :norc t
+         :script t)" \
+    -- wrong arguments
+}
+
+## Embeddable Common Lisp.
+deflisp ecl ecl+asdf
+dump_ecl () {
+  image=$1
+  set -e
+
+  ## Start by compiling a copy of ASDF.
+  cat >"$tmp/ecl-build.lisp" <<EOF
+(require "asdf")
+
+(defparameter *asdf* (asdf:find-system "asdf"))
+
+(defun right-here (pathname pattern)
+  (declare (ignore pattern))
+  (merge-pathnames
+   (make-pathname :name (concatenate 'string
+                                    (string-downcase
+                                     (lisp-implementation-type))
+                                    "-"
+                                    (pathname-name pathname))
+                 :type nil
+                 :version nil
+                 :defaults *default-pathname-defaults*)
+   pathname))
+(asdf:initialize-output-translations '(:output-translations
+                                      ((#p"/" :**/ :*.*.*)
+                                       (:function right-here))
+                                      :ignore-inherited-configuration))
+
+(asdf:operate 'asdf:lib-op *asdf*)
+(si:quit 0)
+EOF
+  (cd "$tmp" && run "${ECL-ecl}" ${eclopt}norc ${eclopt}load "ecl-build.lisp")
+
+  ## And now compile our driver code.
+  cat >"$tmp/ecl-run.lisp" <<EOF
+(cl:defpackage #:runlisp
+  (:use #:common-lisp))
+(cl:in-package #:runlisp)
+
+(defun main ()
+  $ignore_shebang_rune
+  (asdf:register-immutable-system "asdf")
+  (let ((pkg (find-package "COMMON-LISP-USER")))
+    (with-package-iterator (next pkg :internal)
+      (loop (multiple-value-bind (anyp sym how) (next)
+             (declare (ignore how))
+             (unless anyp (return))
+             (unintern sym pkg)))))
+  $set_script_feature_rune
+  (let ((winning t) (script nil) (marker nil)
+       (prog (file-namestring (si:argv 0))) (i 1) (argc (si:argc)))
+    (labels ((lose (msg &rest args)
+              (format *error-output* "~&~A: ~?~%" prog msg args)
+              (setf winning nil))
+            (quit (rc)
+              (si:quit rc))
+            (usage (stream)
+              (format stream "~&usage: ~A -s SCRIPT -- ARGS~%"
+                      prog))
+            (getarg ()
+              (and (< i argc) (prog1 (si:argv i) (incf i)))))
+      (loop (let ((arg (getarg)))
+             (cond ((null arg) (return))
+                   ((string= arg "--") (setf marker t) (return))
+                   ((string= arg "-s") (setf script (getarg)))
+                   ((string= arg "-h") (usage *standard-output*) (quit 0))
+                   (t (lose "unrecognized option \`~A'" arg)))))
+      (unless script (lose "nothing to do"))
+      (unless marker (lose "unexpected end of options (missing \`--'?)"))
+      (unless winning (usage *error-output*) (quit 255))
+      (handler-case
+         (let ((*package* (find-package "COMMON-LISP-USER")))
+           (load script :verbose nil :print nil))
+       (error (err)
+         (format *error-output* "~&~A (uncaught error): ~A~%" prog err)
+         (quit 255)))
+      (quit 0))))
+(main)
+EOF
+  (cd "$tmp" && run "${ECL-ecl}" ${eclopt}norc ${eclopt}load "ecl-asdf.fas" \
+    -s -o "ecl-run.o" ${eclopt}compile "ecl-run.lisp")
+
+  ## Finally link everything together.
+  run "${ECL-ecl}" ${eclopt}norc -o "$image"\
+    ${eclopt}link "$tmp/ecl-asdf.o" "$tmp/ecl-run.o"
+}
+
+## Carnegie--Mellon University Common Lisp.
+deflisp cmucl cmucl+asdf.core
+dump_cmucl () {
+  image=$(lisp_quote "$1")
+  run "${CMUCL-cmucl}" -batch -noinit -nositeinit -quiet \
+    -eval "$load_asdf_rune" \
+    -eval "$common_prelude_rune" \
+    -eval "(ext:save-lisp \"$image\"
+            :batch-mode t :print-herald nil
+            :site-init nil :load-init-file nil)"
+}
+
+###--------------------------------------------------------------------------
+### Command-line processing.
+
+usage () { echo "usage: $prog [-acluv] [-o FILE] [LISP ...]"; }
+version () { echo "$prog, runlisp version $VERSION"; }
+help () {
+  version; echo; usage; cat <<EOF
+
+Options:
+  -h                   Show this help text and exit successfully.
+  -V                   Show the version number and exit successfully.
+  -a                   Dump all installed Lisp implementations.
+  -c                   Check that Lisp systems are installed before
+                         trying to dump.
+  -l                   List known Lisp systems and default image filenames.
+  -o OUT               Store images in OUT (file or directory); default
+                         is \`\$RUNLISP_IMAGEDIR' or \`$imagedir'
+  -u                   Only dump images which don't exist already.
+  -v                   Be verbose, even if things go well.
+EOF
+}
+
+unset outfile; dir=${RUNLISP_IMAGEDIR-$imagedir}; dir=${dir%/}/
+all=nil checkinst=nil bogus=nil out=nil update=nil verbose=nil
+
+## Parse the options.
+while getopts "hVaclo:uv" opt; do
+  case $opt in
+    h) help; exit 0 ;;
+    V) version; exit 0 ;;
+    a) all=t checkinst=t ;;
+    l)
+      for i in $lisps; do
+       eval out=\$${i}_image
+       echo "$i -> $out"
+      done
+      exit 0
+      ;;
+    o) outfile=$OPTARG out=t; dir= ;;
+    u) update=t ;;
+    v) verbose=t ;;
+    *) bogus=t ;;
+  esac
+done
+shift $(( $OPTIND - 1 ))
+
+## If the destination is a directory then notice this.
+case $out in
+  t) if [ -d "$outfile" ]; then dir=${outfile%/}/; out=nil; fi ;;
+esac
+
+## Check that everything matches.
+case $#,$all,$out in
+  0,nil,*) lose "no Lisp systems to dump" ;;
+  0,t,nil) set -- $lisps ;;
+  *,t,*) lose "\`-a' makes no sense with explicit list" ;;
+  1,nil,t) ;;
+  *,*,t) lose "can't name explicit output file for multiple Lisp systems" ;;
+esac
+
+## Check that the Lisp systems named are actually known.
+for lisp in "$@"; do
+  case " $lisps " in
+    *" $lisp "*) ;;
+    *) echo >&2 "$prog: unknown Lisp \`$lisp'"; exit 2 ;;
+  esac
+done
+
+## Complain if there were problems.
+case $bogus in t) usage >&2; exit 2 ;; esac
+
+###--------------------------------------------------------------------------
+### Dump the images.
+
+## Establish a temporary directory to work in.
+i=0
+while :; do
+  tmp=${TMPDIR-/tmp}/runlisp-tmp.$$.
+  if mkdir "$tmp" >/dev/null 2>&1; then break; fi
+  case $i in 64) lose "failed to create temporary directory" ;; esac
+  i=$(expr $i + 1)
+done
+trap 'rm -rf "$tmp"' EXIT INT TERM HUP
+
+## Send stdout to stderr or the log, depending on verbosity.
+output () {
+  case $verbose in
+    nil) $lbuf cat -u >"$tmp/log" ;;
+    t) $lbuf cat >&2 ;;
+  esac
+}
+
+## Work through each requested Lisp system.
+exit=0
+for lisp in "$@"; do
+
+  ## Figure out the output file to use.
+  case $out in nil) eval outfile=\$dir\$${lisp}_image ;; esac
+
+  ## Maybe we skip this one if the output already exists.
+  case $update in
+    t)
+      if [ -f "$outfile" ]; then
+       case $verbose in
+         t)
+           echo >&2 "$prog: \`$outfile' already exists: skipping \`$lisp'"
+           ;;
+       esac
+       continue
+      fi
+      ;;
+  esac
+
+  ## If we're doing all the Lisps, then skip systems which aren't actually
+  ## installed.
+  case $checkinst in
+    t)
+      LISP=$(echo $lisp | tr a-z A-Z)
+      eval lispprog=\${$LISP-$lisp}
+      if ! type >/dev/null 2>&1 $lispprog; then
+       case $verbose in
+         t)
+           echo >&2 "$prog: command \`$LISP' not found: skipping \`$lisp'"
+           ;;
+       esac
+       continue
+      fi
+      ;;
+  esac
+
+  ## Dump the Lisp, capturing its potentially drivellous output in a log
+  ## (unless we're being verbose).  Be careful to keep stdout and stderr
+  ## separate.
+  rc=$(
+    { { { { echo "dumping $lisp to \`$outfile'..."
+           set +e; dump_$lisp "$outfile" 4>&- 5>&-
+           echo $? >&5; } |
+         copy "|" >&4; } 2>&1 |
+       copy "*" >&4; } 4>&1 |
+      output; } 5>&1 </dev/null
+  )
+
+  ## If it failed, and we didn't already spray the output to the terminal,
+  ## then do that now; also record that we encountered a problem.
+  case $rc in
+    0) ;;
+    *) case $verbose in nil) cat >&2 "$tmp/log" ;; esac; exit=2 ;;
+  esac
+done
+
+## All done.
+exit $exit
+
+###----- That's all, folks --------------------------------------------------
diff --git a/eval.lisp b/eval.lisp
new file mode 100644 (file)
index 0000000..24cd107
--- /dev/null
+++ b/eval.lisp
@@ -0,0 +1,59 @@
+;;; -*-lisp-*-
+;;;
+;;; Evaluate expressions and run scripts
+;;;
+;;; (c) 2020 Mark Wooding
+;;;
+
+;;;----- Licensing notice ---------------------------------------------------
+;;;
+;;; This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+;;;
+;;; Runlisp 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.
+;;;
+;;; Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+(cl:defpackage #:runlisp
+  (:use #:common-lisp))
+(cl:in-package #:runlisp)
+
+(setf *features* (remove :runlisp-script *features*))
+
+(let ((*package* (find-package "COMMON-LISP-USER")))
+  (let ((token (cons 'token nil))
+       (args uiop:*command-line-arguments*)
+       (list nil))
+    (flet ((foreach-form (func arg)
+            (with-input-from-string (in arg)
+              (loop (let ((form (read in nil token)))
+                      (when (eq form token) (return))
+                      (funcall func form)))))
+          (print-form (form)
+            (format t "~@[~{~S~^ ~}~%~]"
+                    (multiple-value-list (eval form)))))
+      (loop (let ((arg (pop args)))
+             (when (or (null arg) (string= arg "--")) (return))
+             (when (zerop (length arg))
+               (error "empty argument (no indicator)"))
+             (let ((rest (subseq arg 1)))
+               (ecase (char arg 0)
+                 (#\! (push (lambda ()
+                              (foreach-form #'eval rest))
+                            list))
+                 (#\? (push (lambda ()
+                              (foreach-form #'print-form rest))
+                            list))
+                 (#\< (push (lambda ()
+                              (load rest))
+                            list)))))))
+    (let ((uiop:*command-line-arguments* args))
+      (mapc #'funcall (nreverse list)))))
diff --git a/m4/mdw-auto-version.m4 b/m4/mdw-auto-version.m4
new file mode 120000 (symlink)
index 0000000..db358e4
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/m4/mdw-auto-version.m4
\ No newline at end of file
diff --git a/m4/mdw-decl-environ.m4 b/m4/mdw-decl-environ.m4
new file mode 120000 (symlink)
index 0000000..c9190c8
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/m4/mdw-decl-environ.m4
\ No newline at end of file
diff --git a/m4/mdw-define-paths.m4 b/m4/mdw-define-paths.m4
new file mode 120000 (symlink)
index 0000000..59213bf
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/m4/mdw-define-paths.m4
\ No newline at end of file
diff --git a/m4/mdw-dir-texmf.m4 b/m4/mdw-dir-texmf.m4
new file mode 120000 (symlink)
index 0000000..3290ae1
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/m4/mdw-dir-texmf.m4
\ No newline at end of file
diff --git a/m4/mdw-libtool-version-info.m4 b/m4/mdw-libtool-version-info.m4
new file mode 120000 (symlink)
index 0000000..3298202
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/m4/mdw-libtool-version-info.m4
\ No newline at end of file
diff --git a/m4/mdw-manext.m4 b/m4/mdw-manext.m4
new file mode 120000 (symlink)
index 0000000..56bc718
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/m4/mdw-manext.m4
\ No newline at end of file
diff --git a/m4/mdw-silent-rules.m4 b/m4/mdw-silent-rules.m4
new file mode 120000 (symlink)
index 0000000..52d11e3
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/m4/mdw-silent-rules.m4
\ No newline at end of file
diff --git a/runlisp.1 b/runlisp.1
new file mode 100644 (file)
index 0000000..35143f4
--- /dev/null
+++ b/runlisp.1
@@ -0,0 +1,751 @@
+.\" -*-nroff-*-
+.\"
+.\" Manual for `runlisp'
+.\"
+.\" (c) 2020 Mark Wooding
+.\"
+.
+.\"----- Licensing notice ---------------------------------------------------
+.\"
+.\" This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+.\"
+.\" Runlisp 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.
+.\"
+.\" Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+.
+.ie t \{\
+.  ds o \(bu
+.  if \n(.g \{\
+.    fam P
+.    ev an-1
+.    fam P
+.    ev
+.  \}
+.\}
+.el \{\
+.  ds o o
+.\}
+.
+.de hP
+.IP
+\h'-\w'\fB\\$1\ \fP'u'\fB\\$1\ \fP\c
+..
+.
+.\"--------------------------------------------------------------------------
+.TH runlisp 1 "2 August 2020" "Mark Wooding"
+.SH NAME
+runlisp \- run Common Lisp programs as scripts
+.
+.\"--------------------------------------------------------------------------
+.SH SYNOPSIS
+.
+.B runlisp
+.RB [ \-CDEnqv ]
+.RB [ \-I
+.IR imagedir ]
+.RB [ \-L
+.IB sys , sys , \fR...]
+.RB [ \-P
+.IB sys , sys , \fR...]
+.br
+\h'8n'
+.RB [ \-e
+.IR form  ]
+.RB [ \-l
+.IR file ]
+.RB [ \-p
+.IR form  ]
+.RB [ \-\- ]
+.RI [ script ]
+.RI [ arguments
+\&...]
+.
+.\"--------------------------------------------------------------------------
+.SH DESCRIPTION
+.
+The
+.B runlisp
+program has two main functions.
+.hP 1.
+It can be used in a script's
+.RB ` #! '
+line to run a Common Lisp script.
+.hP 2.
+It can be used in build scripts
+to invoke a Common Lisp system,
+e.g., to build a standalone program.
+.
+.SS "Supported Common Lisp implementations"
+The following Lisp implementations are currently supported.
+.TP
+.B "abcl"
+Armed Bear Common Lisp.
+.TP
+.B "ccl"
+Clozure Common Lisp.
+.TP
+.B "clisp"
+GNU CLisp.
+.TP
+.B "cmucl"
+Carnegie\(enMellon University Common Lisp.
+.TP
+.B "ecl"
+Embeddable Common Lisp.
+.TP
+.B "sbcl"
+Steel Bank Common Lisp.
+.PP
+The
+.B runlisp
+program expects, by default,
+to be able to run a Lisp system
+as a program with the same name,
+found by searching as directed by the
+.B PATH
+environment variable.
+This can be overridden by setting an environment variable,
+with the same name but in
+.IR "upper case" ,
+to the actual name \(en
+either a bare filename to be searched for on the
+.BR PATH ,
+or a pathname containing a
+.RB ` / ',
+relative to the working directory or absolute,
+to the program.
+Note that the entire variable value is used as the program name:
+it's not possible to provide custom arguments to a Lisp system
+using this mechanism.
+If you want to do that,
+you must write a shell script to do the necessary work,
+and point the environment variable
+(or the
+.BR PATH )
+at your script.
+.
+.SS "Options"
+Options are read from the command line, as usual,
+but also from a number of other sources;
+these are, in order:
+.hP \*o
+If a
+.I script
+is named,
+and the script's second line contains a
+.RB ` @RUNLISP: '
+marker,
+then text following the marker is parsed as options.
+.hP \*o
+If files named
+.B ~/.runlisprc
+and/or
+.B ~/.config/runlisprc
+exist,
+then their contents are parsed as options.
+.hP \*o
+If an environment variable
+.B RUNLISP_OPTIONS
+is defined,
+then its contents is parsed as options.
+.PP
+A simple quoting and escaping system is implemented
+to allow spaces and other special characters
+to be included in argument words
+in the script, configuration files, and environment variable.
+The details of all of this are given in the section
+.B Operation
+below.
+.
+.PP
+The options accepted are as follows.
+.
+.TP
+.B "\-\-help"
+Write a synopsis of
+.BR runlisp 's
+command-line syntax
+and a description of the command-line options
+to standard output
+and immediately exit with status 0.
+.
+.TP
+.B "\-\-version"
+Write
+.BR runlisp 's
+version number
+to standard output
+and immediately exit with status 0.
+.
+.TP
+.B "\-C"
+Clear the list of preferred Lisp implementations.
+.
+.TP
+.B "\-D"
+Don't check for a custom Lisp image.
+Usually,
+.B runlisp
+tries to start Lisp systems using a custom image,
+so that they'll start more quickly;
+the
+.RB ` \-D '
+option forces the use of the default `vanilla' image
+provided with the system.
+There's not usually any good reason to prefer the vanilla image,
+except for performance comparisons, or debugging
+.B runlisp
+itself.
+.
+.TP
+.B "\-E"
+Don't read embedded options from the
+second line of the
+.I script
+file.
+This has no effect in eval mode.
+.
+.TP
+.BI "\-I " imagedir
+Look in
+.I imagedir
+for custom Lisp images.
+This option overrides the default image directory,
+which is set at compile time.
+.
+.TP
+.BI "\-L " sys , sys ,\fR...
+Use one of the named Lisp systems.
+Each
+.I sys
+must name a supported Lisp system.
+This option may be given more than once:
+the effect is the same as a single option
+listing all of the systems named, in the same order.
+If a system is named more than once,
+a warning is issued (at verbosity level 1 or higher),
+and all but the first occurrence is ignored.
+.
+.TP
+.BI "\-P " sys , sys ,\fR...
+Set the relative preference order of Lisp systems:
+systems listed earlier are more preferred.
+Each
+.I sys
+must name a supported Lisp system.
+This option may be given more than once:
+the effect is the same as a single option
+listing all of the systems named, in the same order.
+If a system is named more than once,
+a warning is issued (at verbosity level 1 or higher),
+and all but the first occurrence is ignored.
+Unmentioned systems are assigned lowest preference:
+if a
+.RB ` \-L '
+option is given,
+then this provides a default preference ordering;
+otherwise, an ordering hardcoded into the program is used.
+The first acceptable Lisp system,
+according to the preference order just described,
+which actually exists,
+is the one selected.
+.
+.TP
+.BI "\-e " expr
+Evaluate the expression(s)
+.I expr
+and discard the resulting values.
+This option causes
+.B runlisp
+to execute in
+.I eval
+mode.
+.
+.TP
+.BI "\-l " file
+Read and evaluate forms from the
+.IR file .
+This option causes
+.B runlisp
+to execute in
+.I eval
+mode.
+.
+.TP
+.B "\-n"
+Don't actually start the Lisp environment.
+This may be helpful for the curious,
+in conjunction with
+.RB ` \-v '
+to increase the verbosity.
+.
+.TP
+.BI "\-p " expr
+Evaluate the expression(s)
+.I expr
+and print the resulting value(s)
+to standard output
+(as if by
+.BR prin1 ).
+If a form produces multiple values,
+they are printed on a single line,
+separated by a single space character;
+if a form produces no values at all,
+then nothing is printed \(en not even a newline character.
+This option causes
+.B runlisp
+to execute in
+.I eval
+mode.
+.
+.TP
+.B "\-q"
+Don't print warning messages.
+This option may be repeated:
+each use reduces verbosity by one step,
+counteracting one
+.RB ` \-v '
+option.
+The default verbosity level is 1,
+which prints only warning measages.
+.
+.TP
+.B "\-v"
+Print informational or debugging messages.
+This option may be repeated:
+each use increases verbosity by one step,
+counteracting one
+.RB ` \-q '
+option.
+The default verbosity level is 1,
+which prints only warning measages.
+Higher verbosity levels print informational and debugging messages.
+.
+.PP
+The
+.RB ` \-e ',
+.RB ` \-l ',
+and
+.RB ` \-p '
+options may only be given on the command-line itself,
+not following a
+.RB `@ RUNLISP: '
+marker in a script,
+in a configuration file,
+or in the
+.B RUNLISP_OPTIONS
+environment variable.
+These options may be given multiple times:
+they will be processed in the order given.
+If any of these options is given, then no
+.I script
+name will be parsed;
+instead, use
+.RB ` \-l '
+to load code from files.
+The
+.IR arguments ,
+if any,
+are still made available to the evaluated forms and loaded files.
+.
+.SS "Operation"
+The
+.B runlisp
+program behaves as follows.
+.PP
+The first thing it does is parse its command line.
+Options must precede positional arguments,
+though the boundary may be marked explicitly using
+.RB ` \-\- '
+if desired.
+If the command line contains any of
+.RB ` \-e ',
+.RB ` \-l ',
+or
+.RB ` \-p ',
+then
+.B runlisp
+treats all of its positional arguments as
+.I arguments
+to provide to the given forms and files,
+and runs in
+.I eval
+mode;
+otherwise, the first positional argument becomes the
+.I script
+name, the remaining ones become
+.IR arguments ,
+and
+.B runlisp
+runs in
+.I script
+mode.
+.PP
+In
+.I script
+mode,
+.B runlisp
+reads the second line of the script file,
+and checks to see if it contains the string
+.RB ` @RUNLISP: '.
+If so, then the following text is parsed
+for
+.IR "embedded options" ,
+as follows.
+The text is split into words
+separated by sequences of whitespace characters.
+Whitespace,
+and other special characters,
+can be included in a word by
+.I quoting
+or
+.IR escaping .
+Text between single quotes
+.BR ' ... '
+is included literally, without any further interpretation;
+text between double quotes
+.BR """" ... """"
+is treated literally,
+except that escaping can still be used
+to escape (e.g.) double quotes and the escape character itself.
+Outside of single quotes, a backslash
+.RB ` \e '
+causes the following character to be included in a word
+regardless of its usual meaning.
+(None of this allows a newline character
+to be included in a word:
+this is simply not possible.)
+A word which is
+.RB ` \-\- '
+before processing quoting and escaping
+marks the end of embedded options.
+As a concession to Emacs users,
+if the sequence
+.RB ` \-*\- '
+appears at the start of a word
+before processing quoting and escaping,
+then everything up to and including the next occurrence of
+.RB ` \-*\- '
+is ignored.
+The resulting list of words
+is processed as if it held further command-line options.
+However,
+.B runlisp
+is now committed to
+.I script
+mode, so
+.RB ` \-e ',
+.RB ` \-l ',
+and
+.RB ` \-p '
+options may not appear in a script file's embedded options list.
+(This feature allows scripts to provide options even if they use
+.BR env (1)
+to find
+.B runlisp
+on the
+.BR PATH ,
+or to provide more than one option,
+since many operating systems pass the text following
+the interpreter name on a
+.RB ` #! '
+line as a single argument, without further splitting it at spaces.)
+.PP
+If a file named
+.B .runlisprc
+exists in the user's home directory,
+then this file is read to discover more options.
+(If the variable
+.B HOME
+is set in the environment,
+then its value is assumed to name the user's home directory;
+otherwise, the home directory is determined by looking up
+the process's real uid in the password database.)
+Lines consisting entirely of whitespace,
+and lines whose first whitespace character is either
+.RB ` # '
+or
+.RB ` ; '
+are ignored in this file.
+Other lines are split into words,
+and processed as additional command-line options,
+as described for embedded options above,
+except that:
+a
+.RB ` \-\- '
+marker does not terminate word splitting; and
+Emacs-style
+.RB ` \-*\- ... \-*\- '
+local variable lists are not ignored.
+Each line is processed separately,
+so an option and its argument must be written on the same line.
+By this point
+.B runlisp
+will have committed to
+.I script
+or
+.I eval
+mode,
+so
+.RB ` \-e ',
+.RB ` \-l ',
+and
+.RB ` \-p '
+options may not appear in a configuration file.
+.PP
+If a file
+.B runlisprc
+exists in the user's
+.I "configuration home"
+directory,
+then it is processed as for
+.B .runlisprc
+above.
+If a variable
+.B XDG_CONFIG_HOME
+is set in the environment,
+then its value is assumed to name the configuration home;
+otherwise, the configuration home is the directory
+.B .config
+in the user's home directory, as determined above.
+.PP
+If the variable
+.B RUNLISP_OPTIONS
+is set in the environment,
+then its value is split into words
+and processed as additional command-line options,
+as for a line of a configuration file as described above.
+.PP
+The list of
+.I "acceptable Lisp implementations"
+is determined.
+If any
+.RB ` \-L '
+options have been issued,
+then the list of acceptable implementations
+consists of all of the implementations mentioned in
+.RB ` -L '
+options
+in any of the places
+.B runlisp
+looked for options,
+in the order of their first occurrence.
+(If an implementation is named more than once,
+then
+.B runlisp
+prints a warning to stderr
+and ignores all but the first occurrence.)
+If no
+.RB ` \-L '
+option is given, then
+.B runlisp
+uses a default list,
+which consists of all of the supported Lisp implementations
+in an hardcoded order which reflects
+the author's arbitrary preferences.
+.PP
+The list of
+.I "preferred Lisp implementations"
+is determined.
+If any
+.RB ` \-P '
+options have been issued
+.I "since the last"
+.IB ` \-C '
+.IR "option" ,
+then the list of preferred implementations
+consists of all of the implementations mentioned in
+.RB ` \-P '
+options after the last occurrence of
+.RB ` \-C ',
+in the order of their first occurrences.
+(If an implementation is named more than once,
+then
+.B runlisp
+prints a warning to stderr
+and ignores all but the first occurrence.)
+If no
+.RB ` \-P '
+option is given,
+or a
+.RB ` \-C '
+option appears after all of the
+.RB ` \-P '
+options,
+then the list of preferred implementations is empty.
+.PP
+Acceptable Lisp implementations are tried in turn.
+First, the preferred implementations
+which are also listed as acceptable implementations
+are tried, in the order in which they appear
+in the preferred implementations list;
+then, the remaining acceptable implementations are tried
+in the order in which they appear
+in the acceptable implementations list.
+To
+.I try
+a Lisp implementation means to construct a command line
+(whose effect will be described below)
+and pass it to the
+.BR execvp (3)
+function.
+If that succeeds, the Lisp implementation runs;
+if it fails with
+.B ENOENT
+then other Lisp systems are tried;
+if it fails with some other error, then
+.B runlisp
+reports an error message to stderr
+and exits unsuccessfully
+(with code 127).
+If the
+.RB ` \-n '
+option was given, then
+.B runlisp
+just simulates the behaviour of
+.BR execvp (3),
+printing messages to stderr
+if the verbosity level is sufficiently high,
+and exits.
+.PP
+In
+.I script
+mode,
+the script is invoked.
+In
+.I eval
+mode,
+the instructions given in
+.RB ` \-e ',
+.RB ` \-l ',
+and
+.RB ` \-p '
+options are carried out,
+in the order in which the appeared in the command line.
+The details of the environment
+in which Lisp code is executed
+are described next.
+.
+.SS "Script environment"
+Code in scripts and forms invoked by
+.B runlisp
+may assume the following facts about their environment.
+.hP \*o
+The keyword
+.B :runlisp-script
+is added to the
+.B *features*
+list if
+.B runlisp
+is running in
+.I script
+mode.
+.hP \*o
+Most Lisp systems support a user initialization file
+which they load before entering the REPL;
+some also have a system initialization file.
+The
+.B runlisp
+program arranges
+.I not
+to read these files,
+so that the Lisp environment is reasonably predictable,
+and to avoid slowing down script startup
+with things which are convenient for use in an interactive session,
+but can't be relied upon by a script anyway.
+.hP \*o
+The Unix standard input, standard output, and standard error files
+are available through the Lisp
+.BR *standard-input* ,
+.BR *standard-output* ,
+and
+.BR *error-output*
+streams, respectively.
+.hP \*o
+Both
+.B *compile-verbose*
+and
+.B *load-verbose*
+are set to nil.
+On CMU\ CL,
+.B ext:*require-verbose*
+is also nil.
+Alas, this is insufficient to muffle noise while loading add-on systems
+on some implementations.
+.hP \*o
+If an error is signalled, and not caught by user code,
+then the process will print a message to stderr
+and exit with a nonzero status.
+The reported message may be a long, ugly backtrace,
+or a terse error report.
+If no error is signalled but not caught,
+then the process will exit with status 0.
+.hP \*o
+The initial package is
+.BR COMMON-LISP-USER ,
+which has no symbols `present' (i.e., imported or interned).
+.hP \*o
+The
+.B asdf
+and
+.B uiop
+systems are already loaded.
+Further systems can be loaded using
+.B asdf:load-system
+as usual.
+The script name
+(which is only meaningful if
+.B runlisp
+is in
+.I script
+mode, obviously)
+and arguments are available through the
+.B uiop:argv0
+function and
+.B uiop:*command-line-arguments*
+variable, respectively.
+.
+.\"--------------------------------------------------------------------------
+.
+.SH "BUGS"
+.hP \*o
+Loading ASDF systems is irritatingly noisy
+with some Lisp implementations.
+Suggestions for how to improve this are welcome.
+.hP \*o
+More Lisp implementations should be supported.
+I've supported the ones I have installed.
+I'm not willing to put a great deal of effort into supporting
+non-free Lisp implementations;
+but help supporting free Lisps is much appreciated.
+.hP \*o
+The protocol for passing the script name through to
+.B uiop
+(specifically, through the
+.B __CL_ARGV0
+environment variable)
+is terribly fragile,
+but supporting
+.B uiop
+is obviously a better approach than introducing a
+.BR runlisp -specific
+interface to the same information.
+I don't know how to fix this:
+suggestions are welcome.
+.
+.SH "SEE ALSO"
+.BR dump-runlisp-image (1).
+.
+.SH "AUTHOR"
+Mark Wooding, <mdw@distorted.org.uk>
+.
+.\"----- That's all, folks --------------------------------------------------
diff --git a/runlisp.c b/runlisp.c
new file mode 100644 (file)
index 0000000..300c8ed
--- /dev/null
+++ b/runlisp.c
@@ -0,0 +1,1248 @@
+/* -*-c-*-
+ *
+ * Invoke a Lisp script
+ *
+ * (c) 2020 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+ *
+ * Runlisp 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.
+ *
+ * Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "config.h"
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <unistd.h>
+#include <sys/stat.h>
+
+#include <pwd.h>
+
+/*----- Common Lisp runes -------------------------------------------------*/
+
+/* A common preamble rune to do the necessary things.
+ *
+ * We need to ensure that `asdf' (and therefore `uiop') is loaded.  And we
+ * should arrange for `:runlisp-script' to find its way into the `*features*'
+ * list so that scripts can notice that they're being invoked from the
+ * command line rather than loaded into a resident session, and actually do
+ * something useful.
+ */
+#define COMMON_PRELUDE_RUNE                                            \
+       "(progn "                                                       \
+         "(setf *load-verbose* nil *compile-verbose* nil) "            \
+         "(require \"asdf\") "                                         \
+         "(funcall (intern \"REGISTER-IMMUTABLE-SYSTEM\" "             \
+                          "(find-package \"ASDF\")) "                  \
+                  "\"asdf\") "                                         \
+         "(set-dispatch-macro-character "                              \
+          "#\\# #\\! "                                                 \
+          "(lambda (#1=#:stream #2=#:char #3=#:arg) "                  \
+            "(declare (ignore #2# #3#)) "                              \
+            "(values (read-line #1#)))) "                              \
+         "(pushnew :runlisp-script *features*))"
+
+/* Get `uiop' to re-check the command-line arguments following an image
+ * restore.
+ */
+#define IMAGE_RESTORE_RUNE                                             \
+       "(uiop:call-image-restore-hook)"
+
+/* Some Lisps leave crud in the `COMMON-LISP-USER' package.  Clear it out. */
+#define CLEAR_CL_USER_RUNE                                             \
+       "(let ((#4=#:pkg (find-package \"COMMON-LISP-USER\"))) "        \
+         "(with-package-iterator (#5=#:next #4# :internal) "           \
+           "(loop (multiple-value-bind (#6=#:anyp #7=#:sym #8=#:how) " \
+                     "(#5#) "                                          \
+                   "(declare (ignore #8#)) "                           \
+                   "(unless #6# (return)) "                            \
+                   "(unintern #7# #4#)))))"
+
+/*----- Handy macros ------------------------------------------------------*/
+
+#define N(v) (sizeof(v)/sizeof((v)[0]))
+
+#if defined(__GNUC__)
+#  define GCC_VERSION_P(maj, min)                                      \
+       (__GNUC__ > (maj) || (__GNUC__ == (maj) && __GNUC_MINOR__ >= (min)))
+#else
+#  define GCC_VERSION_P(maj, min) 0
+#endif
+
+#ifdef __clang__
+#  define CLANG_VERSION_P(maj, min)                                    \
+       (__clang_major__ > (maj) || (__clang_major__ == (maj) &&        \
+                                    __clang_minor__ >= (min)))
+#else
+#  define CLANG_VERSION_P(maj, min) 0
+#endif
+
+#if GCC_VERSION_P(2, 5) || CLANG_VERSION_P(3, 3)
+#  define NORETURN __attribute__((__noreturn__))
+#  define PRINTF_LIKE(fix, aix) __attribute__((__format__(printf, fix, aix)))
+#endif
+
+#if GCC_VERSION_P(4, 0) || CLANG_VERSION_P(3, 3)
+#  define EXECL_LIKE(ntrail) __attribute__((__sentinel__(ntrail)))
+#endif
+
+#define CTYPE_HACK(func, ch) (func((unsigned char)(ch)))
+#define ISSPACE(ch) CTYPE_HACK(isspace, ch)
+
+#define MEMCMP(x, op, y, n) (memcmp((x), (y), (n)) op 0)
+#define STRCMP(x, op, y) (strcmp((x), (y)) op 0)
+#define STRNCMP(x, op, y, n) (strncmp((x), (y), (n)) op 0)
+
+#define END ((const char *)0)
+
+/*----- The Lisp implementation table -------------------------------------*/
+
+/* The systems, in decreasing order of (not quite my personal) preference.
+ * This list is used to initialize various tables and constants.
+ */
+#define LISP_SYSTEMS(_)                                                        \
+       _(sbcl)                                                         \
+       _(ccl)                                                          \
+       _(clisp)                                                        \
+       _(ecl)                                                          \
+       _(cmucl)                                                        \
+       _(abcl)
+
+enum {
+#define DEFSYS(sys) sys##_INDEX,
+  LISP_SYSTEMS(DEFSYS)
+#undef DEFSYS
+  NSYS
+};
+
+enum {
+#define DEFFLAG(sys) sys##_FLAG = 1 << sys##_INDEX,
+  LISP_SYSTEMS(DEFFLAG)
+#undef DEFFLAG
+  ALL_SYSTEMS = 0
+#define SETFLAG(sys) | sys##_FLAG
+  LISP_SYSTEMS(SETFLAG)
+#undef SETFLAG
+};
+
+struct argstate;
+struct argv;
+
+#define DECLENTRY(sys) \
+static void run_##sys(struct argstate *, const char *);
+  LISP_SYSTEMS(DECLENTRY)
+#undef DECLENTRY
+
+static const struct systab {
+  const char *name;
+  unsigned f;
+  void (*run)(struct argstate *, const char *);
+} systab[] = {
+#define SYSENTRY(sys) { #sys, sys##_FLAG, run_##sys },
+  LISP_SYSTEMS(SYSENTRY)
+#undef SYSENTRY
+};
+
+/*----- Diagnostic utilities ----------------------------------------------*/
+
+static const char *progname = "runlisp";
+
+static void set_progname(const char *prog)
+{
+  const char *p;
+
+  p = strrchr(prog, '/');
+  progname = p ? p + 1 : progname;
+}
+
+static void vmoan(const char *msg, va_list ap)
+{
+  fprintf(stderr, "%s: ", progname);
+  vfprintf(stderr, msg, ap);
+  fputc('\n', stderr);
+}
+
+static PRINTF_LIKE(1, 2) void moan(const char *msg, ...)
+  { va_list ap; va_start(ap, msg); vmoan(msg, ap); va_end(ap); }
+
+static NORETURN PRINTF_LIKE(1, 2) void lose(const char *msg, ...)
+  { va_list ap; va_start(ap, msg); vmoan(msg, ap); va_end(ap); exit(127); }
+
+/*----- Memory allocation -------------------------------------------------*/
+
+static void *xmalloc(size_t n)
+{
+  void *p;
+
+  if (!n) return (0);
+  p = malloc(n); if (!p) lose("failed to allocate memory");
+  return (p);
+}
+
+static void *xrealloc(void *p, size_t n)
+{
+  if (!n) { free(p); return (0); }
+  else if (!p) return (xmalloc(n));
+  p = realloc(p, n); if (!p) lose("failed to allocate memory");
+  return (p);
+}
+
+static char *xstrdup(const char *p)
+{
+  size_t n = strlen(p) + 1;
+  char *q = xmalloc(n);
+
+  memcpy(q, p, n);
+  return (q);
+}
+
+/*----- Dynamic strings ---------------------------------------------------*/
+
+struct dstr {
+  char *p;
+  size_t len, sz;
+};
+#define DSTR_INIT { 0, 0, 0 }
+
+/*
+static void dstr_init(struct dstr *d) { d->p = 0; d->len = d->sz = 0; }
+*/
+
+static void dstr_reset(struct dstr *d) { d->len = 0; }
+
+static void dstr_ensure(struct dstr *d, size_t n)
+{
+  size_t need = d->len + n, newsz;
+
+  if (need <= d->sz) return;
+  newsz = d->sz ? 2*d->sz : 16;
+  while (newsz < need) newsz *= 2;
+  d->p = xrealloc(d->p, newsz); d->sz = newsz;
+}
+
+static void dstr_release(struct dstr *d) { free(d->p); }
+
+static void dstr_putm(struct dstr *d, const void *p, size_t n)
+  { dstr_ensure(d, n); memcpy(d->p + d->len, p, n); d->len += n; }
+
+static void dstr_puts(struct dstr *d, const char *p)
+{
+  size_t n = strlen(p);
+
+  dstr_ensure(d, n + 1);
+  memcpy(d->p + d->len, p, n + 1);
+  d->len += n;
+}
+
+static void dstr_putc(struct dstr *d, int ch)
+  { dstr_ensure(d, 1); d->p[d->len++] = ch; }
+
+static void dstr_putz(struct dstr *d)
+  { dstr_ensure(d, 1); d->p[d->len] = 0; }
+
+static int dstr_readline(struct dstr *d, FILE *fp)
+{
+  size_t n;
+  int any = 0;
+
+  for (;;) {
+    dstr_ensure(d, 2);
+    if (!fgets(d->p + d->len, d->sz - d->len, fp)) break;
+    n = strlen(d->p + d->len); assert(n > 0); any = 1;
+    d->len += n;
+    if (d->p[d->len - 1] == '\n') { d->p[--d->len] = 0; break; }
+  }
+
+  if (!any) return (-1);
+  else return (0);
+}
+/*----- Dynamic vectors of strings ----------------------------------------*/
+
+struct argv {
+  const char **v;
+  size_t o, n, sz;
+};
+#define ARGV_INIT { 0, 0, 0, 0 }
+
+/*
+static void argv_init(struct argv *av)
+  { av->v = 0; av->o = av->n = av->sz = 0; }
+*/
+
+/*
+static void argv_reset(struct argv *av) { av->o = av->n = 0; }
+*/
+
+static void argv_ensure(struct argv *av, size_t n)
+{
+  size_t need = av->n + av->o + n, newsz;
+
+  if (need <= av->sz) return;
+  newsz = av->sz ? 2*av->sz : 8;
+  while (newsz < need) newsz *= 2;
+  av->v = xrealloc(av->v, newsz*sizeof(const char *)); av->sz = newsz;
+}
+
+static void argv_ensure_offset(struct argv *av, size_t n)
+{
+  size_t newoff;
+
+  /* Stupid version.  We won't, in practice, be prepending lots of stuff, so
+   * avoid the extra bookkeeping involved in trying to make a double-ended
+   * extendable array asymptotically efficient.
+   */
+  if (av->o >= n) return;
+  newoff = 16;
+  while (newoff < n) newoff *= 2;
+  argv_ensure(av, newoff - av->o);
+  memmove(av->v + newoff, av->v + av->o, av->n*sizeof(const char *));
+  av->o = newoff;
+}
+
+static void argv_release(struct argv *av) { free(av->v); }
+
+static void argv_append(struct argv *av, const char *p)
+  { argv_ensure(av, 1); av->v[av->n++ + av->o] = p; }
+
+static void argv_appendz(struct argv *av)
+  { argv_ensure(av, 1); av->v[av->n + av->o] = 0; }
+
+static void argv_appendn(struct argv *av, const char *const *v, size_t n)
+{
+  argv_ensure(av, n);
+  memcpy(av->v + av->n + av->o, v, n*sizeof(const char *));
+  av->n += n;
+}
+
+/*
+static void argv_appendav(struct argv *av, const struct argv *bv)
+  { argv_appendn(av, bv->v + bv->o, bv->n); }
+*/
+
+/*
+static void argv_appendv(struct argv *av, va_list ap)
+{
+  const char *p;
+
+  for (;;)
+    { p = va_arg(ap, const char *); if (!p) break; argv_append(av, p); }
+}
+*/
+
+/*
+static EXECL_LIKE(0) void argv_appendl(struct argv *av, ...)
+  { va_list ap; va_start(ap, av); argv_appendv(av, ap); va_end(ap); }
+*/
+
+static void argv_prepend(struct argv *av, const char *p)
+  { argv_ensure_offset(av, 1); av->v[--av->o] = p; av->n++; }
+
+/*
+static void argv_prependn(struct argv *av, const char *const *v, size_t n)
+{
+  argv_ensure_offset(av, 1);
+  av->o -= n; av->n += n;
+  memcpy(av->v + av->o, v, n*sizeof(const char *));
+}
+*/
+
+/*
+static void argv_prependav(struct argv *av, const struct argv *bv)
+  { argv_prependn(av, bv->v + bv->o, bv->n); }
+*/
+
+static void argv_prependv(struct argv *av, va_list ap)
+{
+  const char *p, **v;
+  size_t n = 0;
+
+  for (;;) {
+    p = va_arg(ap, const char *); if (!p) break;
+    argv_prepend(av, p); n++;
+  }
+  v = av->v + av->o;
+  while (n >= 2) {
+    p = v[0]; v[0] = v[n - 1]; v[n - 1] = p;
+    v++; n -= 2;
+  }
+}
+
+static EXECL_LIKE(0) void argv_prependl(struct argv *av, ...)
+  { va_list ap; va_start(ap, av); argv_prependv(av, ap); va_end(ap); }
+
+/*----- Lisp system table (redux) -----------------------------------------*/
+
+static const struct systab *find_system(const char *name)
+{
+  const struct systab *sys;
+  size_t i;
+
+  for (i = 0; i < NSYS; i++) {
+    sys = &systab[i];
+    if (STRCMP(name, ==, sys->name)) return (sys);
+  }
+  lose("unknown Lisp system `%s'", name);
+}
+
+static void lisp_quote_string(struct dstr *d, const char *p)
+{
+  size_t n;
+
+  for (;;) {
+    n = strcspn(p, "\"\\");
+    if (n) { dstr_putm(d, p, n); p += n; }
+    if (!*p) break;
+    dstr_putc(d, '\\'); dstr_putc(d, *p++);
+  }
+  dstr_putz(d);
+}
+
+static const char *expand_rune(struct dstr *d, const char *rune, ...)
+{
+  const struct argv *av;
+  va_list ap;
+  size_t i, n;
+
+  va_start(ap, rune);
+  for (;;) {
+    n = strcspn(rune, "%");
+    if (n) { dstr_putm(d, rune, n); rune += n; }
+    if (!*rune) break;
+    switch (*++rune) {
+      case '%': dstr_putc(d, '%'); break;
+      case 'e': lisp_quote_string(d, va_arg(ap, const char *)); break;
+      case 'E':
+       av = va_arg(ap, const struct argv *);
+       for (i = 0; i < av->n; i++) {
+         if (i) dstr_putc(d, ' ');
+         dstr_putc(d, '"');
+         lisp_quote_string(d, av->v[i]);
+         dstr_putc(d, '"');
+       }
+       break;
+      default: lose("*** BUG unknown expansion `%%%c'", *rune);
+    }
+    rune++;
+  }
+  dstr_putz(d);
+  return (d->p);
+}
+
+/*----- Argument processing -----------------------------------------------*/
+
+struct syslist {
+  const struct systab *sys[NSYS];
+  size_t n;
+  unsigned f;
+};
+#define SYSLIST_INIT { { 0 }, 0, 0 }
+
+struct argstate {
+  unsigned f;
+#define F_BOGUS 1u
+#define F_NOEMBED 2u
+#define F_NOACT 4u
+#define F_NODUMP 8u
+#define F_AUX 16u
+  int verbose;
+  char *imagedir;
+  struct syslist allow, pref;
+  struct argv av;
+};
+#define ARGSTATE_INIT { 0, 1, 0, SYSLIST_INIT, SYSLIST_INIT, ARGV_INIT }
+
+/*----- Running programs --------------------------------------------------*/
+
+#define FEF_EXEC 1u
+static int file_exists_p(const struct argstate *arg, const char *path,
+                        unsigned f)
+{
+  struct stat st;
+
+  if (stat(path, &st)) {
+    if (arg && arg->verbose > 2) moan("file `%s' not found", path);
+    return (0);
+  } else if (!(S_ISREG(st.st_mode))) {
+    if (arg && arg->verbose > 2) moan("`%s' is not a regular file", path);
+    return (0);
+  } else if ((f&FEF_EXEC) && access(path, X_OK)) {
+    if (arg && arg->verbose > 2) moan("file `%s' is not executable", path);
+    return (0);
+  } else {
+    if (arg && arg->verbose > 2) moan("found file `%s'", path);
+    return (1);
+  }
+}
+
+static int found_in_path_p(const struct argstate *arg, const char *prog)
+{
+  struct dstr p = DSTR_INIT, d = DSTR_INIT;
+  const char *path;
+  char *q;
+  size_t n, avail, proglen;
+  int i;
+
+  if (strchr(prog, '/')) return (file_exists_p(arg, prog, 0));
+  path = getenv("PATH");
+  if (path)
+    dstr_puts(&p, path);
+  else {
+    dstr_puts(&p, ".:");
+    i = 0;
+  again:
+    avail = p.sz - p.len;
+    n = confstr(_CS_PATH, p.p + p.len, avail);
+    if (avail > n) { i++; assert(i < 2); dstr_ensure(&p, n); goto again; }
+  }
+
+  q = p.p; proglen = strlen(prog);
+  for (;;) {
+    n = strcspn(q, ":");
+    dstr_reset(&d);
+    if (q[n]) dstr_putm(&d, q, n);
+    else dstr_putc(&d, '.');
+    dstr_putc(&d, '/');
+    dstr_putm(&d, prog, proglen);
+    dstr_putz(&d);
+    if (file_exists_p(arg, d.p, FEF_EXEC)) {
+      if (arg->verbose == 2) moan("found program `%s'", d.p);
+      return (1);
+    }
+    q += n; if (!*q) break; else q++;
+  }
+  return (0);
+}
+
+static void try_exec(const struct argstate *arg, struct argv *av)
+{
+  struct dstr d = DSTR_INIT;
+  size_t i;
+
+  assert(av->n); argv_appendz(av);
+  if (arg->verbose > 1) {
+    for (i = 0; i < av->n; i++) {
+      if (i) { dstr_putc(&d, ','); dstr_putc(&d, ' '); }
+      dstr_putc(&d, '"');
+      lisp_quote_string(&d, av->v[av->o + i]);
+      dstr_putc(&d, '"');
+    }
+    dstr_putz(&d);
+    moan("trying %s...", d.p);
+  }
+  if (arg->f&F_NOACT)
+    { if (found_in_path_p(arg, av->v[av->o])) exit(0); }
+  else {
+    execvp(av->v[av->o], (/*unconst*/ char **)av->v + av->o);
+    if (errno != ENOENT)
+      lose("failed to exec `%s': %s", av->v[av->o], strerror(errno));
+  }
+  if (arg->verbose > 1) moan("`%s' not found", av->v[av->o]);
+  dstr_release(&d);
+}
+
+static const char *getenv_or_default(const char *var, const char *dflt)
+  { const char *p = getenv(var); return (p ? p : dflt); }
+
+/*----- Invoking Lisp systems ---------------------------------------------*/
+
+/* Steel Bank Common Lisp. */
+
+static void run_sbcl(struct argstate *arg, const char *script)
+{
+  struct dstr d = DSTR_INIT;
+
+  argv_prependl(&arg->av, "--script", script, END);
+
+  dstr_puts(&d, arg->imagedir);
+  dstr_putc(&d, '/');
+  dstr_puts(&d, "sbcl+asdf.core");
+  if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, 0))
+    argv_prependl(&arg->av,
+                 "--core", d.p,
+                 "--eval", IMAGE_RESTORE_RUNE,
+                 END);
+  else
+    argv_prependl(&arg->av, "--eval", COMMON_PRELUDE_RUNE, END);
+
+  argv_prependl(&arg->av, getenv_or_default("SBCL", "sbcl"),
+               "--noinform",
+               END);
+  try_exec(arg, &arg->av);
+  dstr_release(&d);
+}
+
+/* Clozure Common Lisp. */
+
+#define CCL_QUIT_RUNE                                                  \
+       "(ccl:quit)"
+
+static void run_ccl(struct argstate *arg, const char *script)
+{
+  struct dstr d = DSTR_INIT;
+
+  argv_prependl(&arg->av, "-b", "-n", "-Q",
+               "-l", script,
+               "-e", CCL_QUIT_RUNE,
+               "--",
+               END);
+
+  dstr_puts(&d, arg->imagedir);
+  dstr_putc(&d, '/');
+  dstr_puts(&d, "ccl+asdf.image");
+  if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, 0))
+    argv_prependl(&arg->av, "-I", d.p, "-e", IMAGE_RESTORE_RUNE, END);
+  else
+    argv_prependl(&arg->av, "-e", COMMON_PRELUDE_RUNE, END);
+
+  argv_prepend(&arg->av, getenv_or_default("CCL", "ccl"));
+  try_exec(arg, &arg->av);
+  dstr_release(&d);
+}
+
+/* GNU CLisp.
+ *
+ * CLisp causes much sadness.  Superficially, it's the most sensible of all
+ * of the systems supported here: you just run `clisp SCRIPT -- ARGS ...' and
+ * it works.
+ *
+ * The problems come when you want to do some preparatory work (e.g., load
+ * `asdf') and then run the script.  There's a `-x' option to evaluate some
+ * Lisp code, but it has three major deficiencies.
+ *
+ *   * It insists on printing the values of the forms it evaluates.  It
+ *     prints a blank line even if the form goes out of its way to produce no
+ *     values at all.  So the whole thing has to be a single top-level form
+ *     which quits the Lisp rather than returning.
+ *
+ *   * For some idiotic reason, you can have /either/ `-x' forms /or/ a
+ *     script, but not both.  So we have to include the `load' here
+ *     explicitly.  I suppose that was inevitable because we have to inhibit
+ *     printing of the result forms, but it's still a separate source of
+ *     annoyance.
+ *
+ *   * The icing on the cake: the `-x' forms are collectively concatenated --
+ *     without spaces! -- and used to build a string stream, which is then
+ *     assigned over the top of `*standard-input*', making the original stdin
+ *     somewhat fiddly to track down.
+ *
+ * There's an `-i' option which will load a file without any of this
+ * stupidity, but nothing analogous for immediate expressions.
+ */
+
+#define CLISP_COMMON_STARTUP_RUNES                                     \
+       "(setf *standard-input* (ext:make-stream :input)) "             \
+       "(load \"%e\" :verbose nil :print nil) "                        \
+       "(ext:quit)"
+
+#define CLISP_STARTUP_RUNE                                             \
+       "(progn "                                                       \
+          COMMON_PRELUDE_RUNE " "                                      \
+          CLISP_COMMON_STARTUP_RUNES ")"
+
+#define CLISP_STARTUP_IMAGE_RUNE                                       \
+       "(progn "                                                       \
+          IMAGE_RESTORE_RUNE " "                                       \
+          CLISP_COMMON_STARTUP_RUNES ")"
+
+static void run_clisp(struct argstate *arg, const char *script)
+{
+  struct dstr d = DSTR_INIT, dd = DSTR_INIT;
+
+  dstr_puts(&d, arg->imagedir);
+  dstr_putc(&d, '/');
+  dstr_puts(&d, "clisp+asdf.mem");
+  if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, 0))
+    argv_prependl(&arg->av, "-M", d.p, "-q",
+                 "-x", expand_rune(&dd, CLISP_STARTUP_IMAGE_RUNE, script),
+                 "--",
+                 END);
+  else
+    argv_prependl(&arg->av, "-norc", "-q",
+                 "-x", expand_rune(&dd, CLISP_STARTUP_RUNE, script),
+                 "--",
+                 END);
+
+  argv_prepend(&arg->av, getenv_or_default("CLISP", "clisp"));
+  try_exec(arg, &arg->av);
+  dstr_release(&d);
+  dstr_release(&dd);
+
+#undef f
+}
+
+/* Embeddable Common Lisp. *
+ *
+ * ECL is changing its command-line option syntax in version 16.  I have no
+ * idea why they think the result can ever be worth the pain of a transition.
+ */
+
+#if ECL_OPTIONS_GNU
+#  define ECLOPT "--"
+#else
+#  define ECLOPT "-"
+#endif
+
+#define ECL_STARTUP_RUNE                                               \
+       "(progn "                                                       \
+          COMMON_PRELUDE_RUNE " "                                      \
+          CLEAR_CL_USER_RUNE ")"
+
+static void run_ecl(struct argstate *arg, const char *script)
+{
+  struct dstr d = DSTR_INIT;
+
+  dstr_puts(&d, arg->imagedir);
+  dstr_putc(&d, '/');
+  dstr_puts(&d, "ecl+asdf");
+  if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, FEF_EXEC)) {
+    argv_prependl(&arg->av, "-s", script, "--", END);
+    argv_prependl(&arg->av, d.p, END);
+  } else {
+    argv_prependl(&arg->av, ECLOPT "shell", script, "--", END);
+    argv_prependl(&arg->av, getenv_or_default("ECL", "ecl"), ECLOPT "norc",
+                 ECLOPT "eval", ECL_STARTUP_RUNE,
+                 END);
+  }
+  try_exec(arg, &arg->av);
+}
+
+/* Carnegie--Mellon University Common Lisp. */
+
+#define CMUCL_STARTUP_RUNE                                             \
+       "(progn "                                                       \
+         "(setf ext:*require-verbose* nil) "                           \
+         COMMON_PRELUDE_RUNE ")"
+#define CMUCL_QUIT_RUNE                                                        \
+       "(ext:quit)"
+
+static void run_cmucl(struct argstate *arg, const char *script)
+{
+  struct dstr d = DSTR_INIT;
+
+  argv_prependl(&arg->av,
+               "-load", script,
+               "-eval", CMUCL_QUIT_RUNE,
+               "--",
+               END);
+
+  dstr_puts(&d, arg->imagedir);
+  dstr_putc(&d, '/');
+  dstr_puts(&d, "cmucl+asdf.core");
+  if (!(arg->f&F_NODUMP) && file_exists_p(arg, d.p, 0))
+    argv_prependl(&arg->av, "-core", d.p, "-eval", IMAGE_RESTORE_RUNE, END);
+  else
+    argv_prependl(&arg->av, "-batch", "-noinit", "-nositeinit", "-quiet",
+                 "-eval", CMUCL_STARTUP_RUNE,
+                 END);
+
+  argv_prepend(&arg->av, getenv_or_default("CMUCL", "cmucl"));
+  try_exec(arg, &arg->av);
+  dstr_release(&d);
+}
+
+/* Armed Bear Common Lisp. *
+ *
+ * CLisp made a worthy effort, but ABCL still manages to take the price.
+ *
+ *   * ABCL manages to avoid touching the `stderr' stream at all, ever.  Its
+ *     startup machinery finds `stdout' (as `java.lang.System.out'), wraps it
+ *     up in a Lisp stream, and uses the result as `*standard-output*' and
+ *     `*error-output*' (and a goodly number of other things too).  So we
+ *     must manufacture a working `stderr' the hard way.
+ *
+ *   * There doesn't appear to be any easy way to prevent toplevel errors
+ *     from invoking the interactive debugger.  For extra fun, the debugger
+ *     reads from `stdin' by default, so an input file which somehow manages
+ *     to break the script can then take over its brain by providing Lisp
+ *     forms for the debugger to evaluate.
+ */
+
+#define ABCL_STARTUP_RUNE                                              \
+       "(let ((#9=#:script \"%e\")) "                                  \
+          COMMON_PRELUDE_RUNE " "                                      \
+          CLEAR_CL_USER_RUNE " "                                       \
+                                                                       \
+          /* Replace the broken `*error-output*' stream with a working \
+           * copy of `stderr'.                                         \
+           */                                                          \
+         "(setf *error-output* "                                       \
+                 "(java:jnew \"org.armedbear.lisp.Stream\" "           \
+                            "'sys::system-stream "                     \
+                            "(java:jfield \"java.lang.System\" \"err\") " \
+                            "'character "                              \
+                            "java:+true+)) "                           \
+                                                                       \
+          /* Trap errors signalled by the script and arrange for them  \
+           * to actually kill the process rather than ending up in the \
+           * interactive debugger.                                     \
+           */                                                          \
+         "(handler-case (load #9# :verbose nil :print nil) "           \
+           "(error (error) "                                           \
+             "(format *error-output* \"~A (unhandled error): ~A~%%\" " \
+                     "#9# error) "                                     \
+           "(ext:quit :status 255))))"
+
+static void run_abcl(struct argstate *arg, const char *script)
+{
+  struct dstr d = DSTR_INIT;
+
+  argv_prependl(&arg->av, getenv_or_default("ABCL", "abcl"),
+               "--batch", "--noinform", "--noinit", "--nosystem",
+               "--eval", expand_rune(&d, ABCL_STARTUP_RUNE, script),
+               "--",
+               END);
+  try_exec(arg, &arg->av);
+  dstr_release(&d);
+}
+
+/*----- Main code ---------------------------------------------------------*/
+
+static void version(FILE *fp)
+  { fprintf(fp, "%s, version %s\n", progname, PACKAGE_VERSION); }
+
+static void usage(FILE *fp)
+{
+  fprintf(fp, "usage: %s [-CDEnqv] [-I IMAGEDIR] "
+             "[-L SYS,SYS,...] [-P SYS,SYS,...]\n"
+             "\t[--] SCRIPT [ARGUMENTS ...] |\n"
+             "\t[-e EXPR] [-p EXPR] [-l FILE] [--] [ARGUMENTS ...]\n",
+         progname);
+}
+
+static void help(FILE *fp)
+{
+  version(fp); fputc('\n', fp); usage(fp);
+  fputs("\n\
+Options:\n\
+  --help               Show this help text and exit successfully.\n\
+  --version            Show the version number and exit successfully.\n\
+  -C                   Clear the list of preferred Lisp systems.\n\
+  -D                   Run system Lisp images, rather than custom images.\n\
+  -E                   Don't read embedded options from the script.\n\
+  -I IMAGEDIR          Look for custom images in IMAGEDIR rather than\n\
+                         `" IMAGEDIR "'.\n\
+  -L SYS,SYS,...       Only use the listed Lisp systems.the script.\n\
+  -P SYS,SYS,...       Prefer the listed Lisp systems.\n\
+  -e EXPR              Evaluate EXPR (can be repeated).\n\
+  -l FILE              Load FILE (can be repeated).\n\
+  -n                   Don't actually run the script (useful with `-v')\n\
+  -p EXPR              Print (`prin1') EXPR (can be repeated).\n\
+  -q                   Don't print warning messages.\n\
+  -v                   Print informational messages (repeat for even more).\n",
+       fp);
+}
+
+/* Parse a comma-separated list of system names SPEC, and add the named
+ * systems to LIST.
+ */
+static void parse_syslist(const char *spec, const struct argstate *arg,
+                         struct syslist *list, const char *what)
+{
+  char *copy = xstrdup(spec), *p = copy, *q;
+  const struct systab *sys;
+  size_t n;
+
+  for (;;) {
+    n = strcspn(p, ",");
+    if (p[n]) q = p + n + 1;
+    else q = 0;
+    p[n] = 0; sys = find_system(p);
+    if (list->f&sys->f) {
+      if (arg->verbose > 0)
+       moan("ignoring duplicate system `%s' in %s list", p, what);
+    } else {
+      list->sys[list->n++] = sys;
+      list->f |= sys->f;
+    }
+    if (!q) break;
+    p = q;
+  }
+  free(copy);
+}
+
+static void push_eval_op(struct argstate *arg, char op, const char *val)
+{
+  char *p;
+  size_t n;
+
+  if (arg->f&F_AUX) {
+    moan("must use `-e', `-p', or `-l' on command line");
+    arg->f |= F_BOGUS;
+    return;
+  }
+
+  n = strlen(val) + 1;
+  p = xmalloc(n + 1);
+  p[0] = op; memcpy(p + 1, val, n);
+  argv_append(&arg->av, p);
+}
+
+/* Parse a vector ARGS of command-line arguments.  Update ARG with the
+ * results.  NARG is the number of arguments, and *I_INOUT is the current
+ * index into the vector, to be updated on exit to identify the first
+ * non-option argument (or the end of the vector).
+ */
+static void parse_arguments(struct argstate *arg, const char *const *args,
+                           size_t nargs, size_t *i_inout)
+{
+  const char *o, *a;
+  char opt;
+
+  for (;;) {
+    if (*i_inout >= nargs) break;
+    o = args[*i_inout];
+    if (STRCMP(o, ==, "--help")) { help(stdout); exit(0); }
+    else if (STRCMP(o, ==, "--version")) { version(stdout); exit(0); }
+    if (!*o || *o != '-' || !o[1]) break;
+    (*i_inout)++;
+    if (STRCMP(o, ==, "--")) break;
+    o++;
+    while (o && *o) {
+      opt = *o++;
+      switch (opt) {
+
+#define GETARG do {                                                    \
+  if (*o)                                                              \
+    { a = o; o = 0; }                                                  \
+  else {                                                               \
+    if (*i_inout >= nargs) goto noarg;                                 \
+    a = args[(*i_inout)++];                                            \
+  }                                                                    \
+} while (0)
+
+       case 'C': arg->pref.n = 0; arg->pref.f = 0; break;
+       case 'D': arg->f |= F_NODUMP; break;
+       case 'E': arg->f |= F_NOEMBED; break;
+       case 'e': GETARG; push_eval_op(arg, '!', a); break;
+       case 'p': GETARG; push_eval_op(arg, '?', a); break;
+       case 'l': GETARG; push_eval_op(arg, '<', a); break;
+       case 'n': arg->f |= F_NOACT; break;
+       case 'q': if (arg->verbose) arg->verbose--; break;
+       case 'v': arg->verbose++; break;
+
+       case 'I':
+         free(arg->imagedir);
+         GETARG; arg->imagedir = xstrdup(a);
+         break;
+
+       case 'L':
+         GETARG;
+         parse_syslist(a, arg, &arg->allow, "allowed");
+         break;
+
+       case 'P':
+         GETARG;
+         parse_syslist(a, arg, &arg->pref, "preferred");
+         break;
+
+       default:
+         moan("unknown option `%c'", opt);
+         arg->f |= F_BOGUS;
+         break;
+
+#undef GETARG
+
+      }
+    }
+  }
+  goto end;
+
+noarg:
+  moan("missing argument for `-%c'", opt);
+  arg->f |= F_BOGUS;
+end:
+  return;
+}
+
+/* Parse a string P into words (destructively), and process them as
+ * command-line options, updating ARG.  Non-option arguments are not
+ * permitted.  If `SOSF_EMACS' is set in FLAGS, then ignore `-*- ... -*-'
+ * editor turds.  If `SOSF_ENDOK' is set, then accept `--' and ignore
+ * whatever comes after; otherwise, reject all positional arguments.
+ */
+#define SOSF_EMACS 1u
+#define SOSF_ENDOK 2u
+static void scan_options_from_string(char *p, struct argstate *arg,
+                                    unsigned flags,
+                                    const char *what, const char *file)
+{
+  struct argv av = ARGV_INIT;
+  char *q;
+  size_t i;
+  int st = 0;
+  unsigned f = 0;
+#define f_escape 1u
+
+  for (;;) {
+    while (ISSPACE(*p)) p++;
+    if (!*p) break;
+    if ((flags&SOSF_EMACS) && p[0] == '-' && p[1] == '*' && p[2] == '-') {
+      p = strstr(p + 3, "-*-");
+      if (!p) lose("unfinished local-variables list in %s `%s'", what, file);
+      p += 3; continue;
+    }
+    if ((flags&SOSF_ENDOK) &&
+       p[0] == '-' && p[1] == '-' && (!p[2] || ISSPACE(p[2])))
+      break;
+    argv_append(&av, p); q = p;
+    for (;;) {
+      if (!*p) break;
+      else if (f&f_escape) { *q++ = *p; f &= ~f_escape; }
+      else if (st && *p == st) st = 0;
+      else if (st != '\'' && *p == '\\') f |= f_escape;
+      else if (!st && (*p == '"' || *p == '\'')) st = *p;
+      else if (!st && ISSPACE(*p)) break;
+      else *q++ = *p;
+      p++;
+    }
+    if (*p) p++;
+    *q = 0;
+    if (f&f_escape) lose("unfinished escape in %s `%s'", what, file);
+    if (st) lose("unfinished `%c' string in %s `%s'", st, what, file);
+  }
+
+  i = 0; parse_arguments(arg, av.v, av.n, &i);
+  if (i < av.n)
+    lose("positional argument `%s' in %s `%s'", av.v[i], what, file);
+  argv_release(&av);
+
+#undef f_escape
+}
+
+/* Read SCRIPT, and check for a `@RUNLISP:' marker in the second line.  If
+ * there is one, parse options from it, and update ARG.
+ */
+static void check_for_embedded_args(const char *script, struct argstate *arg)
+{
+  struct dstr d = DSTR_INIT;
+  char *p;
+  FILE *fp = 0;
+
+  fp = fopen(script, "r");
+  if (!fp) lose("can't read script `%s': %s", script, strerror(errno));
+
+  if (dstr_readline(&d, fp)) goto end;
+  dstr_reset(&d); if (dstr_readline(&d, fp)) goto end;
+
+  p = strstr(d.p, "@RUNLISP:");
+  if (p)
+    scan_options_from_string(p + 9, arg, SOSF_EMACS | SOSF_ENDOK,
+                            "embedded options in script", script);
+
+end:
+  if (fp) {
+    if (ferror(fp))
+      lose("error reading script `%s': %s", script, strerror(errno));
+    fclose(fp);
+  }
+  dstr_release(&d);
+}
+
+/* Read the file PATH (if it exists) and update ARG with the arguments parsed
+ * from it.  Ignore blank lines and (Unix- or Lisp-style) comments.
+ */
+static void read_config_file(const char *path, struct argstate *arg)
+{
+  FILE *fp = 0;
+  struct dstr d = DSTR_INIT;
+  char *p;
+
+  fp = fopen(path, "r");
+  if (!fp) {
+    if (errno == ENOENT) {
+      if (arg->verbose > 2)
+       moan("ignoring nonexistent configuration file `%s'", path);
+      goto end;
+    }
+    lose("failed to open configuration file `%s': %s",
+        path, strerror(errno));
+  }
+  if (arg->verbose > 1)
+    moan("reading configuration file `%s'", path);
+  for (;;) {
+    dstr_reset(&d);
+    if (dstr_readline(&d, fp)) break;
+    p = d.p;
+    while (ISSPACE(*p)) p++;
+    if (!*p || *p == ';' || *p == '#') continue;
+    scan_options_from_string(p, arg, 0, "configuration file `%s'", path);
+  }
+  if (arg->f&F_BOGUS)
+    lose("invalid options in configuration file `%s'", path);
+
+end:
+  if (fp) {
+    if (ferror(fp))
+      lose("error reading configuration file `%s': %s",
+          path, strerror(errno));
+    fclose(fp);
+  }
+  dstr_release(&d);
+}
+
+int main(int argc, char *argv[])
+{
+  struct dstr d = DSTR_INIT;
+  const char *script, *p;
+  const char *home;
+  struct passwd *pw;
+  char *t;
+  size_t i, n;
+  struct argstate arg = ARGSTATE_INIT;
+
+  /* Scan the command line.  This gets low priority, since it's probably
+   * from the script shebang.
+   */
+  set_progname(argv[0]); i = 1;
+  parse_arguments(&arg, (const char *const *)argv, argc, &i);
+  arg.f |= F_AUX;
+  if ((i >= argc && !arg.av.n) || (arg.f&F_BOGUS))
+    { usage(stderr); exit(255); }
+
+  /* Prepare the argument vector.  Keep track of the number of arguments
+   * here: we'll need to refer to this later.
+   */
+  if (!arg.av.n) {
+    script = argv[i++];
+    if (!(arg.f&F_NOEMBED)) check_for_embedded_args(script, &arg);
+    if (arg.f&F_BOGUS)
+      lose("invalid options in `%s' embedded option list", script);
+  } else {
+    script = getenv("RUNLISP_EVAL");
+    if (!script) script = DATADIR "/eval.lisp";
+    argv_append(&arg.av, "--");
+  }
+  argv_appendn(&arg.av, (const char *const *)argv + i, argc - i);
+  n = arg.av.n;
+
+  /* Find the user's home directory.  (Believe them if they set something
+   * strange.)
+   */
+  home = getenv("HOME");
+  if (!home) {
+    pw = getpwuid(getuid());
+    if (!pw) lose("can't find user in password database");
+    home = pw->pw_dir;
+  }
+
+  /* Check user configuration file `~/.runlisprc'. */
+  dstr_reset(&d);
+  dstr_puts(&d, home); dstr_putc(&d, '/'); dstr_puts(&d, ".runlisprc");
+  read_config_file(d.p, &arg);
+
+  /* Check user configuration file `~/.config/runlisprc'. */
+  dstr_reset(&d);
+  p = getenv("XDG_CONFIG_HOME");
+  if (p)
+    dstr_puts(&d, p);
+  else
+    { dstr_puts(&d, home); dstr_putc(&d, '/'); dstr_puts(&d, ".config"); }
+  dstr_putc(&d, '/'); dstr_puts(&d, "runlisprc");
+  read_config_file(d.p, &arg);
+
+  /* Finally, check the environment variables. */
+  p = getenv("RUNLISP_OPTIONS");
+  if (p) {
+    t = xstrdup(p);
+    scan_options_from_string(t, &arg, 0,
+                            "environment variable", "RUNLISP_OPTIONS");
+    free(t);
+  }
+  if (arg.f&F_BOGUS)
+    lose("invalid options in environment variable `RUNLISP_OPTIONS'");
+  if (!arg.imagedir) {
+    arg.imagedir = getenv("RUNLISP_IMAGEDIR");
+    if (!arg.imagedir) arg.imagedir = IMAGEDIR;
+  }
+
+  /* If no systems are listed as acceptable, try them all. */
+  if (!arg.allow.n) {
+    if (arg.verbose > 1)
+      moan("no explicitly allowed implementations: allowing all");
+    for (i = 0; i < NSYS; i++) arg.allow.sys[i] = &systab[i];
+    arg.allow.n = NSYS; arg.allow.f = (1u << NSYS) - 1;
+  }
+
+  /* Print what we're going to do. */
+  if (arg.verbose > 2) {
+    dstr_reset(&d); p = "";
+    for (i = 0; i < arg.allow.n; i++)
+      { dstr_puts(&d, p); p = ", "; dstr_puts(&d, arg.allow.sys[i]->name); }
+    moan("permitted Lisps: %s", d.p);
+
+    dstr_reset(&d); p = "";
+    for (i = 0; i < arg.pref.n; i++)
+      { dstr_puts(&d, p); p = ", "; dstr_puts(&d, arg.pref.sys[i]->name); }
+    moan("preferred Lisps: %s", d.p);
+
+    dstr_reset(&d); p = "";
+    for (i = 0; i < arg.pref.n; i++)
+      if (arg.pref.sys[i]->f&arg.allow.f)
+       { dstr_puts(&d, p); p = ", "; dstr_puts(&d, arg.pref.sys[i]->name); }
+    for (i = 0; i < arg.allow.n; i++)
+      if (!(arg.allow.sys[i]->f&arg.pref.f))
+       { dstr_puts(&d, p); p = ", "; dstr_puts(&d, arg.allow.sys[i]->name); }
+    moan("overall preference order: %s", d.p);
+  }
+
+  /* Inform `uiop' of the script name.
+   *
+   * As an aside, this is a terrible interface.  It's too easy to forget to
+   * set it.  (To illustrate this, `cl-launch -x' indeed forgets to set it.)
+   * If you're lucky, the script just thinks that its argument is `nil', in
+   * which case maybe it can use `*load-pathname*' as a fallback.  If you're
+   * unlucky, your script was invoked (possibly indirectly) by another
+   * script, and now you've accidentally inherited the calling script's name.
+   *
+   * It would have been far better simply to repeat the script name as the
+   * first user argument, if nothing else had come readily to mind.
+   */
+  if (setenv("__CL_ARGV0", script, 1))
+    lose("failed to set script-name environment variable");
+
+  /* Work through the list of preferred Lisp systems, trying the ones which
+   * are allowed.
+   */
+  for (i = 0; i < arg.pref.n; i++)
+    if (arg.pref.sys[i]->f&arg.allow.f) {
+      arg.av.o += arg.av.n - n; arg.av.n = n;
+      arg.pref.sys[i]->run(&arg, script);
+    }
+
+  /* That didn't work.  Try the remaining allowed systems, in the given
+   * order.
+   */
+  for (i = 0; i < arg.allow.n; i++)
+    if (!(arg.allow.sys[i]->f&arg.pref.f)) {
+      arg.av.o += arg.av.n - n; arg.av.n = n;
+      arg.allow.sys[i]->run(&arg, script);
+    }
+
+  /* No joy.  Give up. */
+  argv_release(&arg.av);
+  lose("no supported Lisp systems found");
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/t/Makefile.am b/t/Makefile.am
new file mode 100644 (file)
index 0000000..1c706d5
--- /dev/null
@@ -0,0 +1,31 @@
+### -*-makefile-*-
+###
+### Build script for tests
+###
+### (c) 2020 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+###
+### Runlisp 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.
+###
+### Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+include autotest.am
+autotest_TESTS          =
+TEST_ARGS               = -j8
+
+autotest_TESTS         += $(top_srcdir)/tests.at
+
+###----- That's all, folks --------------------------------------------------
diff --git a/t/atlocal.in b/t/atlocal.in
new file mode 100644 (file)
index 0000000..638aace
--- /dev/null
@@ -0,0 +1,36 @@
+### -*-sh-*-
+###
+### Configuration variables interesting to the test suite
+###
+### (c) 2020 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+###
+### Runlisp 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.
+###
+### Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+###--------------------------------------------------------------------------
+### Configuration snippets.
+
+## Lisp systems.
+SBCL=@SBCL@
+CCL=@CCL@
+CLISP=@CLISP@
+ECL=@ECL@
+CMUCL=@CMUCL@
+ABCL=@ABCL@
+
+###----- That's all, folks --------------------------------------------------
diff --git a/t/autotest.am b/t/autotest.am
new file mode 120000 (symlink)
index 0000000..2309b1e
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/build/autotest.am
\ No newline at end of file
diff --git a/t/package.m4 b/t/package.m4
new file mode 100644 (file)
index 0000000..e43d152
--- /dev/null
@@ -0,0 +1,6 @@
+### package information
+m4_define([AT_PACKAGE_NAME],      [runlisp])
+m4_define([AT_PACKAGE_TARNAME],   [runlisp])
+m4_define([AT_PACKAGE_VERSION],   [UNKNOWN])
+m4_define([AT_PACKAGE_STRING],    [runlisp UNKNOWN])
+m4_define([AT_PACKAGE_BUGREPORT], [mdw@distorted.org.uk])
diff --git a/t/tests.m4 b/t/tests.m4
new file mode 100644 (file)
index 0000000..acb8e73
--- /dev/null
@@ -0,0 +1 @@
+TESTS([..], [tests.at])
diff --git a/t/testsuite.at b/t/testsuite.at
new file mode 120000 (symlink)
index 0000000..78fa5b5
--- /dev/null
@@ -0,0 +1 @@
+../.ext/cfd/build/testsuite.at
\ No newline at end of file
diff --git a/tests.at b/tests.at
new file mode 100644 (file)
index 0000000..3eee970
--- /dev/null
+++ b/tests.at
@@ -0,0 +1,348 @@
+### -*-autotest-*-
+###
+### Test script for `runlisp'
+###
+### (c) 2020 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+###
+### Runlisp 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.
+###
+### Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+m4_define([RUNLISP_PATH], [$abs_top_builddir/runlisp])
+
+m4_define([_FOREACH], [dnl
+m4_if([$#], [1], [_foreach_func($1)],
+       [_foreach_func($1)[]_FOREACH(m4_shift($@))])])
+m4_define([FOREACH], [dnl
+m4_pushdef([_foreach_func], [$2])dnl
+_FOREACH($1)[]dnl
+m4_popdef([_foreach_func])])
+
+m4_define([LISP_SYSTEMS],
+  [sbcl, sbcl/noimage,
+   ccl, ccl/noimage,
+   clisp, clisp/noimage,
+   ecl, ecl/noimage,
+   cmucl, cmucl/noimage,
+   abcl, abcl/noimage])
+
+m4_define([PREPARE_LISP_TEST],
+[lisp=$1
+LISP=$m4_translit(m4_bregexp([$1], [/.*$], []), [a-z], [A-Z])
+AT_SKIP_IF([test "x$LISP" = x])
+case $lisp in
+  */*) opt=${lisp#*/} lisp=${lisp%%/*} ;;
+  *) opt="" ;;
+esac
+case /$opt/ in
+  */noimage/*) RUNLISP_IMAGEDIR=./notexist ;;
+  *) RUNLISP_IMAGEDIR=$abs_top_builddir ;;
+esac
+export RUNLISP_IMAGEDIR])
+
+m4_define([WHICH_LISP],
+[(or #+sbcl "sbcl" #+ccl "ccl" #+clisp "clisp"
+     #+ecl "ecl" #+cmu "cmucl" #+abcl "abcl"
+     "unknown")])
+
+m4_define([NL], [
+])
+
+m4_define([SETUP_RUNLISP_IMAGEDIR],
+[RUNLISP_IMAGEDIR=$abs_top_builddir; export RUNLISP_IMAGEDIR])
+
+m4_define([SETUP_RUNLISP_EVAL],
+[RUNLISP_EVAL=$abs_top_srcdir/eval.lisp; export RUNLISP_EVAL])
+
+###--------------------------------------------------------------------------
+### A basic smoke test.
+
+## Check that the system basically works, by running a trivial test program.
+## Also try to verify that we're not running user or site startup code,
+## though this is hard to do in general.
+FOREACH([LISP_SYSTEMS],
+[AT_SETUP([$1 smoke])
+AT_KEYWORDS([script smoke $1])
+PREPARE_LISP_TEST([$1])
+
+## Prepare a user-init file which will break the test if it's run by printing
+## something unexpected.
+mkdir HOME
+case $lisp in
+  sbcl) initfile=.sbclrc ;;
+  ccl) initfile=.ccl-init.lisp ;;
+  clisp) initfile=.clisprc.lisp ;;
+  ecl) initfile=.eclrc ;;
+  cmucl) initfile=.cmucl-init.lisp ;;
+  abcl) initfile=.abclrc ;;
+esac
+cat >HOME/$initfile <<EOF
+(format t "*** I should not be seen~%")
+EOF
+HOME=$(pwd)/HOME; export HOME
+
+## Prepare the script.
+cat >test-script <<EOF
+[#!] RUNLISP_PATH -L$lisp
+
+;; Print a greeting to \`*standard-output*', identifying the Lisp system, so
+;; that we can tell whether we called the right one.
+(format t "Hello from ~A (~A)!~%" (lisp-implementation-type) WHICH_LISP)
+
+#! this should be a comment everywhere
+
+;; Make sure that \`*error-output*' is hooked up properly.
+(format *error-output* "to stderr~%")
+
+;; Make sure that \`*standard-input*' is hooked up properly, by reading a line
+;; and echoing it.
+(format t "from stdin: ~S~%" (read-line))
+
+;; Check that \`:runlisp-script' is set in \`*features*'.  If not, \`assert'
+;; will at least write a complaint to some stream, which will fail the test.
+(assert (member :runlisp-script *features*))
+
+;; Check that there are no symbols present (interned or imported) in the
+;; \`common-lisp-user' package.  Obviously, we must avoid interning any
+;; ourselves.  Alas, ABCL and ECL pollute \`cl-user' out of the box.  (ECL
+;; does this deliberately; ABCL's ``adjoin.lisp' lacks an \`in-package' form.
+(let ((#1=#:syms (sort (loop :for #2=#:s :being :the :present-symbols
+                              :of *package*
+                            :collect #2#)
+                      #'string<)))
+  (format t "package \`~A' [~:[ok~;has unexpected symbols ~:*~S~]]~%"
+         (package-name *package*) #1#))
+
+;; Print the program name and command-line arguments.
+(format t "program name = ~S~%~
+          arguments = ~:S~%"
+       (uiop:argv0)
+       uiop:*command-line-arguments*)
+EOF
+chmod +x test-script
+
+case $lisp in
+  sbcl) impl="SBCL" ;;
+  ccl) impl="Clozure Common Lisp" ;;
+  clisp) impl="CLISP" ;;
+  ecl) impl="ECL" ;;
+  cmucl) impl="CMU Common Lisp" ;;
+  abcl) impl="Armed Bear Common Lisp" ;;
+  *) AT_FAIL_IF([:]) ;;
+esac
+
+## Prepare an input file.
+echo some random text >stdin
+
+## Prepare the reference stdout and stderr.
+cat >stdout.ref <<EOF
+Hello from $impl ($lisp)!
+from stdin: "some random text"
+package \`COMMON-LISP-USER' ok
+program name = "./test-script"
+arguments = ("--eval" "nonsense" "--" "more" "args" "here")
+EOF
+cat >stderr.ref <<EOF
+to stderr
+EOF
+
+AT_CHECK([echo "lisp=$lisp opt=$opt"; env | grep RUNLISP | sort],, [stdout])
+AT_CHECK([./test-script --eval nonsense -- more args here <stdin],,
+        [stdout], [stderr])
+AT_CHECK([diff -u stdout.ref stdout])
+AT_CHECK([diff -u stderr.ref stderr])
+AT_CLEANUP])
+
+###--------------------------------------------------------------------------
+### Check error handling.
+
+FOREACH([LISP_SYSTEMS],
+[AT_SETUP([$1 errors])
+AT_KEYWORDS([script error $1])
+PREPARE_LISP_TEST([$1])
+
+## A simple script which signals an error without catching it.
+cat >test <<EOF
+[#!] RUNLISP_PATH -L$lisp
+(error "just kill me now")
+EOF
+chmod +x test
+
+## As long as it exits with a nonzero status, I'm happy.  Some Lisps
+## desperately want to drop the user into an interactive debugger, which is
+## possibly useful for a developer, but an end user is now faced with a
+## confusing internal error message /and/ a confusing prompt which won't go
+## away.  The output may still be confusing and (certainly in CCL's case)
+## voluminous, but that's not significantly worse than Tcl or Java.
+./test >out >err; rc=$?
+AT_FAIL_IF([test $rc = 0])
+
+AT_CLEANUP])
+
+###--------------------------------------------------------------------------
+### Check eval mode.
+
+### Eval mode is implemented centrally through a script, so we don't need to
+### test it separately for each Lisp implementation.
+
+AT_SETUP([eval mode])
+AT_KEYWORDS([eval common])
+SETUP_RUNLISP_IMAGEDIR
+SETUP_RUNLISP_EVAL
+
+## A very basic smoke test.
+AT_CHECK([RUNLISP_PATH -e '(format t "Just another Lisp hacker!~%")'],,
+[Just another Lisp hacker!
+])
+
+## The `:runlisp-script' keyword should /not/ be in `*features*'.
+traceon
+AT_CHECK([RUNLISP_PATH -p '(find :runlisp-script *features*)'],, [NIL
+])
+
+## Check a mixture of all the kinds of evaluation.  We'll need a stunt script
+## to make this work.  Also check that the individual forms are read and
+## evaluated one at a time, so that each one can affect the way the reader
+## interprets the next.
+cat >script.lisp <<EOF
+#! just want to check that Lisp doesn't choke on a shebang line here
+(format t "And we're running the script...~%~
+          Command-line arguments: ~:S~%~
+          Symbols in package \`~A': ~:S~%"
+       uiop:*command-line-arguments*
+       (package-name *package*)
+       (sort (loop :for #2=#:s :being :the :present-symbols :of *package*
+                   :collect #2#)
+             #'string<))
+EOF
+AT_CHECK([RUNLISP_PATH \
+           -e '(defpackage [#:]runlisp-test (:export [#:]foo))
+               (defvar runlisp-test:foo 1)' \
+           -p runlisp-test:foo \
+           -e '(incf runlisp-test:foo)' \
+           -l script.lisp \
+           -p runlisp-test:foo \
+           -- -e one two three],,
+[1
+And we're running the script...
+Command-line arguments: ("-e" "one" "two" "three")
+Symbols in package `COMMON-LISP-USER': ()
+2
+])
+
+AT_CLEANUP
+
+###--------------------------------------------------------------------------
+### Check Lisp system selection and preference work.
+
+AT_SETUP([preferences])
+AT_KEYWORDS([prefs common])
+SETUP_RUNLISP_IMAGEDIR
+SETUP_RUNLISP_EVAL
+
+## Before we can make this happen, we need to decide on three Lisp systems,
+## two of which actually work, and one other.  These are ordered by startup
+## speed.
+unset lisp0 lisp1 badlisp; win=nil
+set -- cmucl sbcl ccl clisp ecl abcl
+while :; do
+  case $# in 0) break ;; esac
+  lisp=$1; shift
+  if RUNLISP_PATH -L$lisp -enil 2>/dev/null; then good=t; else good=nil; fi
+  case ${lisp0+t},${badlisp+t},$good in
+    ,*,t) lisp0=$lisp ;;
+    t,*,t) lisp1=$lisp win=t; break ;;
+    *,,nil) badlisp=$lisp ;;
+  esac
+done
+AT_CHECK([case $win in nil) exit 77 ;; esac])
+case ${badlisp+t} in t) ;; *) badlisp=$1 ;; esac
+BADLISP=$(echo $badlisp | tr a-z A-Z)
+eval $BADLISP=/notexist/definitely-wrong
+export $BADLISP
+echo Primary Lisp = $lisp0
+echo Secondary Lisp = $lisp1
+echo Bad Lisp = $badlisp
+
+## Check that our selection worked.
+AT_CHECK_UNQUOTED([RUNLISP_PATH -L$lisp0 -p 'WHICH_LISP'],, ["$lisp0"NL])
+AT_CHECK_UNQUOTED([RUNLISP_PATH -L$lisp1 -p 'WHICH_LISP'],, ["$lisp1"NL])
+AT_CHECK([RUNLISP_PATH -L$badlisp -p 'WHICH_LISP'], [127],,
+[runlisp: no supported Lisp systems found[]NL])
+
+## Unset all of the user preference mechanisms.
+unset RUNLISP_OPTIONS
+here=$(pwd)
+mkdir HOME config
+HOME=$here/HOME XDG_CONFIG_HOME=$here/config; export HOME XDG_CONFIG_HOME
+
+## We generally take the first one listed that exists.
+AT_CHECK_UNQUOTED([RUNLISP_PATH -L$lisp0,$lisp1 -p 'WHICH_LISP'],, ["$lisp0"NL])
+AT_CHECK_UNQUOTED([RUNLISP_PATH -L$lisp1,$lisp0 -p 'WHICH_LISP'],, ["$lisp1"NL])
+AT_CHECK_UNQUOTED([RUNLISP_PATH -L$badlisp,$lisp0,$lisp1 -p 'WHICH_LISP'],,
+                 ["$lisp0"NL])
+
+## Check parsing of embedded options.
+for i in 0 1; do
+  j=$(( 1 - $i )); eval lisp=\$lisp$i olisp=\$lisp$j
+  cat >script$i <<EOF
+[#!] RUNLISP_PATH
+;;; -z @RUNLISP: -L$lisp -*- -z -*- -L$olisp -- -z
+(prin1 WHICH_LISP) (terpri)
+EOF
+  chmod +x script$i
+  AT_CHECK_UNQUOTED([./script$i],, ["$lisp"NL])
+done
+
+## Preferences will override the order of acceptable implementations.
+AT_CHECK_UNQUOTED([RUNLISP_OPTIONS=-P$badlisp,$lisp0 ./script0],, ["$lisp0"NL])
+AT_CHECK_UNQUOTED([RUNLISP_OPTIONS=-P$badlisp,$lisp0 ./script1],, ["$lisp0"NL])
+
+## But doesn't affect the preference order of unmentioned Lisps.
+AT_CHECK_UNQUOTED([RUNLISP_OPTIONS=-P$badlisp ./script0],, ["$lisp0"NL])
+AT_CHECK_UNQUOTED([RUNLISP_OPTIONS=-P$badlisp ./script1],, ["$lisp1"NL])
+
+## Test configuration files and interactions with the environment.
+for conf in HOME/.runlisprc config/runlisprc; do
+  for i in 0 1; do
+    j=$(( 1 - $i )); eval lisp=\$lisp$i olisp=\$lisp$j
+    cat >$conf <<EOF
+### -*-conf-*-
+-P$lisp
+EOF
+
+    ## Basic check.
+    AT_CHECK_UNQUOTED([./script0],, ["$lisp"NL])
+    AT_CHECK_UNQUOTED([./script1],, ["$lisp"NL])
+
+    ## Environment variable only appends.
+    AT_CHECK_UNQUOTED([RUNLISP_OPTIONS=-P$olisp ./script0],, ["$lisp"NL])
+    AT_CHECK_UNQUOTED([RUNLISP_OPTIONS=-P$olisp ./script1],, ["$lisp"NL])
+
+    ## But we can clear the preferred list.
+    AT_CHECK_UNQUOTED([RUNLISP_OPTIONS="-C -P$olisp" ./script0],, ["$olisp"NL])
+    AT_CHECK_UNQUOTED([RUNLISP_OPTIONS="-C -P$olisp" ./script1],, ["$olisp"NL])
+
+  done
+  rm -f $conf
+done
+
+
+
+AT_CLEANUP
+
+###----- That's all, folks --------------------------------------------------
diff --git a/toy-runlisp b/toy-runlisp
new file mode 100755 (executable)
index 0000000..0723aff
--- /dev/null
@@ -0,0 +1,85 @@
+#! /bin/sh -e
+
+case $# in
+  0 | 1) echo >&2 "usage: $0 LISP SCRIPT [ARGS ...]"; exit 127 ;;
+esac
+lisp=$1 script=$2; shift 2
+
+__CL_ARGV0=$script; export __CL_ARGV0 # this is stupid
+
+lispscript=$(printf "%s" "$script" | sed 's/[\"]/\\&/g')
+
+load_asdf_rune="\
+(let ((*load-verbose* nil)
+      #+cmu (ext:*require-verbose* nil))
+  (require \"asdf\"))"
+
+ignore_shebang_rune="\
+(set-dispatch-macro-character
+ #\# #\!
+ (lambda (stream char arg)
+   (declare (ignore char arg))
+   (values (read-line stream))))"
+
+clisp_startup_rune="\
+(progn
+  $ignore_shebang_rune
+  $load_asdf_rune
+  (setf *standard-input* (ext:make-stream :input))
+  (load \"$lispscript\" :verbose nil :print nil)
+  (ext:quit))"
+
+abcl_startup_rune="\
+(let ((script \"$lispscript\"))
+  $load_asdf_rune
+  $ignore_shebang_rune
+  (setf *error-output*
+         (java:jnew \"org.armedbear.lisp.Stream\"
+                    'sys::system-stream
+                    (java:jfield \"java.lang.System\" \"err\")
+                    'character
+                    java:+true+))
+  (handler-case (load script :verbose nil :print nil)
+    (error (error)
+      (format *error-output* \"~A (unhandled error): ~A~%\" script error)
+    (ext:quit :status 255))))"
+
+#set -x
+case $lisp in
+
+  sbcl)
+    exec sbcl --noinform --eval "$load_asdf_rune" --script "$script" "$@"
+    ;;
+
+  ecl)
+    exec ecl --norc --eval "$load_asdf_rune" --shell "$script" -- "$@"
+    ;;
+
+  clisp)
+    exec clisp -norc -q -x "$clisp_startup_rune" -- "$@"
+    ;;
+
+  cmucl)
+    exec cmucl -batch -noinit -nositeinit -quiet \
+        -eval "$load_asdf_rune" \
+        -eval "$ignore_shebang_rune" \
+        -load "$script" -eval "(ext:quit)" -- "$@"
+    ;;
+
+  ccl)
+    exec ccl -b -n -Q \
+        -e "$load_asdf_rune" \
+        -e "$ignore_shebang_rune" \
+        -l "$script" -e "(ccl:quit)" -- "$@"
+    ;;
+
+  abcl)
+    exec abcl --batch --noinform --noinit --nosystem \
+        --eval "$abcl_startup_rune" -- "$@"
+    ;;
+
+  *)
+    echo >&2 "$0: unsupported Lisp \`$lisp'"
+    exit 127
+    ;;
+esac
diff --git a/vars.am b/vars.am
new file mode 100644 (file)
index 0000000..4c9e164
--- /dev/null
+++ b/vars.am
@@ -0,0 +1,71 @@
+### -*-makefile-*-
+###
+### Common build-system definitions
+###
+### (c) 2020 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+###
+### Runlisp 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.
+###
+### Runlisp 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 Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+
+###--------------------------------------------------------------------------
+### Initial values for common variables.
+
+EXTRA_DIST              =
+CLEANFILES              =
+
+bin_PROGRAMS            =
+bin_SCRIPTS             =
+nodist_bin_SCRIPTS      =
+
+man_MANS                =
+
+noinst_PROGRAMS                 =
+noinst_DATA             =
+
+###--------------------------------------------------------------------------
+### Standard configuration substitutions.
+
+## Substitute tags in files.
+confsubst               = $(top_srcdir)/config/confsubst
+
+SUBSTITUTIONS = \
+       prefix=$(prefix) exec_prefix=$(exec_prefix) \
+       libdir=$(libdir) includedir=$(includedir) \
+       bindir=$(bindir) sbindir=$(sbindir) \
+       imagedir=$(imagedir) \
+       PACKAGE=$(PACKAGE) VERSION=$(VERSION) \
+       ECLOPT=$(ECLOPT)
+
+v_subst                         = $(v_subst_@AM_V@)
+v_subst_                = $(v_subst_@AM_DEFAULT_V@)
+v_subst_0               = @echo "  SUBST    $@";
+SUBST                   = $(v_subst)$(confsubst)
+
+###--------------------------------------------------------------------------
+### List of Lisp systems.
+
+LISPS                   =
+
+LISPS                  += sbcl
+LISPS                  += ccl
+LISPS                  += clisp
+LISPS                  += ecl
+LISPS                  += cmucl
+LISPS                  += abcl
+
+###----- That's all, folks --------------------------------------------------