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