chiark / gitweb /
Drag build machinery into the 21st century.
[bascat] / bascat.c
1 /* -*-c-*-
2  *
3  * $Id$
4  *
5  * Display BBC BASIC programs more or less anywhere
6  *
7  * (c) 1996, 1997 Matthew Wilcox and Mark Wooding
8  */
9
10 /*----- Licensing notice --------------------------------------------------*
11  *
12  * This file is part of Bascat.
13  *
14  * Bascat is free software; you can redistribute it and/or modify it
15  * under the terms of the GNU Library General Public License as
16  * published by the Free Software Foundation; either version 2 of the
17  * License, or (at your option) any later version.
18  *
19  * Bascat is distributed in the hope that it will be useful, but
20  * WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU Library General Public License for more details.
23  *
24  * You should have received a copy of the GNU General Public License
25  * along with Bascat; if not, write to the Free Software Foundation,
26  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 
27  */
28
29 /*----- Header files ------------------------------------------------------*/
30
31 /* --- ANSI library headers --- */
32
33 #include <ctype.h>
34 #include <errno.h>
35 #include <signal.h>
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <string.h>
39
40 /* --- Operating system specific headers --- */
41
42 #ifdef HAVE_LIBTERMCAP
43 #  include <termcap.h>
44 #endif
45 #include <unistd.h>
46
47 /* --- Private headers --- */
48
49 #include "mdwopt.h"
50
51 /*----- Version information -----------------------------------------------*/
52
53 #ifdef DEBUG
54 #  define D(x) x
55 #else
56 #  define D(x)
57 #endif
58
59 /*----- Tokenisation tables -----------------------------------------------*
60  *
61  * These tables are from the BBC BASIC guide.  Additional verification
62  * carried out on an A440 with RISC OS 3.1
63  */
64
65 static const char *tok_base[] = {
66   "OTHERWISE",
67   "AND", "DIV", "EOR", "MOD", "OR", "ERROR", "LINE", "OFF",
68   "STEP", "SPC", "TAB(", "ELSE", "THEN", "*", "OPENIN", "PTR",
69   "PAGE", "TIME", "LOMEM", "HIMEM", "ABS", "ACS", "ADVAL", "ASC",
70   "ASN", "ATN", "BGET", "COS", "COUNT", "DEG", "ERL", "ERR",
71   "EVAL", "EXP", "EXT", "FALSE", "FN", "GET", "INKEY", "INSTR(",
72   "INT", "LEN", "LN", "LOG", "NOT", "OPENUP", "OPENOUT", "PI",
73   "POINT(", "POS", "RAD", "RND", "SGN", "SIN", "SQR", "TAN",
74   "TO", "TRUE", "USR", "VAL", "VPOS", "CHR$", "GET$", "INKEY$",
75   "LEFT$(", "MID$(", "RIGHT$(", "STR$", "STRING$(", "EOF", "*", "*",
76   "*", "WHEN", "OF", "ENDCASE", "ELSE", "ENDIF", "ENDWHILE", "PTR",
77   "PAGE", "TIME", "LOMEM", "HIMEM", "SOUND", "BPUT", "CALL", "CHAIN",
78   "CLEAR", "CLOSE", "CLG", "CLS", "DATA", "DEF", "DIM", "DRAW",
79   "END", "ENDPROC", "ENVELOPE", "FOR", "GOSUB", "GOTO", "GCOL", "IF",
80   "INPUT", "LET", "LOCAL", "MODE", "MOVE", "NEXT", "ON", "VDU",
81   "PLOT", "PRINT", "PROC", "READ", "REM", "REPEAT", "REPORT", "RESTORE",
82   "RETURN", "RUN", "STOP", "COLOUR", "TRACE", "UNTIL", "WIDTH", "OSCLI"
83 };
84
85 static const char *tok_c6[] = {
86   "SUM", "BEAT"
87 };
88
89 static const char *tok_c7[] = {
90   "APPEND", "AUTO",
91   "CRUNCH", "DELETE", "EDIT", "HELP", "LIST", "LOAD", "LVAR", "NEW",
92   "OLD", "RENUMBER", "SAVE", "TEXTLOAD", "TEXTSAVE", "TWIN", "TWINO",
93     "INSTALL"
94 };
95
96 static const char *tok_c8[] = {
97   "CASE", "CIRCLE",
98   "FILL", "ORIGIN", "POINT", "RECTANGLE", "SWAP", "WHILE", "WAIT", "MOUSE",
99   "QUIT", "SYS", "INSTALL", "LIBRARY", "TINT", "ELLIPSE", "BEATS", "TEMPO",
100   "VOICES", "VOICE", "STEREO", "OVERLAY"
101 };
102
103 #define ITEMS(array) (sizeof(array) / sizeof((array)[0]))
104
105 /*----- Static variables --------------------------------------------------*/
106
107 enum {
108   s_keyword,                            /* Expecting a keyword next */
109   s_normal,                             /* Normal state, reading input */
110   s_comment,                            /* In a command (or literal *cmd) */
111   s_quote,                              /* Inside a quoted string */
112   s_c6, s_c7, s_c8,                     /* Various shift states */
113   s_linex, s_liney, s_linez,            /* Line number states */
114   s_dummy
115 };
116
117 static int state = s_normal;            /* Current detokenisation state */
118 static unsigned int lineno;             /* Line number */
119
120 enum {
121   f_highlight = 1,                      /* Highlight keywords and things */
122   f_linenumbers = 2,                    /* Display linenumbers on left */
123   f_tty = 4,                            /* We're writing to TTY (or pager) */
124   f_less = 8,                           /* We're piping through `less' */
125   f_dummy
126 };
127
128 static int flags;                       /* Various options flags */
129
130 #ifdef HAVE_LIBTERMCAP
131 static char termcap[2048];              /* Terminal capabilities buffer */
132 #endif
133
134 static char *pager = 0;                 /* Pointer to pager to use */
135
136 /*----- Main code ---------------------------------------------------------*/
137
138 /* --- @die@ --- *
139  *
140  * Arguments:   @char *p@ = pointer to a string
141  *
142  * Returns:     1
143  *
144  * Use:         Reports an error to the user and falls over flat on its face.
145  */
146
147 static int die(const char *p)
148 {
149   fprintf(stderr, "%s: %s\n", optprog, p);
150   return (1);
151 }
152
153 /* --- @keyword@ --- *
154  *
155  * Arguments:   @char *s@ = pointer to keyword string
156  *              @FILE *fp@ = stream to write onto
157  *
158  * Returns:     --
159  *
160  * Use:         Displays a keyword in a nice way.  There's some nasty hacking
161  *              here to make GNU's `less' work properly.  `more' appears to
162  *              cope with highlighting codes OK, so that's fine.  `less'
163  *              prefers it if we attempt to `overstrike' the bolded
164  *              characters.  What fun...
165  */
166
167 static void keyword(const char *s, FILE *fp)
168 {
169 #ifdef HAVE_LIBTERMCAP
170   if ((~flags & (f_less | f_highlight)) == 0) {
171     while (*s) {
172       putc(*s, fp);
173       putc('\b', fp);
174       putc(*s, fp);
175       s++;
176     }
177   } else {
178     static char buff[24];
179     static char *hs, *he, *p = buff;
180
181     if (!hs) {
182       if (flags & f_highlight) {
183         hs = tgetstr("md", &p);
184         he = tgetstr("me", &p);
185       } else
186         hs = he = "";
187     }
188     fputs(hs, fp);
189     fputs(s, fp);
190     fputs(he, fp);
191   }
192 #else
193   fputs(s, fp);
194 #endif
195 }
196
197 /* --- @mbtok@ --- *
198  *
199  * Arguments:   @int byte@ = the current byte
200  *              @const char *t[]@ = pointer to token table
201  *              @int n@ = number of items in token table
202  *              @FILE *fp@ = stream to write onto
203  *
204  * Returns:     0 if everything's OK
205  *
206  * Use:         Decodes multibyte tokens.
207  */
208
209 static int mbtok(int byte, const char *t[], int n, FILE *fp)
210 {
211   byte -= 0x8E;
212   if (byte >= n)
213     return (die("Bad program: invalid multibyte token"));
214   keyword(t[byte], fp);
215   state = s_normal;
216   return (0);
217 }
218
219 /* --- @decode@ --- *
220  *
221  * Arguments:   @int byte@ = byte to decode
222  *              @FILE *fp@ = stream to write onto
223  *
224  * Returns:     0 if everything's going OK
225  *
226  * Use:         Decodes a byte, changing states as necessary.
227  */
228
229 static int decode(int byte, FILE *fp)
230 {
231   switch (state) {
232
233     /* --- Tokenised states --- */
234
235     case s_keyword:
236       if (byte == '*')
237         state = s_comment;
238       else
239         state = s_normal;
240       /* Fall through here */
241
242     case s_normal:
243       if (byte >= 0x7F) {
244         switch (byte) {
245           case 0xC6:
246             state = s_c6;
247             break;
248           case 0xC7:
249             state = s_c7;
250             break;
251           case 0xC8:
252             state = s_c8;
253             break;
254           case 0x8D:
255             state = s_linex;
256             break;
257           case 0x8B:                    /* ELSE */
258           case 0x8C:                    /* THEN */
259           case 0xF5:                    /* REPEAT (a funny one) */
260             state = s_keyword;
261             goto keyword;
262             break;
263           case 0xDC:                    /* DATA */
264           case 0xF4:                    /* REM */
265             state = s_comment;
266             /* Fall through here */
267           default:
268           keyword:
269             keyword(tok_base[byte - 0x7F], fp);
270             break;
271         }
272       } else {
273         if (byte == '"')
274           state = s_quote;
275         fputc(byte, fp);
276       }
277       break;
278
279     /* --- Non-tokenised states --- */
280
281     case s_quote:
282       if (byte == '"')
283         state = s_normal;
284       /* Fall through here */
285
286     case s_comment:
287       fputc(byte, fp);
288       break;
289
290     /* --- Double-byte token states --- */
291
292     case s_c6:
293       return (mbtok(byte, tok_c6, ITEMS(tok_c6), fp));
294       break;
295
296     case s_c7:
297       return (mbtok(byte, tok_c7, ITEMS(tok_c7), fp));
298       break;
299
300     case s_c8:
301       return (mbtok(byte, tok_c8, ITEMS(tok_c8), fp));
302       break;
303
304     /* --- Encoded line number states --- */
305
306     case s_linex:
307       byte ^= 0x54;
308       lineno = (((byte & 0x30) << 2) |
309                 ((byte & 0x0c) << 12) |
310                 ((byte & 0x03)  << 16));
311       state++;
312       break;
313
314     case s_liney:
315       lineno |= byte & 0x3f;
316       state++;
317       break;
318
319     case s_linez:
320       lineno |= (byte & 0x3f) << 8;
321       fprintf(fp, "%u", lineno);
322       state = s_normal;
323       break;
324
325   }
326   return (0);
327 }
328
329 /* --- @line@ --- *
330  *
331  * Arguments:   @FILE *in@ = input stream to read
332  *              @FILE *out@ = output stream to write
333  *
334  * Returns:     Zero if there's another line after this one.
335  *
336  * Use:         Decodes a BASIC line into stuff to be written.
337  */
338
339 static int line(FILE *in, FILE *out)
340 {
341   /* --- Read the line number --- */
342
343   {
344     int a, b;
345
346     a = getc(in);
347     D( fprintf(stderr, "ln_0 == %i\n", a); )
348       if (a == EOF)
349       goto eof;
350     if (a == 0xFF)
351       return (1);
352
353     b = getc(in);
354     D( fprintf(stderr, "ln_1 == %i\n", b); )
355       if (b == EOF)
356       goto eof;
357
358     if (flags & f_linenumbers)
359       fprintf(out, "%5i", (a << 8) + b);
360   }
361
362   {
363     int len;
364     int byte;
365
366     len = getc(in);
367     D( fprintf(stderr, "linelen == %i\n", len); )
368       if (len == EOF)
369       goto eof;
370     len -= 4;
371
372     state = s_keyword;
373     while (len) {
374       byte = getc(in);
375       D( fprintf(stderr, "state == %i, byte == %i\n",
376                  state, byte); )
377         if (byte == EOF)
378         goto eof;
379       decode(byte, out);
380       len--;
381     }
382     putc('\n', out);
383
384     byte = getc(in);
385     D( fprintf(stderr, "eol == %i\n", byte); )
386       if (byte == EOF)
387       goto eof;
388     else if (byte != 0x0D)
389       return (die("Bad program: expected end-of-line delimiter"));
390   }
391
392   return (0);
393
394 eof:
395   return (die("Bad program: unexpected end-of-file"));
396 }
397
398 /* --- @file@ --- *
399  *
400  * Arguments:   @FILE *in@ = the input stream
401  *              @FILE *out@ = the output stream
402  *
403  * Returns:     --
404  *
405  * Use:         Decodes an entire file.
406  */
407
408 static void file(FILE *in, FILE *out)
409 {
410   int byte;
411
412   /* --- Check for the inital newline char --- */
413
414   byte = getc(in);
415   if (byte != 0x0D)
416     die("Bad program: doesn't start with a newline");
417
418   /* --- Now read the lines in one by one --- */
419
420   while (!line(in, out)) ;
421
422   /* --- Check that we're really at end-of-file --- */
423
424   byte = getc(in);
425   if (byte != EOF)
426     die("Found data after end of program");
427 }
428
429 /* --- @sig_pipe@ --- *
430  *
431  * Arguments:   @int s@ = signal number
432  *
433  * Returns:     Doesn't
434  *
435  * Use:         Handles SIGPIPE signals, and gracefully kills the program.
436  */
437
438 static void sig_pipe(int s)
439 {
440   (void) s;
441   exit(0);                              /* Gracefully, oh yes */
442 }
443
444 /* --- @options@ --- *
445  *
446  * Arguments:   @int c@ = number of arguments
447  *              @char *v[]@ = pointer to arguments
448  *              @char *s@ = pointer to short options
449  *              @struct option *o@ = pointer to long options
450  *
451  * Returns:     --
452  *
453  * Use:         Parses lots of arguments.
454  */
455
456 static void options(int c, char *v[], const char *s,
457                         const struct option *o)
458 {
459   int i;
460
461   for (;;) {
462     i = mdwopt(c, v, s, o, 0, 0, OPTF_NEGATION | OPTF_ENVVAR);
463     if (i == -1)
464       break;
465
466     switch (i) {
467       case 'v':
468       case 'h':
469         printf("%s version " VERSION "\n", optprog);
470         if (i == 'v')
471           exit(0);
472         printf("\n"
473                "%s [-hv] [-n|+n] [-l|+l] [-p PAGER] [file...]\n"
474                "\n"
475                "Types BBC BASIC programs in a readable way.  Options "
476                "currently supported are as\n"
477                "follows:\n"
478                "\n"
479                "-h, --help:            Displays this evil help message\n"
480                "-v, --version:         Displays the current version number\n"
481                "-n, --line-numbers:    Displays line numbers for each line\n"
482                "-l, --highlight:       Attempts to highlight keywords\n"
483                "-p, --pager=PAGER:     Sets pager to use (default $PAGER)\n"
484                "\n"
485                "Prefix long options with `no-' to cancel them.  Use `+' to "
486                "cancel short options.\n"
487                "Options can also be placed in the `BASCAT' environment "
488                "variable, if you don't\n"
489                "like the standard settings.\n",
490                optprog);
491         exit(0);
492         break;
493       case 'n':
494         flags |= f_linenumbers;
495         break;
496       case 'n' | OPTF_NEGATED:
497         flags &= ~f_linenumbers;
498         break;
499       case 'l':
500         flags |= f_highlight;
501         break;
502       case 'l' | OPTF_NEGATED:
503         flags &= ~f_highlight;
504         break;
505       case 'p':
506         pager = optarg;
507         break;
508     }
509   }
510 }
511
512 /* --- @main@ --- *
513  *
514  * Arguments:   @int argc@ = number of arguments
515  *              @char *argc[]@ = pointer to what the arguments are
516  *
517  * Returns:     0 if it all worked
518  *
519  * Use:         Displays BASIC programs.
520  */
521
522 int main(int argc, char *argv[])
523 {
524   static struct option opts[] = {
525     { "help",           0,              0,      'h' },
526     { "version",        0,              0,      'v' },
527     { "line-numbers",   OPTF_NEGATE,    0,      'n' },
528     { "highlight",      OPTF_NEGATE,    0,      'l' },
529     { "pager",          OPTF_ARGREQ,    0,      'p' },
530     { 0,                0,              0,      0 }
531   };
532   static char *shortopts = "hvn+l+p:";
533
534   /* --- Parse the command line options --- */
535
536   options(argc, argv, shortopts, opts);
537
538   /* --- Now do the job --- */
539
540   if (optind == argc && isatty(0)) {
541     fprintf(stderr,
542             "%s: No filenames given, and standard input is a tty\n"
543             "To force reading from stdin, use `%s -'.  For help, type "
544             "`%s --help'.\n",
545             optprog, optprog, optprog);
546     exit(0);
547   }
548
549 #ifdef HAVE_LIBTERMCAP
550   if (flags & f_highlight)
551     tgetent(termcap, getenv("TERM"));
552 #endif
553
554   {
555     FILE *in;
556     FILE *out;
557
558     /* --- If output is to a terminal, try paging --- *
559      *
560      * All programs which spew text should do this ;-)
561      */
562
563     if (isatty(1)) {
564       if (!pager)
565         pager = getenv("PAGER");
566       if (!pager)
567         pager = PAGER;                  /* Worth a try */
568       if (strstr(pager, "less"))
569         flags |= f_less;                /* HACK!!! */
570       out = popen(pager, "w");
571       if (!out)
572         out = stdout;
573       else {
574         flags |= f_tty;
575         signal(SIGPIPE, sig_pipe);
576       }
577     } else
578       out = stdout;
579
580     /* --- Now go through all the files --- */
581
582     if (optind == argc)
583       file(stdin, out);
584     else
585       while (optind < argc) {
586         if (strcmp(argv[optind], "-") == 0)
587           file(stdin, out);
588         else {
589           in = fopen(argv[optind], "rb");
590           if (!in) {
591             fprintf(stderr,
592                     "%s: Couldn't open input file: %s\n",
593                     optprog, strerror(errno));
594           } else {
595             file(in, out);
596             fclose(in);
597           }
598         }
599         optind++;
600       }
601     if (flags & f_tty)
602       pclose(out);
603   }
604
605   return (0);
606 }
607
608 /*----- That's all, folks -------------------------------------------------*/