aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorB. Watson <urchlay@slackware.uk>2025-11-14 04:22:39 -0500
committerB. Watson <urchlay@slackware.uk>2025-11-14 04:22:39 -0500
commite286ed23ae3cabfb75327d8512dc937b2ecf9be1 (patch)
treea39a2494cdcca3145f283a515bfa42eb5f33e0df /src
parente2da2bffe58a76c091d3496bd3ca2d2f18ea2eb6 (diff)
downloadunalf-e286ed23ae3cabfb75327d8512dc937b2ecf9be1.tar.gz
Add command-line options.
Diffstat (limited to 'src')
-rw-r--r--src/Makefile6
-rw-r--r--src/extract.c75
-rw-r--r--src/glob.c120
-rw-r--r--src/io.c16
-rw-r--r--src/listalf.c52
-rw-r--r--src/opts.c65
-rw-r--r--src/unalf.1103
-rw-r--r--src/unalf.c60
-rw-r--r--src/unalf.h35
-rw-r--r--src/unalf.rst100
10 files changed, 531 insertions, 101 deletions
diff --git a/src/Makefile b/src/Makefile
index 56951f2..f8b73cf 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -3,10 +3,14 @@ CFLAGS=-Wall -Wno-unused-label -I../f65 $(COPT)
all: unalf unalf.1 alfsum alfsum.1
-unalf: unalf.o io.o listalf.o extract.o ../f65/f65.o
+unalf: unalf.o io.o listalf.o extract.o ../f65/f65.o glob.o opts.o
unalf.o: unalf.c unalf.h ../f65/f65.h
+glob.o: glob.c unalf.h
+
+opts.o: opts.c unalf.h
+
io.o: io.c unalf.h addrs.h ../f65/f65.h
listalf.o: listalf.c addrs.h unalf.h ../f65/f65.h
diff --git a/src/extract.c b/src/extract.c
index 079fe7f..4f761e4 100644
--- a/src/extract.c
+++ b/src/extract.c
@@ -1,9 +1,14 @@
#include <stdio.h>
#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+#include <limits.h>
#include <f65.h>
#include "unalf.h"
#include "addrs.h"
+char *out_filename;
+
void dpoke(int addr, u16 value) {
mem[addr] = value & 0xff;
mem[addr + 1] = value >> 8;
@@ -13,29 +18,79 @@ u16 dpeek(int addr) {
return mem[addr] | (mem[addr + 1] << 8);
}
-void extract_alf(void) {
- char *filename;
+void fix_filename(void) {
+ char *p;
+
+ if(!out_filename) return;
+
+ if(opts.lowercase) {
+ for(p = out_filename; *p; p++)
+ *p = tolower(*p);
+ }
+
+ if(!opts.keepdot) {
+ for(p = out_filename; *p; p++)
+ if(p[0] == '.' && !p[1])
+ *p = '\0';
+ }
+}
+
+void make_backup(void) {
+ char backup[PATH_MAX];
+
+ strcpy(backup, out_filename);
+ strcat(backup, "~");
+ /* silently ignore errors! */
+ rename(out_filename, backup);
+}
+
+void extract_alf(void) {
/* get ready to call fake 6502 stuff. set up memory like the Atari. */
dpoke(MEMTOP, 0xbc1f);
while(read_alf_header()) {
- filename = (char *)(mem + alf_hdr_filename);
- printf("Uncrunching %s\n", filename);
+ out_filename = (char *)(mem + alf_hdr_filename);
- if(!(out_file = fopen(filename, "wb"))) {
- fprintf(stderr, "%s: ", self);
- perror(filename);
- exit(1);
+ fix_filename();
+
+ if(!file_wanted(out_filename)) {
+ if(fseek(in_file, getquad(alf_hdr_compsize0), SEEK_CUR) != 0) {
+ fputs(self, stderr);
+ perror(": fseek");
+ exit(1);
+ }
+ out_filename = 0;
+ continue;
+ }
+
+ if(!opts.quiet) {
+ printf("%s %s\n", opts.testonly ? "Testing" : "Uncrunching", out_filename);
+ }
+
+ if(opts.extract_to_stdout) {
+ out_file = stdout;
+ } else {
+ if(opts.testonly) {
+ out_filename = "/dev/null";
+ } else if(!opts.overwrite) {
+ make_backup();
+ }
+ if(!(out_file = fopen(out_filename, "wb"))) {
+ perror(out_filename);
+ exit(1);
+ }
}
uncrunch_file();
- fclose(out_file);
+
+ if(!opts.extract_to_stdout) fclose(out_file);
+ out_filename = 0;
}
}
static void chksum_err(void) {
- fprintf(stderr, "%s: checksum error on file %s\n", self, in_filename);
+ fprintf(stderr, "%s: checksum error on file %s\n", self, out_filename);
}
void uncrunch_file(void) {
diff --git a/src/glob.c b/src/glob.c
new file mode 100644
index 0000000..cb91b79
--- /dev/null
+++ b/src/glob.c
@@ -0,0 +1,120 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include "unalf.h"
+
+/* this is an Atari DOS or MS-DOS style match: "*.*" matches everything,
+ even files with no extension (no dot). "*" by itself matches only
+ files with no extension, as does "*."
+ matching is case-insensitive. */
+
+static void split_filename(const char *fname, char *name, const char **ext) {
+ int i;
+
+ for(i = 0; i < 8; i++) {
+ if(fname[i] == '.' || fname[i] == '\0')
+ break;
+ name[i] = fname[i];
+ }
+ name[i] = '\0';
+
+ *ext = strchr(fname, '.');
+ if(*ext) {
+ (*ext)++;
+ if(!**ext) *ext = 0;
+ }
+
+ // fprintf(stderr, "split %s into: '%s' and '%s'\n",
+ // fname, name, (*ext ? *ext : "<none>"));
+}
+
+static int match_part(const char *pat, const char *arg) {
+ char p;
+
+ if(arg && arg[0] == '\0') arg = 0;
+
+ if(!pat) {
+ if(arg)
+ return 0;
+ else
+ return 1;
+ }
+
+ while((p = toupper(*pat++))) {
+ // fprintf(stderr, "p == %c\n", p);
+ switch(p) {
+ case '*': return 1;
+ case '?':
+ if(!arg || *arg == '\0')
+ return 0;
+ arg++;
+ break;
+ default:
+ if(!arg || toupper(*arg) != p)
+ return 0;
+ arg++;
+ break;
+ }
+ }
+
+ return *arg ? 0 : 1;
+}
+
+int globmatch(const char *pat, const char *arg) {
+ char npat[9], name[9];
+ const char *epat = 0, *ext = 0;
+
+ split_filename(pat, npat, &epat);
+ split_filename(arg, name, &ext);
+
+ return match_part(npat, name) && match_part(epat, ext);
+}
+
+int file_wanted(const char *filename) {
+ int i;
+ char **ig;
+
+ /* if excluded, always return false */
+ for(i = 0; i < exclude_count; i++) {
+ if(globmatch(exclude_globs[i], filename))
+ return 0;
+ }
+
+ /* if no include globs, always match (as though *.* were given) */
+ if(!*include_globs)
+ return 1;
+
+ ig = (char **)include_globs;
+ while(*ig) {
+ if(globmatch(*ig, filename))
+ return 1;
+ ig++;
+ }
+
+ return 0;
+}
+
+/* for testing: */
+#if 0
+int main(int argc, char **argv) {
+ int matched;
+ const char *pattern;
+
+ if(argc < 3) {
+ fprintf(stderr, "usage: %s <glob-pattern> <filename> ...\n", argv[0]);
+ return 1;
+ }
+
+ pattern = argv[1];
+ argv++;
+
+ while(*++argv) {
+ matched = globmatch(pattern, *argv);
+ printf("%s %s %s\n", *argv, matched ? "matches" : "DOES NOT match", pattern);
+ }
+
+ return 0;
+}
+#endif
diff --git a/src/io.c b/src/io.c
index 9b8e40e..8469a6b 100644
--- a/src/io.c
+++ b/src/io.c
@@ -76,15 +76,29 @@ void readblock(void) {
dpoke(buf_len_l, bytes);
}
+int is_text_file(char *fn) {
+ if(globmatch("*.txt", fn)) return 1;
+ if(globmatch("*.doc", fn)) return 1;
+ if(globmatch("*.lst", fn)) return 1;
+ return 0;
+}
+
/* mirror of readblock() */
void writeblock(void) {
- int bytes, len, bufadr;
+ int i, bytes, len, bufadr;
u8 *buf;
+ extern char *out_filename;
bufadr = dpeek(buf_adr_l);
buf = mem + bufadr;
len = dpeek(buf_len_l);
+ if(opts.txtconv) {
+ if(opts.txtconv > 1 || is_text_file(out_filename))
+ for(i = 0; i < len; i++)
+ if(buf[i] == 0x9b) buf[i] = '\n';
+ }
+
// fprintf(stderr, "writeblock, bufadr = $%04x, len = $%04x\n", bufadr, len);
bytes = fwrite(buf, 1, len, out_file);
diff --git a/src/listalf.c b/src/listalf.c
index 051f2fe..71d8195 100644
--- a/src/listalf.c
+++ b/src/listalf.c
@@ -77,30 +77,40 @@ static int comp_percent(unsigned int orig, unsigned int comp) {
void list_alf(void) {
unsigned int c = 0, orig_size, comp_size, total_osize = 0, total_csize = 0;
char buf[100];
- char *filename;
+ extern char *out_filename;
- puts("Name Length Size now Comp Date Time CkSum");
- puts("============ ======== ======== ==== ========= ====== =====");
+ if(opts.verbose_list) {
+ puts("Name Length Size now Comp Date Time CkSum");
+ puts("============ ======== ======== ==== ========= ====== =====");
+ }
while(read_alf_header()) {
c++;
orig_size = getquad(alf_hdr_origsize0);
comp_size = getquad(alf_hdr_compsize0);
- filename = (char *)(mem + alf_hdr_filename);
+ out_filename = (char *)(mem + alf_hdr_filename);
+
+ if(!opts.verbose_list)
+ fix_filename();
total_osize += orig_size;
total_csize += comp_size;
- printf("%-12s ", filename);
- printf("%8d ", orig_size);
- printf("%8d ", comp_size);
- printf("%3d%% ", comp_percent(orig_size, comp_size));
- format_msdos_date(buf);
- printf("%9s ", buf);
- format_msdos_time(buf);
- printf("%6s ", buf);
- printf(" %04x", getword(alf_hdr_cksum_l));
- putchar('\n');
+ if(opts.verbose_list) {
+ printf("%-12s ", out_filename);
+ printf("%8d ", orig_size);
+ printf("%8d ", comp_size);
+ printf("%3d%% ", comp_percent(orig_size, comp_size));
+ format_msdos_date(buf);
+ printf("%9s ", buf);
+ format_msdos_time(buf);
+ printf("%6s ", buf);
+ printf(" %04x", getword(alf_hdr_cksum_l));
+ putchar('\n');
+ } else {
+ if(file_wanted(out_filename))
+ printf("%s\n", out_filename);
+ }
if(fseek(in_file, comp_size, SEEK_CUR) != 0) {
fputs(self, stderr);
@@ -109,10 +119,12 @@ void list_alf(void) {
}
}
- fputs(" ==== ======== ======== ====\nTotal ", stdout);
- printf("%4d ", c);
- printf("%8d ", total_osize);
- printf("%8d ", total_csize);
- printf("%3d%% ", comp_percent(total_osize, total_csize));
- putchar('\n');
+ if(opts.verbose_list) {
+ fputs(" ==== ======== ======== ====\nTotal ", stdout);
+ printf("%4d ", c);
+ printf("%8d ", total_osize);
+ printf("%8d ", total_csize);
+ printf("%3d%% ", comp_percent(total_osize, total_csize));
+ putchar('\n');
+ }
}
diff --git a/src/opts.c b/src/opts.c
new file mode 100644
index 0000000..2a5628e
--- /dev/null
+++ b/src/opts.c
@@ -0,0 +1,65 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include "unalf.h"
+
+#define OPTIONS "aeklLopqtvd:x:"
+
+/* uncomment to test exclude/include glob lists */
+// #define DEBUG_GLOBS
+
+static void add_exclude(const char *glob) {
+ if(exclude_count == MAX_EXCLUDES)
+ return;
+ exclude_globs[exclude_count++] = glob;
+}
+
+#ifdef DEBUG_GLOBS
+static void show_globs(void) {
+ int i;
+
+ printf("Include globs:\n");
+ while(*include_globs)
+ printf(" + %s\n", *include_globs++);
+ printf("Exclude globs:\n");
+ for(i = 0; i < exclude_count; i++)
+ printf(" - %s\n", exclude_globs[i]);
+ exit(0);
+}
+#endif
+
+void parse_opts(int argc, char * const *argv) {
+ int opt;
+
+ while((opt = getopt(argc, argv, OPTIONS)) != -1) {
+ switch(opt) {
+ case 'a': opts.txtconv++; break;
+ case 'e': opts.listonly = opts.testonly = 0; break;
+ case 'k': opts.keepdot++; break;
+ case 'l': opts.listonly++; opts.testonly = 0; break;
+ case 'L': opts.lowercase++; break;
+ case 'o': opts.overwrite++; break;
+ case 'p': opts.extract_to_stdout++; opts.quiet++; break;
+ case 'q': opts.quiet++; break;
+ case 't': opts.testonly++; opts.listonly = 0; break;
+ case 'v': opts.listonly = 1; opts.testonly = 0; opts.verbose_list++; break;
+ case 'd': opts.outdir = optarg; break;
+ case 'x': add_exclude(optarg); break;
+ default:
+ fprintf(stderr, "%s: invalid option (try -h or --help)\n", self);
+ exit(1);
+ }
+ }
+
+ if(optind >= argc) {
+ fprintf(stderr, "%s: missing alf file argument (try -h or --help)\n", self);
+ exit(1);
+ }
+
+ in_filename = argv[optind];
+ include_globs = &argv[optind + 1]; /* might be null, that's OK */
+
+#ifdef DEBUG_GLOBS
+ show_globs();
+#endif
+}
diff --git a/src/unalf.1 b/src/unalf.1
index 3ed769b..040a94e 100644
--- a/src/unalf.1
+++ b/src/unalf.1
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "UNALF" 1 "2025-11-12" "0.0.0" "Urchlay's Atari 8-bit Tools"
+.TH "UNALF" 1 "2025-11-14" "0.0.0" "Urchlay's Atari 8-bit Tools"
.SH NAME
unalf \- extract Atari 8-bit ALF archives
.\" RST source for unalf(1) man page. Convert with:
@@ -36,7 +36,7 @@ unalf \- extract Atari 8-bit ALF archives
.
.SH SYNOPSIS
.sp
-unalf [\fB\-l\fP] \fBalf\-file\fP
+unalf [\fB\-aetklLopqv\fP] [\fB\-d\fP \fIdir\fP] [\fB\-x\fP \fIwildcard\fP] \fIalf\-file\fP [\fIwildcard\fP ...]
.SH DESCRIPTION
.sp
\fBunalf\fP lists or extracts the contents of an \fIALF\fP archive.
@@ -45,30 +45,89 @@ unalf [\fB\-l\fP] \fBalf\-file\fP
not compatible with it. It was used on the Atari 8\-bit platform
beginning in the late 1980s.
.sp
-Extracted files are written to the current directory. Existing files
-are overwritten without warning.
-.SH OPTIONS
+Extracted files are written to the current directory by
+default. Existing files are backed up by default (by adding a \fB~\fP
+suffix to the filename).
.sp
-For compatibility with \fBarc\fP(1) usage, the \fB\-\fP in front of
-option letters is optional.
+If one or more \fBwildcard\fP arguments are provided, files will only be
+listed or extracted if they match one of the wildcards. See \fBWILDCARDS\fP
+below for details.
+.SH OPTIONS
.INDENT 0.0
.TP
-.B \-l
-List contents of archive, but do not extract. The output format
-is similar to that of \fBarc \-v\fP, minus the \fIStowage\fP column, since
-\fIALF\fP doesn\(aqt support multiple compression types. The date and time
-are displayed, but in most .alf files these are the default values
-of "8 Jan 82 12:24a".
+.B \-a
+Convert text file line endings from Atari EOL to UNIX newline. Text
+files are detected by name: \fB*.txt\fP, \fB*.doc\fP, \fB*.lst\fP files
+are considered text.
.TP
-.B \-v
-Same as \fB\-l\fP\&.
-.UNINDENT
-.INDENT 0.0
+.BI \-a\fB a
+Convert line endings in all extracted files. This will corrupt any
+executables or non\-text data files, so use with caution.
.TP
-.B \fB\-e\fP, \fB\-x\fP
+.BI \-d \ output\-dir
+Write extracted files to this directory, which will be created if it
+does not exist. The default is the current directory.
+.TP
+.B \-e
Extract files. This is actually a no\-op, as extraction is the
default action.
+.TP
+.B \-k
+Keep trailing periods. In most (all?) ALF files, filenames that have no
+extension are stored with a trailing period, e.g. \(aq\fBFOO\fP\(aq would
+be stored as \(aq\fBFOO.\fP\(aq. By default, \fBunalf\fP removes the trailing
+period; use \fB\-k\fP to keep it.
+.TP
+.B \-l
+List filenames in archive, one per line, but do not extract. Use
+\fB\-v\fP for more information (file size, etc).
+.TP
+.B \-L
+Lowercase filesnames. Example: \fBFOO.TXT\fP will extract to \fBfoo.txt\fP\&.
+.TP
+.B \-o
+Overwrite files, if they already exist. The default is to rename
+existing files, adding a \fB~\fP suffix. Note that renaming \fBfile\fP
+to \fBfile~\fP \fIwill\fP overwrite \fBfile~\fP if it already exists.
+.TP
+.B \-p
+Extract to standard output. All the files that get extracted will be
+printed to stdout with no delimiters, so this is most useful when
+only extracting one file. \fB\-q\fP is automatically enabled by this option.
+.TP
+.B \-q
+Quiet extraction: do not print "Uncrunching \fIfile\fP" messages. Errors
+and warnings will still be printed. This is the default when \fB\-p\fP
+is used.
+.TP
+.B \-v
+Verbose listing of archive contents, with compressed and original
+sizes, compression ration, date/time stamps, and checksum. The
+output format is similar to that of \fBarc \-v\fP, minus the \fIStowage\fP
+column, since \fIALF\fP doesn\(aqt support multiple compression types. The
+date and time are displayed, but in most .alf files these are the
+default values of "8 Jan 82 12:24a".
+.TP
+.BI \-x \ wildcard
+Exclude (do not list or extract) files matching \fIwildcard\fP\&. See
+\fBWILDCARDS\fP below. This option can be given multiple times
+to exclude multiple patterns.
.UNINDENT
+.SH WILDCARDS
+.sp
+Wildcard (aka glob) matching works like it does on the Atari: \fI*.*\fP
+matches any filename (whether or not it has an extension), and \fI*\fP
+or \fI*.\fP only match filenames with no extension.
+.sp
+A \fI*\fP matches any number of characters (including zero). A \fI?\fP
+matches a single character.
+.sp
+Like the Atari, anything in the name or extension following a \fI*\fP is
+ignored: \fBFOO*BAR.TXT\fP is equivalent to \fBFOO*.TXT\fP, and
+\fBFOO.T*T\fP is equivalent to \fBFOO.T*\fP\&. \fBFOO*TXT\fP is
+equivalent to \fBFOO*\fP, and \fIwill not\fP match the file \fBFOO.TXT\fP\&.
+.sp
+Unlike the Atari, matching is case\-insensitive.
.SH EXIT STATUS
.sp
\fB0\fP for success, \fB1\fP for failure.
@@ -106,12 +165,10 @@ aka \fBDZ.COM\fP, with the following differences:
.INDENT 0.0
.IP \(bu 2
There is no interactive mode. The file to extract must be given as
-a command\-line argument.
-.IP \(bu 2
-Files are always extracted to the current directory. The original
-\fBUNALF.COM\fP accepted a 2nd argument or prompted for the output directory.
+a command\-line argument, and the output directory must be given
+with the \fB\-d\fP option.
.IP \(bu 2
-This \fBunalf\fP is capable of listing the contents of an archive
+This \fBunalf\fP is capable of listing or testing the contents of an archive
without extracting it.
.IP \(bu 2
Turning the screen off for speed makes no sense on modern operating
diff --git a/src/unalf.c b/src/unalf.c
index ba6405e..9a722f8 100644
--- a/src/unalf.c
+++ b/src/unalf.c
@@ -2,11 +2,18 @@
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
+#include <errno.h>
+#include <sys/stat.h>
#include <f65.h>
#include "unalf.h"
FILE *in_file, *out_file;
char *in_filename, *self;
+opts_t opts;
+const char *exclude_globs[MAX_EXCLUDES];
+int exclude_count;
+char * const *include_globs;
+static void create_outdir(void);
static void usage(void) {
printf("usage: %s [-l] file.alf\n", self);
@@ -21,35 +28,32 @@ static void set_self(char *argv0) {
if(p) self = p + 1;
}
-int main(int argc, char **argv) {
- char opt = 0;
- int listonly = 0;
+/* like "mkdir -p" (no error if dir already exists),
+ followed by "cd" (which will error if the existing
+ "directory" turns out to be a file or broken symlink */
+static void create_outdir(void) {
+ int r;
- set_self(argv[0]);
+ r = mkdir(opts.outdir, 0777);
+ if(r < 0 && errno != EEXIST) {
+ fprintf(stderr, "%s: ", self);
+ perror(opts.outdir);
+ exit(1);
+ }
- if(argc == 1) {
- fprintf(stderr, "%s: missing required argument(s)\n", self);
- usage();
- } else if(argc > 3) {
- fprintf(stderr, "%s: too many arguments\n", self);
- usage();
- } else if(argc == 2) {
- in_filename = argv[1];
- } else { /* argc == 3 */
- in_filename = argv[2];
- if(argv[1][0] == '-')
- opt = argv[1][1];
- else
- opt = argv[1][0];
- if(opt == 'l' || opt == 'v') {
- listonly = 1;
- } else if(opt == 'x' || opt == 'e') {
- /* NOP */
- } else {
- fprintf(stderr, "%s: unknown option '-%c'\n", self, opt);
- usage();
- }
+ if(chdir(opts.outdir) < 0) {
+ fprintf(stderr, "%s: ", self);
+ perror(opts.outdir);
+ exit(1);
}
+}
+
+int main(int argc, char **argv) {
+ set_self(argv[0]);
+
+ if(argc < 2) usage();
+
+ parse_opts(argc, argv);
if(!(in_file = fopen(in_filename, "rb"))) {
fprintf(stderr, "%s: ", self);
@@ -57,7 +61,9 @@ int main(int argc, char **argv) {
exit(1);
}
- if(listonly)
+ if(opts.outdir) create_outdir();
+
+ if(opts.listonly)
list_alf();
else
extract_alf();
diff --git a/src/unalf.h b/src/unalf.h
index 855ca80..030f6f2 100644
--- a/src/unalf.h
+++ b/src/unalf.h
@@ -1,3 +1,8 @@
+#ifndef u8
+#define u8 unsigned char
+#define u16 unsigned short
+#endif
+
/* converted from asm */
void uncrunch_file(void);
void setup_io_bufs(void);
@@ -16,6 +21,10 @@ void L79E7(void);
void writeblock(void);
void L7A5D(void);
+/* glob.c */
+int globmatch(const char *pat, const char *arg);
+int file_wanted(const char *filename);
+
/* io.c - asm rewritten in C */
int read_alf_header(void);
void readblock(void);
@@ -27,14 +36,40 @@ void open_output(void);
/* listalf.c */
void list_alf(void);
+unsigned int getquad(int offs);
/* extract.c */
void extract_alf(void);
void dpoke(int addr, u16 value);
u16 dpeek(int addr);
+void fix_filename(void);
+
+/* opts.c */
+void parse_opts(int argc, char * const *argv);
/* unalf.c */
extern char *self;
extern FILE *out_file;
extern FILE *in_file;
extern char *in_filename;
+
+typedef struct {
+ int txtconv;
+ int testonly;
+ int keepdot;
+ int listonly;
+ int lowercase;
+ const char *outdir;
+ int overwrite;
+ int extract_to_stdout;
+ int quiet;
+ char *exclude_glob;
+ int verbose_list;
+} opts_t;
+
+extern opts_t opts;
+extern const char *exclude_globs[256];
+extern char * const *include_globs;
+extern int exclude_count;
+
+#define MAX_EXCLUDES 256
diff --git a/src/unalf.rst b/src/unalf.rst
index 774ddf2..9eaa0ad 100644
--- a/src/unalf.rst
+++ b/src/unalf.rst
@@ -20,7 +20,7 @@ extract Atari 8-bit ALF archives
SYNOPSIS
========
-unalf [**-l**] **alf-file**
+unalf [**-aetklLopqv**] [**-d** *dir*] [**-x** *wildcard*] *alf-file* [*wildcard* ...]
DESCRIPTION
===========
@@ -31,29 +31,93 @@ DESCRIPTION
not compatible with it. It was used on the Atari 8-bit platform
beginning in the late 1980s.
-Extracted files are written to the current directory. Existing files
-are overwritten without warning.
+Extracted files are written to the current directory by
+default. Existing files are backed up by default (by adding a **~**
+suffix to the filename).
+
+If one or more **wildcard** arguments are provided, files will only be
+listed or extracted if they match one of the wildcards. See **WILDCARDS**
+below for details.
OPTIONS
=======
-For compatibility with **arc**\(1) usage, the **-** in front of
-option letters is optional.
+-a
+ Convert text file line endings from Atari EOL to UNIX newline. Text
+ files are detected by name: **\*.txt**, **\*.doc**, **\*.lst** files
+ are considered text.
--l
- List contents of archive, but do not extract. The output format
- is similar to that of **arc -v**, minus the *Stowage* column, since
- *ALF* doesn't support multiple compression types. The date and time
- are displayed, but in most .alf files these are the default values
- of "8 Jan 82 12:24a".
+-aa
+ Convert line endings in all extracted files. This will corrupt any
+ executables or non-text data files, so use with caution.
--v
- Same as **-l**.
+-d output-dir
+ Write extracted files to this directory, which will be created if it
+ does not exist. The default is the current directory.
-**-e**, **-x**
+-e
Extract files. This is actually a no-op, as extraction is the
default action.
+-k
+ Keep trailing periods. In most (all?) ALF files, filenames that have no
+ extension are stored with a trailing period, e.g. '**FOO**' would
+ be stored as '**FOO.**'. By default, **unalf** removes the trailing
+ period; use **-k** to keep it.
+
+-l
+ List filenames in archive, one per line, but do not extract. Use
+ **-v** for more information (file size, etc).
+
+-L
+ Lowercase filesnames. Example: **FOO.TXT** will extract to **foo.txt**.
+
+-o
+ Overwrite files, if they already exist. The default is to rename
+ existing files, adding a **~** suffix. Note that renaming **file**
+ to **file~** *will* overwrite **file~** if it already exists.
+
+-p
+ Extract to standard output. All the files that get extracted will be
+ printed to stdout with no delimiters, so this is most useful when
+ only extracting one file. **-q** is automatically enabled by this option.
+
+-q
+ Quiet extraction: do not print "Uncrunching *file*" messages. Errors
+ and warnings will still be printed. This is the default when **-p**
+ is used.
+
+-v
+ Verbose listing of archive contents, with compressed and original
+ sizes, compression ration, date/time stamps, and checksum. The
+ output format is similar to that of **arc -v**, minus the *Stowage*
+ column, since *ALF* doesn't support multiple compression types. The
+ date and time are displayed, but in most .alf files these are the
+ default values of "8 Jan 82 12:24a".
+
+
+-x wildcard
+ Exclude (do not list or extract) files matching *wildcard*. See
+ **WILDCARDS** below. This option can be given multiple times
+ to exclude multiple patterns.
+
+WILDCARDS
+=========
+
+Wildcard (aka glob) matching works like it does on the Atari: *\*.\**
+matches any filename (whether or not it has an extension), and *\**
+or *\*.* only match filenames with no extension.
+
+A *\** matches any number of characters (including zero). A *?*
+matches a single character.
+
+Like the Atari, anything in the name or extension following a *\** is
+ignored: **FOO\*BAR.TXT** is equivalent to **FOO\*.TXT**, and
+**FOO.T\*T** is equivalent to **FOO.T\***. **FOO\*TXT** is
+equivalent to **FOO\***, and *will not* match the file **FOO.TXT**.
+
+Unlike the Atari, matching is case-insensitive.
+
EXIT STATUS
===========
@@ -93,12 +157,10 @@ This **unalf** is 100% compatible with the original Atari **UNALF.COM**
aka **DZ.COM**, with the following differences:
- There is no interactive mode. The file to extract must be given as
- a command-line argument.
-
-- Files are always extracted to the current directory. The original
- **UNALF.COM** accepted a 2nd argument or prompted for the output directory.
+ a command-line argument, and the output directory must be given
+ with the **-d** option.
-- This **unalf** is capable of listing the contents of an archive
+- This **unalf** is capable of listing or testing the contents of an archive
without extracting it.
- Turning the screen off for speed makes no sense on modern operating