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