diff options
Diffstat (limited to 'listamsb.c')
-rw-r--r-- | listamsb.c | 1010 |
1 files changed, 1010 insertions, 0 deletions
diff --git a/listamsb.c b/listamsb.c new file mode 100644 index 0000000..3815db2 --- /dev/null +++ b/listamsb.c @@ -0,0 +1,1010 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> +#include <stdarg.h> +#include <ctype.h> +#include <sys/stat.h> + +#include "atascii.h" +#include "amsbtok.h" + +/* this should always be defined in <stdio.h>, but just in case... */ +#ifndef BUFSIZ +#define BUFSIZ 4096 +#endif + +/* range for one-byte tokens */ +#define MIN_STD_TOK 0x80 /* END */ +#define MAX_STD_TOK 0xf8 /* < */ + +/* range for 2nd byte of two-byte tokens (1st is always 0xff) */ +#define MIN_EXT_TOK 0xa3 /* SGN */ +#define MAX_EXT_TOK 0xc5 /* STACK */ + +/* AMSB's tokens for "!", "'", REM. used to introduce comments */ +#define TOK_REM 0x98 +#define TOK_SQUOTE 0x9a +#define TOK_BANG 0x9b + +/* various other AMSB tokens */ +#define TOK_ELSE 0x9c +#define TOK_IF 0x97 +#define TOK_THEN 0x9d +#define TOK_AND 0xf3 +#define TOK_OR 0xf4 +#define TOK_NOT 0xf5 + +/* good old Atari EOL character */ +#define EOL 0x9b + +/* every MS BASIC I ever saw had the same line number limit. it's + kind of arbitrary: why not allow the full range, up to 65535? */ +#define MAX_LINENO 63999 + +/* a program bigger than this can't possibly fit into memory, + even with cart-based AMSB2 and no DOS loaded. */ +#define MAX_PROGLEN 30000 + +/* there should never be a valid line of BASIC longer than MAX_LINE_LEN + bytes, since there would be no way to enter it in the editor. AMSB + uses the standard E: device, which limits you to 3 40-column screen + lines, or max 120 bytes (after POKE 82,0). there *cannot* be a line + longer than MAX_LINE_LEN_HARD, because AMSB would crash when you + try to LOAD such a program. */ +#define MAX_LINE_LEN 0x80 +#define MAX_LINE_LEN_HARD 0xff + +/* a program whose header has a length less than MIN_PROGLEN can't be + a real AMSB program. EMPTY_PROGLEN is what you get if you + SAVE when there's no program in memory (right after boot or + a NEW). The minimum size for a program that actually contains + code seems to be 5 (for 10 PRINT) */ +#define MIN_PROGLEN 5 +#define EMPTY_PROGLEN 2 + +/* a file shorter than this can't be an AMSB program */ +#define MIN_BYTES 5 + +/* an EOL address below this has to be an error, since + this is the lowest MEMLO can ever be. */ +#define MIN_PTR 0x0700 + +/* an EOL address higher than this has to be an error, since + it would overlap the GR.0 display list or the ROMs at $c000 */ +#define MAX_PTR 0xbc1f + +/* SAVE "filename" LOCK does 'encryption' by subtracting every byte + from this (except the 3-byte header) */ +#define UNLOCK_KEY 0x54 + +#define ENV_VAR "COLORIZE_AMSB" + +const char *self; + +atascii_ctx actx; + +int verbosity = 0; /* -v */ +int raw_output = 0; /* -a */ +int check_only = 0; /* -c */ +int startline = 0; /* -r */ +int endline = 65536; /* -r */ +int unlock_mode = 0; /* -l */ +int initial_eol = 1; /* -n */ +int crunch = 0; /* -C, -D */ +int decrunch = 0; /* -D */ +int keep_rems = 0; /* -k */ +int color = 1; /* -m */ + +int locked = 0; +int need_pclose = 0; +int bytes_read = 0; +int warnings = 0; +int proglen = 0; +int linecount = 0; +int header_read = 0; +int printing = 0; +int spacecount = 0; + +FILE *infile; +FILE *outfile; + +const char *outname = NULL; + +void verbose(int level, const char *fmt, ...) { + va_list ap; + + if(verbosity < level) return; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); +} + +void finish(int rv); + +#define VA_FUNC(func, prefix, action) \ + void func(const char *fmt, ...) { \ + va_list ap; \ + va_start(ap, fmt); \ + fprintf(stderr, "%s: %s: ", self, prefix); \ + vfprintf(stderr, fmt, ap); \ + va_end(ap); \ + fputc('\n', stderr); \ + action; \ + } + +VA_FUNC(die, "error", finish(2)); +VA_FUNC(os_err, "fatal", exit(1)); +VA_FUNC(warn, "warning", warnings++); + +void set_self(const char *argv0) { + char *p; + + self = argv0; + p = strrchr(self, '/'); + if(p) self = p + 1; +} + +const char *plural(int count) { + return count == 1 ? "" : "s"; +} + +int is_comment_start(unsigned char tok) { + return (tok == TOK_REM || tok == TOK_SQUOTE || tok == TOK_BANG); +} + +off_t get_file_len(const char *name) { + struct stat st; + if(stat(name, &st) != 0) return 0; + return st.st_size; +} + +/* post-processing: print "summary", exit. called by either die() + or main() (on normal exit). */ +void finish(int rv) { + int progsize; + + verbose(1, "read %d bytes", bytes_read); + verbose(1, "listed %d line%s", linecount, plural(linecount)); + + if(header_read) { + if(!linecount) warn("no lines of code in program"); + progsize = bytes_read - 3; + if(proglen == progsize) { + verbose(1, "file size matches proglen"); + } else { + warn("program size %d doesn't match size %d in header,", + progsize, proglen); + fputs(" ", stderr); + if(proglen > progsize) { + fputs("AMSB will give #136 ERROR and fail to LOAD this file\n", stderr); + } else { + fputs("AMSB will stop LOADing before the end of this file\n", stderr); + } + } + if(fgetc(infile) != EOF) warn("trailing garbage at end of file"); + } else { + if(feof(infile)) + warn("file is %d byte%s, too short to be an AMSB program", bytes_read, plural(bytes_read)); + } + + if(warnings) { + fprintf(stderr, "%s: file has %d warning%s\n", + self, warnings, plural(warnings)); + rv = 2; + } + + if(need_pclose) { + int got = pclose(outfile); + verbose(1, "return value from pipe is %d", got); + if(got != 0) { + os_err("colorize-amsb child process failed, do you have it on your PATH?"); + } + } else { + fclose(outfile); + /* if we were writing to actual file and no data was written, rm it */ + if(outname && get_file_len(outname) == 0) + unlink(outname); + } + + verbose(2, "spaces outside strings/comments: %d (%.1f%%)", + spacecount, + (float)(spacecount) / (float)(bytes_read) * 100.0); + + verbose(1, "exit status: %d (%s)", rv, (rv ? "ERROR" : "OK")); + + exit(rv); +} + +unsigned char read_byte(void) { + int c; + + c = fgetc(infile); + + if(c < 0) die("unexpected EOF in %s (byte %d), file truncated?", + (header_read ? "program" : "header"), bytes_read); + + bytes_read++; + return (unsigned char)c; +} + +/* "decrypt" a byte from a "SAVE x LOCK" program. */ +unsigned char unlock_byte(unsigned char b) { + return ((UNLOCK_KEY - b) & 0xff); +} + +/* the "encryption" is the same (process is reciprocal) */ +#define lock_byte(x) unlock_byte(x) + +/* read and (if needed) decrypt a byte from the program. */ +unsigned char read_prog_byte(void) { + unsigned char b = read_byte(); + return locked ? unlock_byte(b) : b; +} + +/* read a word from the program, does not decrypt */ +int read_word(void) { + int w; + + w = read_byte(); + w |= (read_byte() << 8); + + return w; +} + +/* read and (if needed) decrypt a word from the program. */ +int read_prog_word(void) { + int w; + + w = read_prog_byte(); + w |= (read_prog_byte() << 8); + + return w; +} + +void read_header(void) { + /* $00 for plain, $01 for SAVE with LOCK */ + locked = read_byte(); + if(locked > 1) die("not an AMSB file: first byte is $%02x, not $00 or $01", locked); + + if(locked) + verbose(1, "program is locked, decrypting"); + else + verbose(1, "program is not locked"); + + proglen = read_word(); + + verbose(1, "proglen == %d (%04x)", proglen, proglen); + + if(proglen > MAX_PROGLEN) { + die("not an AMSB file: too big (%d bytes), won't fit in Atari memory", proglen); + } + + if(proglen == EMPTY_PROGLEN) { + warn("program length is 2, no code in file (SAVE after NEW)"); + } else { + if(proglen < MIN_PROGLEN) { + die("not an AMSB file: program size too small (%d). Atari BASIC file?", proglen); + } + } + + header_read = 1; +} + +void unknown_token(int tok) { + int ext; + if(!printing) return; + ext = (tok > 0xff); + tok &= 0xff; + fprintf(outfile, "<unknown %stoken ", (ext ? "extended " : "")); + fprintf(outfile, "%s%02x>", (ext ? "$ff ": ""), tok); +} + +void list_char(unsigned char c) { + if(!printing) return; + if(raw_output) { + fputc(c, outfile); + } else { + char buf[20]; + fputs(atascii_a2utf(&actx, c, buf), outfile); + } +} + +int expand_token(int t, unsigned char *buf); + +int list_token(int tok) { + unsigned char text[10]; + if(!expand_token(tok, text)) { + unknown_token(tok); + return 0; + } + if(printing) fputs((char *)text, outfile); + return 1; +} + +void list_lineno(int l) { + /* note that AMSB always puts a space after the line number in LIST */ + if(printing) fprintf(outfile, "%d ", l); +} + +void start_listing(void) { + /* AMSB always prints a blank line when it LISTs, so we do, too. + unless it's disabled with -n, of course. */ + if(initial_eol) { + if(raw_output) { + fputc(0x9b, outfile); + } else { + fputc('\n', outfile); + } + } +} + +int read_token(int literal); + +/* meat and potatoes. does the actual detokenizing. gets called once + per line of code. returns false when it hits the last line, or + true if there are more lines. */ +int next_line(void) { + static int last_lineno = -1; + static int last_ptr = -1; + int tok, ptr, lineno, was_colon = 0, in_string = 0, in_comment = 0, offset, len; + + offset = bytes_read; + + /* pointer to last token on the line, offset by whatever MEMLO + happened to be when the file was SAVEd. 0 means this is the + last line. */ + ptr = read_prog_word(); + if(!ptr) { + verbose(1, "end of program"); + return 0; + } + + lineno = read_prog_word(); + verbose(2, "found line %d, offset %d, end-of-line %d", lineno, offset, ptr); + + printing = (lineno >= startline) && (lineno <= endline); + + if(ptr < MIN_PTR) { + warn("line %d: EOL address $%04x too low (<$%04x)", lineno, ptr, MIN_PTR); + } else if(ptr >= MAX_PTR) { + warn("line %d: EOL address $%04x too high (>$%04x)", lineno, ptr, MAX_PTR); + } + + if(last_ptr != -1) { + if(ptr <= last_ptr) { + warn("line %d: EOL address $%04x <= previous $%04x", lineno, ptr, last_ptr); + } + } + + if(lineno <= last_lineno) { + warn("line number out of order (%d <= %d)", lineno, last_lineno); + } + + if(lineno > MAX_LINENO) { + warn("line number out range (%d > %d)", lineno, MAX_LINENO); + } + + last_lineno = lineno; + + list_lineno(lineno); + + /* walk and print the tokens. when we hit a null byte, break out of the + loop, we're done with this line. */ + while(1) { + tok = read_token(in_string || in_comment); + + if(in_string) { + if(tok == 0x00) { + /* null byte ends both the string and the line of code. + don't print a closing quote because AMSB doesn't. */ + break; + } else if(tok == '|') { + /* pipe is how AMSB stores the closing quote. end the string + but not the line of code, and print a " character. */ + in_string = 0; + list_char('"'); + } else { + /* normal string character. */ + list_char(tok); + /* one " character embedded in a string gets printed as "" */ + if(tok == '"') list_char(tok); + } + continue; + } else if(in_comment) { + /* null byte ends both the comment and the line of code. */ + if(tok == 0x00) break; + list_char(tok); + continue; + } + + if(was_colon) { + if(tok != TOK_SQUOTE && tok != TOK_BANG && tok != TOK_ELSE) { + list_char(':'); + } + was_colon = 0; + } + + if(tok == ':') { + /* statement separator. don't print the colon yet, the next token + might be ELSE or a ! or ' for a comment */ + was_colon = 1; + } else if(tok == '"') { + /* begin string. strings start but *don't end* with a double-quote */ + in_string = 1; + list_char(tok); + } else if(tok >= 0x80) { + if(!list_token(tok)) + warn("unknown %stoken %s$%02x at line %d", + (tok > 0xff ? "extended " : ""), + (tok > 0xff ? "$ff " : ""), + tok & 0xff, + lineno); + if(is_comment_start(tok)) in_comment = 1; + } else { + /* null byte means the line of code is done */ + if(!tok) break; + if(tok < 0x20) { + /* ATASCII graphics outside of a string */ + warn("line %d has character %d outside of a string, maybe Atari BASIC?", + lineno, tok); + } + if(tok == ' ') spacecount++; + list_char(tok); + } + } + + len = bytes_read - offset; + + verbose(2, " line %d length: %d", lineno, len); + + if(len > MAX_LINE_LEN) { + int hard = len > MAX_LINE_LEN_HARD; + warn("line %d is %s long (length %d > %d)", + lineno, + hard ? "impossibly" : "supiciously", + len, + hard ? MAX_LINE_LEN_HARD : MAX_LINE_LEN); + } + + if(last_ptr != -1) { + int plen = ptr - last_ptr; + if(len != plen) { + warn("line %d: EOL address doesn't match actual line length %d (should be %d)", lineno, len, plen); + } + } + + last_ptr = ptr; + + list_char(EOL); + + if(!raw_output && (actx.inv)) { + char buf[20]; + fputs(atascii_a2utf(&actx, ATA_CHR_FINISH, buf), outfile); + } + + return 1; +} + +/* when this gets called, input and output are open, read_header() + has already run. "locking" and "unlocking" are the same + transform, so this function does both. + note that *no* checking of the program code is done here, so + there's no need to finish() afterwards. */ +void unlock_program(void) { + int c; + + /* do not convert this to warn() (it ain't a warning) */ + fprintf(stderr, "%s: program is %slocked, output will be %slocked\n", + self, locked ? "" : "un", locked ? "un" : ""); + + /* 3-byte header: 0 for unlocked, 1 for locked */ + fputc(!locked, outfile); + /* LSB of program length (not encrypted) */ + fputc(proglen & 0xff, outfile); + /* MSB */ + fputc((proglen >> 8) & 0xff, outfile); + + /* rest of file, including trailing nulls, is transformed */ + while( (c = fgetc(infile)) >= 0) + fputc(unlock_byte(c & 0xff), outfile); + + fclose(outfile); + exit(0); +} + +void write_code(int addr, int lineno, const unsigned char *code) { + fputc(addr & 0xff, outfile); + fputc((addr >> 8) & 0xff, outfile); + fputc(lineno & 0xff, outfile); + fputc((lineno >> 8) & 0xff, outfile); + fputs((char *)code, outfile); + fputc(0x00, outfile); +} + +int comment_only_line(const unsigned char *code) { + if(code[0] == TOK_REM) return 1; + if(code[0] == ':') { + if(code[1] == TOK_SQUOTE || code[1] == TOK_BANG) + return 1; + } + return 0; +} + +int crunch_line(void) { + unsigned char code[MAX_LINE_LEN_HARD + 1], byte; + int lineno, ptr, codelen = 0, in_string = 0, in_comment = 0, commentstart = 0; + static int addr = 0x700; /* doesn't really matter where */ + + ptr = read_prog_word(); + if(!ptr) return -1; + lineno = read_prog_word(); + + verbose(2, "crunching line %d", lineno); + + while(1) { + if(codelen >= MAX_LINE_LEN_HARD) + die("line %d too long, crunching failed", lineno); + byte = read_prog_byte(); + + if(byte != 0) { + if(in_string) { + if(byte == '|') + in_string = 0; + } else if(in_comment) { + continue; + } else { + if(byte == '"') + in_string = 1; + else if(is_comment_start(byte)) { + in_comment = 1; + commentstart = codelen; + } + else if(byte == ' ') + continue; + } + } + + code[codelen++] = byte; + if(byte == 0) break; + } + + /* omit comment-only lines */ + if(!keep_rems) { + if(comment_only_line(code)) { + verbose(1, "removing comment-only line %d", lineno); + return 0; + } + } + + /* omit trailing comments */ + if(commentstart > 1) { + code[commentstart - 1] = 0; /* null out the colon before the comment */ + codelen = commentstart; + + /* removing the comment from this: 10 PRINT :! BLAH + ...leaves a trailing colon. get rid of it if needed. */ + if(codelen >= 2) { + if(code[codelen - 2] == ':') { + code[codelen - 2] = 0; + codelen--; + } + } + } + + codelen += 4; /* account for ptr and lineno */ + addr += codelen; + + write_code(addr, lineno, code); + + return codelen; +} + +int expand_token(int t, unsigned char *buf) { + const char *result; + + if(t < 0x80) { + buf[0] = t; + buf[1] = 0; + return 1; + } + + if(t > 0xff) { + t &= 0xff; + if(t > MAX_EXT_TOK) + return 0; + result = ext_tokens[t - MIN_EXT_TOK]; + } else { + if(t > MAX_STD_TOK) + return 0; + result = std_tokens[t - MIN_STD_TOK]; + } + + strcpy((char *)buf, result); + return 1; +} + +int read_token(int literal) { + int tok = read_prog_byte(); + if(literal) return tok; + if(tok == 0xff) { + tok <<= 8; + tok |= read_prog_byte(); + } + return tok; +} + +int token_is_extended(int tok) { + return tok > 0xff; +} + +int append_token(int tok, unsigned char *buf, int len) { + if(token_is_extended(tok)) { + buf[len++] = 0xff; + buf[len++] = tok & 0xff; + } else { + buf[len++] = tok; + } + return len; +} + +/* only called during decrunching. + the tokenizer in AMSB requires spaces to separate keywords + and variables/numbers. "IF A THEN 100" really needs the spaces + before and after the A, for example. */ +int need_space_between(int t1, int t2) { + unsigned char text1[10], text2[10]; + unsigned char t1last, t2first; + + if(!t1) return 0; /* start of line */ + if(!t2) return 0; /* end of line */ + if(t1 < 0x80 && t2 < 0x80) return 0; /* 2 ASCII chars */ + + if( !(expand_token(t1, text1) && expand_token(t2, text2)) ) + die("invalid token in program, decrunching failed"); + + t1last = text1[strlen((char *)text1) - 1]; /* "PRINT" => "T" */ + t2first = text2[0]; /* "PRINT" => "P" */ + + /* if we already have a space, don't need to add another */ + if(t1last == ' ' || t2first == ' ') return 0; + + /* IF, THEN, and operators like AND/OR/NOT always get a + space after them, for readability. */ + switch(t1) { + case TOK_IF: + case TOK_THEN: + case TOK_ELSE: + case TOK_AND: + case TOK_OR: + case TOK_NOT: + return 1; + default: break; + } + + /* these always get a space before them, for readability. */ + switch(t2) { + case TOK_THEN: + case TOK_AND: + case TOK_OR: + case TOK_NOT: + return 1; + default: break; + } + + if(t1 >= 0x80 && isalnum(t1last)) { + /* space not really required between OPEN/PRINT/CLOSE and #, + but put one there for neatness. */ + if(t2first == '#') return 1; + /* same for POKE &52,0 or DATA &FF */ + if(t2first == '&') return 1; + /* INPUT "PROMPT";A$ or DATA "FOO" */ + if(t2first == '"') return 1; + } + + /* space not really required between a closing quote and + a keyword, but put it in for neatness. examples: + OPEN #1,"D:X" INPUT + IF A$="FOO" THEN 10 + PRINT "YOUR IQ IS" IQ + these look weird without the space after the " */ + if(isalnum(t2first)) { + if(t1last == '|') return 1; + if(t1last == '$') return 1; /* OPEN #1,F$ OUTPUT */ + } + + return(isalnum(t1last) && isalnum(t2first)); +} + +int decrunch_line(void) { + unsigned char code[MAX_LINE_LEN_HARD + 10]; + int lineno, tok = 0, prev, ptr, codelen = 0, in_string = 0, in_comment = 0; + static int addr = 0x700; /* doesn't really matter where */ + + ptr = read_prog_word(); + if(!ptr) return -1; + lineno = read_prog_word(); + + verbose(2, "decrunching line %d", lineno); + + while(1) { + if(codelen >= MAX_LINE_LEN_HARD) + die("line %d too long, decrunching failed", lineno); + prev = tok; + tok = read_token(in_string || in_comment); + + if(tok != 0) { + if(in_string) { + if(tok == '|') + in_string = 0; + } else if(in_comment) { + /* NOP */ + } else { + if(tok == ' ' || is_comment_start(tok)) { + verbose(1, "found space/comment at line %d", lineno); + spacecount++; + } + if(tok == '"') { + in_string = 1; + if(need_space_between(prev, tok)) + code[codelen++] = ' '; + } else if(is_comment_start(tok)) { + in_comment = 1; + } else if(prev == ':' && tok == TOK_ELSE) { + /* special case, ELSE needs a space before the (invisible) + colon that comes before it. */ + code[codelen - 1] = ' '; + code[codelen++] = prev; + } else if(need_space_between(prev, tok)) { + code[codelen++] = ' '; + } + } + } + + codelen = append_token(tok, code, codelen); + if(tok == 0) break; + } + + codelen += 4; /* account for ptr and lineno */ + addr += codelen; + + write_code(addr, lineno, code); + + return codelen; +} + +/* despite the name, this handles both crunching and decrunching */ +void crunch_program(void) { + int newproglen = 0, linelen = 0; + float percent; + + fputc(0x00, outfile); /* signature (0 = not locked) */ + fputc(0x00, outfile); /* length LSB (fill in later) */ + fputc(0x00, outfile); /* length MSB " " */ + + while((linelen = decrunch ? decrunch_line() : crunch_line()) != -1) + newproglen += linelen; + + /* trailing $00 $00 */ + fputc(0x00, outfile); + fputc(0x00, outfile); + + /* fill in length in header */ + if(fseek(outfile, 1L, SEEK_SET) < 0) os_err("fseek() failed"); + newproglen += 2; /* account for trailing $00 $00 */ + fputc(newproglen & 0xff, outfile); + fputc((newproglen >> 8) & 0xff, outfile); + fclose(outfile); + + /* show the user how the size changed */ + newproglen += 3; + percent = 100.0 * (1.0 - (float)newproglen / (float)bytes_read); + verbose(1, "%scrunched %d byte program to %d bytes (%.1f%% %s)", + decrunch ? "de" : "", + bytes_read, + newproglen, + percent < 0 ? -percent : percent, + percent < 0 ? "larger" : "smaller"); + + if(decrunch && spacecount) + warn("%d spaces/comments, are you sure it was crunched?", spacecount); +} + +void print_help(void) { + printf("%s v" VERSION " - detokenize Atari Microsoft BASIC files\n", self); + puts("By B. Watson <urchlay@slackware.uk>, released under the WTFPL"); + printf("Usage: %s [[-[acCDUMnviutms] ...] [-r *start,end*]] <file> <outfile>\n", self); + puts(" -a: raw ATASCII output"); + puts(" -c: check only (no listing)"); + puts(" -C: crunch program"); + puts(" -D: decrunch program"); + puts(" -U: lock or unlock program"); + puts(" -M: monochrome listing (no colorization)"); + puts(" -n: no blank line at start of listing"); + puts(" -v: verbosity"); + puts(" -r: only list lines numbered from *start* to *end*"); + puts(" --help, -h: print this help and exit"); + puts(" --version: print version number and exit"); + puts(" -i -u -t -m -s: same as a8cat (try 'a8cat -h')"); + puts("file must be a tokenized (SAVEd) AMSB file. if not given, reads from stdin."); + puts("if outfile not given, writes to stdout"); +} + +void version(void) { + printf("%s " VERSION "\n", self); +} + +void get_line_range(const char *arg) { + int val = 0, comma = 0; + const char *p = arg; + + while(*p) { + if(*p >= '0' && *p <= '9') { + val *= 10; + val += *p - '0'; + if(val > MAX_LINENO) { + os_err("invalid line number for -r (range is 0-%d)", MAX_LINENO); + } + } else if(*p == ',' || *p == '-') { + if(comma) os_err("invalid argument for -r (too many commas)"); + comma++; + startline = val; + val = 0; + } else { + if(comma) os_err("invalid argument for -r (only digits and comma allowed)"); + } + p++; + } + + if(comma) + endline = val ? val : MAX_LINENO; + else + startline = endline = val; + + if(endline < startline) + os_err("invalid argument for -r (start > end)"); +} + +void parse_args(int argc, char **argv) { + int opt, ropt = 0; + int ata_mode = ATA_MODE_UTF8, ata_flags = ATA_FLAG_NONE, a8catopt = 0; + + if(argc >= 2) { + if(strcmp(argv[1], "--help") == 0) { + print_help(); + exit(0); + } else if(strcmp(argv[1], "--version") == 0) { + version(); + exit(0); + } + } + + while( (opt = getopt(argc, argv, "MDCnLUlr:cvaiutmshk")) != -1) { + switch(opt) { + case 'L': crunch = decrunch = unlock_mode = 0; break; + case 'D': decrunch = 1; break; + case 'C': crunch = 1; break; + case 'l': + case 'U': unlock_mode = 1; break; + case 'k': keep_rems = 1; break; + case 'c': check_only = 1; break; + case 'a': raw_output = 1; break; + case 'v': verbosity++ ; break; + case 'n': initial_eol = 0; break; + case 'M': color = 0; break; + case 'h': print_help(); exit(0); + case 'r': get_line_range(optarg); ropt++; break; + case 'i': ata_flags |= ATA_FLAG_ICS; a8catopt++; break; + case 'u': ata_flags |= ATA_FLAG_UNDERLINE; a8catopt++; break; + case 't': ata_flags |= ATA_FLAG_ICS; a8catopt++; break; + case 'm': ata_mode = ATA_MODE_MAGAZINE; a8catopt++; break; + case 's': ata_flags |= ATA_FLAG_STRIP_INVERSE; a8catopt++; break; + default: print_help(); exit(1); + } + } + + if((unlock_mode + crunch + decrunch) > 1) { + os_err("only one of -C, -D, -L, -U may be used"); + } + + if(keep_rems && !decrunch) { + warn("-k option ignored since -D not given"); + } + + if(unlock_mode || crunch || decrunch) { + if(!initial_eol || check_only || raw_output || ropt || a8catopt || !color) { + warn("-[acimMnrstu] options ignored with -U, -C, or -D"); + } + } else if(!raw_output) { + atascii_context_init(&actx, ata_mode, ata_flags); + } + + if(optind >= argc) { + if(isatty(fileno(stdin))) { + print_help(); + os_err("can't read binary data from a terminal"); + } + freopen(NULL, "rb", stdin); + infile = stdin; + } else { + infile = fopen(argv[optind], "rb"); + if(!infile) { + os_err("%s: %s", argv[optind], strerror(errno)); + } + } + + optind++; + if(optind < argc) { + /* we were passed an output file */ + outname = argv[optind]; + if(check_only) + os_err("can't use output filename %s with -c", outname); + if(!freopen(outname, "wb", stdout)) + os_err("%s: %s", outname, strerror(errno)); + verbose(1, "redirected stdout to %s", outname); + } else if(crunch || decrunch) { + os_err("can't use stdout with -%c, must give an output filename", + crunch ? 'C' : 'D'); + } +} + +void open_output() { + if(check_only) { + outfile = freopen("/dev/null", "wb", stdout); + if(!outfile) { + os_err("/dev/null: %s", strerror(errno)); + } + verbose(1, "using /dev/null for output (check_only)"); + } else if(raw_output || unlock_mode || crunch || decrunch) { + if(isatty(fileno(stdout))) { + os_err("refusing to write %s to a terminal", + (raw_output ? "raw ATASCII" : "tokenized BASIC")); + } + outfile = stdout; + verbose(1, "using stdout for output"); + } else { + if(color) { + char *exe = getenv(ENV_VAR); + if(!exe) exe = "colorize-amsb"; + verbose(1, "using pipe for output: %s", exe); + outfile = popen(exe, "w"); /* "wb" not allowed! */ + /* we probably never see this error. popen() fails only if + we feed it an invalid mode, or if fork() or pipe() fails, + or I suppose if some idjit has done a "rm -f /bin/sh"... + all other errors are caught at pclose(). */ + if(!outfile) os_err("|%s: %s", exe, strerror(errno)); + need_pclose = 1; + } else { + outfile = stdout; + } + } +} + +int main(int argc, char **argv) { + set_self(argv[0]); + + parse_args(argc, argv); + + open_output(); + + read_header(); + + if(unlock_mode) { + unlock_program(); + exit(0); /* don't need finish() here, no parsing done */ + } else if(crunch || decrunch) { + crunch_program(); + exit(0); + } + + start_listing(); + + while(next_line()) + linecount++; + + finish(0); + return 0; /* never executes; shuts up gcc warning */ +} |