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