@@@ work in progress
[runlisp] / runlisp.c
CommitLineData
e29834b8
MW
1/* -*-c-*-
2 *
7b8ff279 3 * Invoke Lisp scripts and implementations
e29834b8
MW
4 *
5 * (c) 2020 Mark Wooding
6 */
7
8/*----- Licensing notice --------------------------------------------------*
9 *
10 * This file is part of Runlisp, a tool for invoking Common Lisp scripts.
11 *
12 * Runlisp is free software: you can redistribute it and/or modify it
13 * under the terms of the GNU General Public License as published by the
14 * Free Software Foundation; either version 3 of the License, or (at your
15 * option) any later version.
16 *
17 * Runlisp is distributed in the hope that it will be useful, but WITHOUT
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
20 * for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with Runlisp. If not, see <https://www.gnu.org/licenses/>.
24 */
25
26/*----- Header files ------------------------------------------------------*/
27
28#include "config.h"
29
e29834b8
MW
30#include <ctype.h>
31#include <errno.h>
e29834b8
MW
32#include <stdio.h>
33#include <stdlib.h>
34#include <string.h>
35
7b8ff279
MW
36#include "common.h"
37#include "lib.h"
38#include "mdwopt.h"
e29834b8 39
7b8ff279 40/*----- Static data -------------------------------------------------------*/
e29834b8 41
7b8ff279
MW
42struct lispsys {
43 struct treap_node _node;
44 struct lispsys *next_lisp, *next_accept, *next_prefer, *next_order;
e29834b8 45 unsigned f;
7b8ff279
MW
46#define LF_KNOWN 1u
47#define LF_ACCEPT 2u
48#define LF_PREFER 4u
49 struct config_section *sect;
50 struct config_var *var;
e29834b8 51};
7b8ff279
MW
52#define LISPSYS_NAME(lisp) TREAP_NODE_KEY(lisp)
53#define LISPSYS_NAMELEN(lisp) TREAP_NODE_KEYLEN(lisp)
e29834b8 54
7b8ff279
MW
55struct lispsys_list {
56 struct lispsys *head, **tail;
e29834b8 57};
e29834b8 58
7b8ff279
MW
59static struct argv argv_tail = ARGV_INIT;
60const char *script = 0;
61
62static unsigned flags = 0;
63#define AF_CMDLINE 0x0000u
64#define AF_EMBED 0x0001u
65#define AF_ENV 0x0002u
66#define AF_CONF 0x0003u
67#define AF_STATEMASK 0x000fu
68#define AF_BOGUS 0x0010u
69#define AF_SETCONF 0x0020u
70#define AF_NOEMBED 0x0040u
71#define AF_DRYRUN 0x0080u
72#define AF_VANILLA 0x0100u
73
74struct treap lispsys = TREAP_INIT;
75static struct lispsys_list
76 lisps = { 0, &lisps.head },
77 accept = { 0, &accept.head },
78 prefer = { 0, &prefer.head };
e29834b8 79
7b8ff279 80/*----- Main code ---------------------------------------------------------*/
e29834b8 81
7b8ff279
MW
82static void version(FILE *fp)
83 { fprintf(fp, "%s, version %s\n", progname, PACKAGE_VERSION); }
e29834b8 84
7b8ff279 85static void usage(FILE *fp)
e29834b8 86{
7b8ff279
MW
87 fprintf(fp, "\
88usage:\n\
89 %s [OPTIONS] [--] SCRIPT [ARGUMENTS ...]\n\
90 %s [OPTIONS] [-e EXPR] [-p EXPR] [-l FILE] [--] [ARGUMENTS ...]\n\
91OPTIONS:\n\
92 [-CDEnqv] [+DEn] [-L SYS,SYS,...] [-c CONF] [-o [SECT:]VAR=VAL]\n",
93 progname, progname);
e29834b8
MW
94}
95
7b8ff279 96static void help(FILE *fp)
e29834b8 97{
7b8ff279
MW
98 version(fp); fputc('\n', fp); usage(fp);
99 fputs("\n\
100Help options:\n\
101 -h, --help Show this help text and exit successfully.\n\
102 -V, --version Show version number and exit successfully.\n\
103\n\
104Diagnostics:\n\
105 -n, --dry-run Don't run run anything (useful with `-v').\n\
106 -q, --quiet Don't print warning messages.\n\
107 -v, --verbose Print informational messages (repeatable).\n\
108\n\
109Configuration:\n\
110 -E, --command-line-only Don't read embedded options from script.\n\
111 -c, --config-file=CONF Read configuration from CONF (repeatable).\n\
112 -o, --set-option=[SECT:]VAR=VAL Set configuration variable (repeatable).\n\
113\n\
114Lisp implementation selection:\n\
115 -D, --vanilla-image Run vanilla Lisp images, not custom ones.\n\
116 -L, --accept-lisp=SYS,SYS,... Only use the listed Lisp systems.\n\
117\n\
118Evaluation mode:\n\
119 -e, --evaluate-expression=EXPR Evaluate EXPR for effect (repeatable).\n\
120 -l, --load-file=FILE Load FILE (repeatable).\n\
121 -p, --print-expression=EXPR Print (`prin1') EXPR (repeatable).\n",
122 fp);
e29834b8 123}
e29834b8 124
7b8ff279 125static struct lispsys *ensure_lispsys(const char *name, size_t n)
e29834b8 126{
7b8ff279
MW
127 struct lispsys *lisp;
128 struct treap_path path;
e29834b8 129
7b8ff279
MW
130 lisp = treap_probe(&lispsys, name, n, &path);
131 if (!lisp) {
132 lisp = xmalloc(sizeof(*lisp));
133 lisp->f = 0; lisp->sect = 0;
134 treap_insert(&lispsys, &path, &lisp->_node, name, n);
e29834b8 135 }
7b8ff279 136 return (lisp);
e29834b8
MW
137}
138
7b8ff279
MW
139#define LISP_LINK(lisp, linkoff) \
140 ((struct lispsys **)((unsigned char *)(lisp) + (linkoff)))
e29834b8 141
7b8ff279
MW
142static void add_lispsys(const char *p, const char *what,
143 struct lispsys_list *list,
144 unsigned flag, size_t linkoff)
e29834b8 145{
7b8ff279
MW
146 struct lispsys *lisp, **link;
147 const char *q;
e29834b8 148
7b8ff279 149 if (!*p) return;
e29834b8 150 for (;;) {
e29834b8 151 if (!*p) break;
7b8ff279
MW
152 q = p; while (*p && *p != ',') p++;
153 lisp = ensure_lispsys(q, p - q);
154 if (lisp->f&flag) {
155 if (verbose >= 1)
156 moan("ignoring duplicate %s Lisp `%.*s'", what, (int)(p - q), q);
157 } else {
158 link = LISP_LINK(lisp, linkoff);
159 lisp->f |= flag; *link = 0;
160 *list->tail = lisp; list->tail = link;
e29834b8 161 }
7b8ff279
MW
162 if (!*p) break;
163 p++;
e29834b8
MW
164 }
165}
166
7b8ff279
MW
167static void check_lisps(const char *what,
168 struct lispsys_list *list, size_t linkoff)
e29834b8 169{
7b8ff279 170 struct lispsys *lisp;
e29834b8 171
7b8ff279
MW
172 for (lisp = list->head; lisp; lisp = *LISP_LINK(lisp, linkoff))
173 if (!(lisp->f&LF_KNOWN))
174 lose("unknown Lisp implementation `%s'", LISPSYS_NAME(lisp));
e29834b8
MW
175}
176
7b8ff279
MW
177static void dump_lisps(const char *what,
178 struct lispsys_list *list, size_t linkoff)
e29834b8
MW
179{
180 struct dstr d = DSTR_INIT;
7b8ff279
MW
181 struct lispsys *lisp;
182 int first;
183
184 first = 1;
185 for (lisp = list->head; lisp; lisp = *LISP_LINK(lisp, linkoff)) {
186 if (first) first = 0;
187 else dstr_puts(&d, ", ");
188 dstr_putf(&d, "`%s'", LISPSYS_NAME(lisp));
e29834b8 189 }
7b8ff279
MW
190 if (first) dstr_puts(&d, "(none)");
191 dstr_putz(&d);
192 moan("%s: %s", what, d.p);
e29834b8
MW
193 dstr_release(&d);
194}
195
7b8ff279 196static void push_eval_op(char op, const char *val)
e29834b8
MW
197{
198 char *p;
199 size_t n;
200
7b8ff279 201 if ((flags&AF_STATEMASK) != AF_CMDLINE) {
e29834b8 202 moan("must use `-e', `-p', or `-l' on command line");
7b8ff279 203 flags |= AF_BOGUS;
e29834b8
MW
204 return;
205 }
206
207 n = strlen(val) + 1;
208 p = xmalloc(n + 1);
209 p[0] = op; memcpy(p + 1, val, n);
7b8ff279 210 argv_append(&argv_tail, p);
e29834b8
MW
211}
212
7b8ff279 213static void parse_options(int argc, char *argv[])
e29834b8 214{
7b8ff279 215 int i;
e29834b8 216
7b8ff279
MW
217 static const struct option opts[] = {
218 { "help", 0, 0, 'h' },
219 { "version", 0, 0, 'V' },
220 { "vanilla-image", OPTF_NEGATE, 0, 'D' },
221 { "command-line-only", OPTF_NEGATE, 0, 'E' },
222 { "accept-lisp", OPTF_ARGREQ, 0, 'L' },
223 { "config-file", OPTF_ARGREQ, 0, 'c' },
224 { "evaluate-expression", OPTF_ARGREQ, 0, 'e' },
225 { "load-file", OPTF_ARGREQ, 0, 'l' },
226 { "dry-run", OPTF_NEGATE, 0, 'n' },
227 { "set-option", OPTF_ARGREQ, 0, 'o' },
228 { "print-expression", OPTF_ARGREQ, 0, 'p' },
229 { "quiet", 0, 0, 'q' },
230 { "verbose", 0, 0, 'v' },
231 { 0, 0, 0, 0 }
232 };
233
234 optarg = 0; optind = 0; optprog = (/*unconst*/ char *)progname;
e29834b8 235 for (;;) {
7b8ff279
MW
236 i = mdwopt(argc, argv, "+hVD+E+L:c:e:l:n+o:p:qv", opts, 0, 0,
237 OPTF_NEGATION | OPTF_NOPROGNAME);
238 if (i < 0) break;
239 switch (i) {
240 case 'h': help(stdout); exit(0);
241 case 'V': version(stdout); exit(0);
242 case 'D': flags |= AF_VANILLA; break;
243 case 'D' | OPTF_NEGATED: flags &= ~AF_VANILLA; break;
244 case 'E': flags |= AF_NOEMBED; break;
245 case 'E' | OPTF_NEGATED: flags &= ~AF_NOEMBED; break;
246 case 'L':
247 add_lispsys(optarg, "acceptable", &accept, LF_ACCEPT,
248 offsetof(struct lispsys, next_accept));
249 break;
250 case 'c': read_config_path(optarg, 0); flags |= AF_SETCONF; break;
251 case 'e': push_eval_op('!', optarg); break;
252 case 'l': push_eval_op('<', optarg); break;
253 case 'n': flags |= AF_DRYRUN; break;
254 case 'n' | OPTF_NEGATED: flags &= ~AF_DRYRUN; break;
255 case 'o': if (set_config_var(optarg)) flags |= AF_BOGUS; break;
256 case 'p': push_eval_op('?', optarg); break;
257 case 'q': if (verbose) verbose--; break;
258 case 'v': verbose++; break;
259 default: flags |= AF_BOGUS; break;
e29834b8 260 }
e29834b8 261 }
e29834b8
MW
262}
263
7b8ff279 264static void handle_embedded_args(const char *script)
e29834b8
MW
265{
266 struct dstr d = DSTR_INIT;
7b8ff279
MW
267 struct argv av = ARGV_INIT;
268 char *p, *q, *r; const char *l;
269 size_t n;
270 int qstate = 0;
e29834b8
MW
271 FILE *fp = 0;
272
273 fp = fopen(script, "r");
274 if (!fp) lose("can't read script `%s': %s", script, strerror(errno));
275
276 if (dstr_readline(&d, fp)) goto end;
277 dstr_reset(&d); if (dstr_readline(&d, fp)) goto end;
278
7b8ff279
MW
279 p = strstr(d.p, "@RUNLISP:"); if (!p) goto end;
280 p += 9; q = p; l = d.p + d.len;
281 for (;;) {
282 while (p < l && ISSPACE(*p)) p++;
283 if (p >= l) break;
284 if (l - p >= 3 && p[0] == '-' && p[1] == '*' && p[2] == '-') {
285 p = strstr(p + 3, "-*-");
286 if (!p || p + 3 > l)
287 lose("%s:2: unfinished local-variables list", script);
288 p += 3;
289 continue;
290 }
291 if (l - p >= 2 && p[0] == '-' && p[1] == '-' &&
292 (l == p + 2 || ISSPACE(p[2])))
293 break;
e29834b8 294
7b8ff279
MW
295 argv_append(&av, q);
296 while (p < l && (qstate || !ISSPACE(*p))) {
297 if (*p == '"') { p++; qstate = !qstate; }
298 else if (*p == '\\') {
299 p++; if (p >= l) lose("%s:2: unfinished `\\' escape", script);
300 *q++ = *p++;
301 } else if (*p == '\'') {
302 p++; r = strchr(p, '\'');
303 if (!r || r > l) lose("%s:2: missing `''", script);
304 n = r - p; memmove(q, p, n); q += n; p = r + 1;
305 } else {
306 n = strcspn(p, qstate ? "\"\\" : "\"'\\ \f\n\r\t\v");
307 if (n > l - p) n = l - p;
308 memmove(q, p, n); q += n; p += n;
309 }
e29834b8 310 }
7b8ff279
MW
311 if (qstate) lose("%s:2: missing `\"'", script);
312 if (p < l) p++;
313 *q++ = 0;
e29834b8 314 }
7b8ff279
MW
315
316 flags = (flags&~AF_STATEMASK) | AF_EMBED;
317 parse_options(av.n, (char * /*unconst*/*)av.v);
318 if (optind < av.n)
319 lose("%s:2: positional argument `%s' not permitted here",
320 script, av.v[optind]);
e29834b8
MW
321
322end:
323 if (fp) {
324 if (ferror(fp))
7b8ff279 325 lose("error reading script `%s': %s", script, strerror(errno));
e29834b8
MW
326 fclose(fp);
327 }
7b8ff279 328 dstr_release(&d); argv_release(&av);
e29834b8
MW
329}
330
331int main(int argc, char *argv[])
332{
7b8ff279
MW
333 struct config_section_iter si;
334 struct config_section *sect;
335 struct config_var *var;
336 struct lispsys_list order;
337 struct lispsys *lisp, **tail;
e29834b8 338 struct dstr d = DSTR_INIT;
7b8ff279 339 struct argv av = ARGV_INIT;
e29834b8 340
7b8ff279 341 set_progname(argv[0]);
e29834b8 342
7b8ff279
MW
343 init_config();
344 config_set_var(&config, toplevel, 0, "prefer", "${@ENV:RUNLISP_PREFER?}");
e29834b8 345
7b8ff279
MW
346 flags = (flags&~AF_STATEMASK) | AF_CMDLINE;
347 parse_options(argc - 1, argv + 1); optind++;
e29834b8 348
7b8ff279
MW
349 if (argv_tail.n)
350 flags |= AF_NOEMBED;
351 else if (!script && !argv_tail.n) {
352 if (optind < argc) script = argv[optind]++;
353 else flags |= AF_BOGUS;
e29834b8
MW
354 }
355
7b8ff279
MW
356 argc -= optind; argv += optind;
357 if (argv_tail.n) {
358 argv_append(&argv_tail, "--");
359 argv_appendn(&argv_tail, (const char *const *)argv, argc);
360 argc = argv_tail.n; argv = (/*unconst*/ char */*unconst*/ *)argv_tail.v;
e29834b8
MW
361 }
362
7b8ff279
MW
363 if (flags&AF_BOGUS) { usage(stderr); exit(2); }
364 if (!(flags&AF_NOEMBED)) handle_embedded_args(script);
365 if (!(flags&AF_SETCONF)) load_default_config();
366 if (verbose >= 5) dump_config();
e29834b8 367
7b8ff279
MW
368 dstr_reset(&d);
369 var = config_find_var(&config, toplevel, CF_INHERIT, "prefer");
370 config_subst_var(&config, toplevel, var, &d);
371 add_lispsys(d.p, "preferred", &prefer, LF_PREFER,
372 offsetof(struct lispsys, next_prefer));
e29834b8 373
7b8ff279
MW
374 if (!script)
375 script = config_subst_string_alloc
376 (&config, common, "<internal>",
377 "${@ENV:RUNLISP_EVAL?${@CONFIG:data-dir}/eval.lisp}");
e29834b8 378
e29834b8
MW
379 if (setenv("__CL_ARGV0", script, 1))
380 lose("failed to set script-name environment variable");
7b8ff279
MW
381 config_set_var(&config, builtin, CF_LITERAL, "@SCRIPT", script);
382
383 tail = lisps.tail;
384 for (config_start_section_iter(&config, &si);
385 (sect = config_next_section(&si)); ) {
386 var = config_find_var(&config, sect, CF_INHERIT, "run-script");
387 if (!var) continue;
388 lisp = ensure_lispsys(CONFIG_SECTION_NAME(sect),
389 CONFIG_SECTION_NAMELEN(sect));
390 lisp->f |= LF_KNOWN; lisp->sect = sect; lisp->var = var;
391 *tail = lisp; tail = &lisp->next_lisp;
392 }
393 *tail = 0; lisps.tail = tail;
394
395 check_lisps("acceptable", &accept, offsetof(struct lispsys, next_accept));
396 check_lisps("preferred", &prefer, offsetof(struct lispsys, next_prefer));
397
398 if (!accept.head) {
399 if (verbose >= 2)
400 moan("no explicitly acceptable implementations: allowing all");
401 tail = accept.tail;
402 for (lisp = lisps.head; lisp; lisp = lisp->next_lisp)
403 { lisp->f |= LF_ACCEPT; *tail = lisp; tail = &lisp->next_accept; }
404 *tail = 0; accept.tail = tail;
405 }
e29834b8 406
7b8ff279
MW
407 tail = &order.head;
408 for (lisp = prefer.head; lisp; lisp = lisp->next_prefer)
409 if (lisp->f&LF_ACCEPT) { *tail = lisp; tail = &lisp->next_order; }
410 for (lisp = accept.head; lisp; lisp = lisp->next_accept)
411 if (!(lisp->f&LF_PREFER)) { *tail = lisp; tail = &lisp->next_order; }
412 *tail = 0;
413
414 if (verbose >= 4)
415 dump_lisps("known Lisps", &lisps, offsetof(struct lispsys, next_lisp));
416 if (verbose >= 3) {
417 dump_lisps("acceptable Lisps", &accept,
418 offsetof(struct lispsys, next_accept));
419 dump_lisps("preferred Lisps", &prefer,
420 offsetof(struct lispsys, next_prefer));
421 dump_lisps("overall preference order", &order,
422 offsetof(struct lispsys, next_order));
423 }
e29834b8 424
7b8ff279
MW
425 for (lisp = order.head; lisp; lisp = lisp->next_order) {
426 if (config_find_var(&config, lisp->sect, CF_INHERIT, "image-file")) {
427 var = config_find_var(&config, lisp->sect, CF_INHERIT, "image-path");
428 dstr_reset(&d); config_subst_var(&config, lisp->sect, var, &d);
429 if (file_exists_p(d.p, verbose >= 2 ? FEF_VERBOSE : 0))
430 config_set_var(&config, lisp->sect, CF_LITERAL, "@IMAGE", "t");
e29834b8 431 }
7b8ff279
MW
432 argv_reset(&av);
433 config_subst_split_var(&config, lisp->sect, lisp->var, &av);
434 if (!av.n) {
435 moan("empty command for Lisp implementation `%s'", LISPSYS_NAME(lisp));
436 continue;
437 }
438 argv_appendn(&av, (const char *const *)argv, argc);
439 if (!try_exec(&av,
440 (flags&AF_DRYRUN ? TEF_DRYRUN : 0) |
441 (verbose >= 2 ? TEF_VERBOSE : 0)))
442 return (0);
443 }
e29834b8 444
7b8ff279 445 lose("no acceptable Lisp systems found");
e29834b8
MW
446}
447
448/*----- That's all, folks -------------------------------------------------*/