Fix decoding of line numbers.
[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 -------------------------------------------------*/