Fix decoding of line numbers.
[bascat] / bascat.c
CommitLineData
0398bb0d 1/* -*-c-*-
2 *
aa1ca0ec 3 * $Id: bascat.c,v 1.5 1999/11/25 01:08:57 mdw Exp $
0398bb0d 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 $
aa1ca0ec 32 * Revision 1.5 1999/11/25 01:08:57 mdw
33 * Fix decoding of line numbers.
34 *
992dc059 35 * Revision 1.4 1999/10/28 10:42:23 mdw
36 * More minor twiddling.
37 *
b98146e0 38 * Revision 1.3 1999/10/28 10:18:17 mdw
39 * Minor name changes for new coding standards.
40 *
af9da5bb 41 * Revision 1.2 1998/06/26 11:29:28 mdw
42 * Added support for line-numbers.
43 *
0398bb0d 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
992dc059 88static const char *tok_base[] = {
0398bb0d 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
992dc059 108static const char *tok_c6[] = {
0398bb0d 109 "SUM", "BEAT"
110};
111
992dc059 112static const char *tok_c7[] = {
0398bb0d 113 "APPEND", "AUTO",
114 "CRUNCH", "DELETE", "EDIT", "HELP", "LIST", "LOAD", "LVAR", "NEW",
115 "OLD", "RENUMBER", "SAVE", "TEXTLOAD", "TEXTSAVE", "TWIN", "TWINO",
116 "INSTALL"
117};
118
992dc059 119static const char *tok_c8[] = {
0398bb0d 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
130enum {
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 */
af9da5bb 136 s_linex, s_liney, s_linez, /* Line number states */
0398bb0d 137 s_dummy
138};
139
b98146e0 140static int state = s_normal; /* Current detokenisation state */
141static unsigned int lineno; /* Line number */
0398bb0d 142
143enum {
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
b98146e0 151static int flags; /* Various options flags */
0398bb0d 152
153#ifdef HAVE_LIBTERMCAP
b98146e0 154static char termcap[2048]; /* Terminal capabilities buffer */
0398bb0d 155#endif
156
b98146e0 157static char *pager = 0; /* Pointer to pager to use */
0398bb0d 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
170static int die(const char *p)
171{
172 fprintf(stderr, "%s: %s\n", optprog, p);
173 return (1);
174}
175
b98146e0 176/* --- @keyword@ --- *
0398bb0d 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
b98146e0 190static void keyword(const char *s, FILE *fp)
0398bb0d 191{
192#ifdef HAVE_LIBTERMCAP
b98146e0 193 if ((~flags & (f_less | f_highlight)) == 0) {
0398bb0d 194 while (*s) {
195 putc(*s, fp);
b98146e0 196 putc('\b', fp);
0398bb0d 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) {
b98146e0 205 if (flags & f_highlight) {
0398bb0d 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
b98146e0 220/* --- @mbtok@ --- *
0398bb0d 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
aa1ca0ec 232static int mbtok(int byte, const char *t[], int n, FILE *fp)
0398bb0d 233{
234 byte -= 0x8E;
235 if (byte >= n)
236 return (die("Bad program: invalid multibyte token"));
b98146e0 237 keyword(t[byte], fp);
238 state = s_normal;
0398bb0d 239 return (0);
240}
241
b98146e0 242/* --- @decode@ --- *
0398bb0d 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
aa1ca0ec 252static int decode(int byte, FILE *fp)
0398bb0d 253{
b98146e0 254 switch (state) {
af9da5bb 255
256 /* --- Tokenised states --- */
257
0398bb0d 258 case s_keyword:
259 if (byte == '*')
b98146e0 260 state = s_comment;
0398bb0d 261 else
b98146e0 262 state = s_normal;
0398bb0d 263 /* Fall through here */
af9da5bb 264
0398bb0d 265 case s_normal:
266 if (byte >= 0x7F) {
267 switch (byte) {
268 case 0xC6:
b98146e0 269 state = s_c6;
0398bb0d 270 break;
271 case 0xC7:
b98146e0 272 state = s_c7;
0398bb0d 273 break;
274 case 0xC8:
b98146e0 275 state = s_c8;
0398bb0d 276 break;
af9da5bb 277 case 0x8D:
b98146e0 278 state = s_linex;
af9da5bb 279 break;
0398bb0d 280 case 0x8B: /* ELSE */
281 case 0x8C: /* THEN */
282 case 0xF5: /* REPEAT (a funny one) */
b98146e0 283 state = s_keyword;
af9da5bb 284 goto keyword;
0398bb0d 285 break;
286 case 0xDC: /* DATA */
287 case 0xF4: /* REM */
b98146e0 288 state = s_comment;
0398bb0d 289 /* Fall through here */
290 default:
af9da5bb 291 keyword:
992dc059 292 keyword(tok_base[byte - 0x7F], fp);
0398bb0d 293 break;
294 }
295 } else {
296 if (byte == '"')
b98146e0 297 state = s_quote;
0398bb0d 298 fputc(byte, fp);
299 }
300 break;
af9da5bb 301
302 /* --- Non-tokenised states --- */
303
0398bb0d 304 case s_quote:
305 if (byte == '"')
b98146e0 306 state = s_normal;
0398bb0d 307 /* Fall through here */
af9da5bb 308
0398bb0d 309 case s_comment:
310 fputc(byte, fp);
311 break;
af9da5bb 312
313 /* --- Double-byte token states --- */
314
0398bb0d 315 case s_c6:
992dc059 316 return (mbtok(byte, tok_c6, ITEMS(tok_c6), fp));
0398bb0d 317 break;
af9da5bb 318
0398bb0d 319 case s_c7:
992dc059 320 return (mbtok(byte, tok_c7, ITEMS(tok_c7), fp));
0398bb0d 321 break;
af9da5bb 322
0398bb0d 323 case s_c8:
992dc059 324 return (mbtok(byte, tok_c8, ITEMS(tok_c8), fp));
0398bb0d 325 break;
af9da5bb 326
327 /* --- Encoded line number states --- */
328
329 case s_linex:
aa1ca0ec 330 byte ^= 0x54;
331 lineno = (((byte & 0x30) << 2) |
332 ((byte & 0x0c) << 12) |
333 ((byte & 0x03) << 16));
b98146e0 334 state++;
af9da5bb 335 break;
336
337 case s_liney:
aa1ca0ec 338 lineno |= byte & 0x3f;
b98146e0 339 state++;
af9da5bb 340 break;
341
342 case s_linez:
aa1ca0ec 343 lineno |= (byte & 0x3f) << 8;
b98146e0 344 fprintf(fp, "%u", lineno);
345 state = s_normal;
af9da5bb 346 break;
347
0398bb0d 348 }
349 return (0);
350}
351
b98146e0 352/* --- @line@ --- *
0398bb0d 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
b98146e0 362static int line(FILE *in, FILE *out)
0398bb0d 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
b98146e0 381 if (flags & f_linenumbers)
0398bb0d 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
aa1ca0ec 395 state = s_keyword;
0398bb0d 396 while (len) {
397 byte = getc(in);
af9da5bb 398 D( fprintf(stderr, "state == %i, byte == %i\n",
b98146e0 399 state, byte); )
0398bb0d 400 if (byte == EOF)
401 goto eof;
b98146e0 402 decode(byte, out);
0398bb0d 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
417eof:
418 return (die("Bad program: unexpected end-of-file"));
419}
420
b98146e0 421/* --- @file@ --- *
0398bb0d 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
b98146e0 431static void file(FILE *in, FILE *out)
0398bb0d 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
b98146e0 443 while (!line(in, out)) ;
0398bb0d 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
992dc059 452/* --- @sig_pipe@ --- *
0398bb0d 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
992dc059 461static void sig_pipe(int s)
0398bb0d 462{
463 (void) s;
464 exit(0); /* Gracefully, oh yes */
465}
466
b98146e0 467/* --- @options@ --- *
0398bb0d 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
b98146e0 479static void options(int c, char *v[], const char *s,
0398bb0d 480 const struct option *o)
481{
482 int i;
483
484 for (;;) {
b98146e0 485 i = mdwopt(c, v, s, o, 0, 0, OPTF_NEGATION | OPTF_ENVVAR);
0398bb0d 486 if (i == -1)
487 break;
488
489 switch (i) {
490 case 'v':
491 case 'h':
b98146e0 492 printf("%s version " VERSION "\n", optprog);
0398bb0d 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':
b98146e0 517 flags |= f_linenumbers;
0398bb0d 518 break;
b98146e0 519 case 'n' | OPTF_NEGATED:
520 flags &= ~f_linenumbers;
0398bb0d 521 break;
522 case 'l':
b98146e0 523 flags |= f_highlight;
0398bb0d 524 break;
b98146e0 525 case 'l' | OPTF_NEGATED:
526 flags &= ~f_highlight;
0398bb0d 527 break;
528 case 'p':
b98146e0 529 pager = optarg;
0398bb0d 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
545int main(int argc, char *argv[])
546{
547 static struct option opts[] = {
b98146e0 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 }
0398bb0d 554 };
555 static char *shortopts = "hvn+l+p:";
556
557 /* --- Parse the command line options --- */
558
b98146e0 559 options(argc, argv, shortopts, opts);
0398bb0d 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
b98146e0 573 if (flags & f_highlight)
574 tgetent(termcap, getenv("TERM"));
0398bb0d 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)) {
b98146e0 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");
0398bb0d 594 if (!out)
595 out = stdout;
596 else {
b98146e0 597 flags |= f_tty;
992dc059 598 signal(SIGPIPE, sig_pipe);
0398bb0d 599 }
600 } else
601 out = stdout;
602
603 /* --- Now go through all the files --- */
604
605 if (optind == argc)
b98146e0 606 file(stdin, out);
0398bb0d 607 else
608 while (optind < argc) {
609 if (strcmp(argv[optind], "-") == 0)
b98146e0 610 file(stdin, out);
0398bb0d 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 {
b98146e0 618 file(in, out);
0398bb0d 619 fclose(in);
620 }
621 }
622 optind++;
623 }
b98146e0 624 if (flags & f_tty)
0398bb0d 625 pclose(out);
626 }
627
628 return (0);
629}
630
631/*----- That's all, folks -------------------------------------------------*/