exp/regexp: add CompilePOSIX, more tests

R=r
CC=golang-dev
https://golang.org/cl/4967060
diff --git a/src/pkg/exp/regexp/testdata/testregex.c b/src/pkg/exp/regexp/testdata/testregex.c
new file mode 100644
index 0000000..37545d0
--- /dev/null
+++ b/src/pkg/exp/regexp/testdata/testregex.c
@@ -0,0 +1,2286 @@
+#pragma prototyped noticed
+
+/*
+ * regex(3) test harness
+ *
+ * build:	cc -o testregex testregex.c
+ * help:	testregex --man
+ * note:	REG_* features are detected by #ifdef; if REG_* are enums
+ *		then supply #define REG_foo REG_foo for each enum REG_foo
+ *
+ *	Glenn Fowler <gsf@research.att.com>
+ *	AT&T Research
+ *
+ * PLEASE: publish your tests so everyone can benefit
+ *
+ * The following license covers testregex.c and all associated test data.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of THIS SOFTWARE FILE (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following disclaimer:
+ *
+ * THIS SOFTWARE IS PROVIDED BY AT&T ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL AT&T BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+static const char id[] = "\n@(#)$Id: testregex (AT&T Research) 2010-06-10 $\0\n";
+
+#if _PACKAGE_ast
+#include <ast.h>
+#else
+#include <sys/types.h>
+#endif
+
+#include <stdio.h>
+#include <regex.h>
+#include <ctype.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifdef	__STDC__
+#include <stdlib.h>
+#include <locale.h>
+#endif
+
+#ifndef RE_DUP_MAX
+#define RE_DUP_MAX	32767
+#endif
+
+#if !_PACKAGE_ast
+#undef	REG_DISCIPLINE
+#endif
+
+#ifndef REG_DELIMITED
+#undef	_REG_subcomp
+#endif
+
+#define TEST_ARE		0x00000001
+#define TEST_BRE		0x00000002
+#define TEST_ERE		0x00000004
+#define TEST_KRE		0x00000008
+#define TEST_LRE		0x00000010
+#define TEST_SRE		0x00000020
+
+#define TEST_EXPAND		0x00000100
+#define TEST_LENIENT		0x00000200
+
+#define TEST_QUERY		0x00000400
+#define TEST_SUB		0x00000800
+#define TEST_UNSPECIFIED	0x00001000
+#define TEST_VERIFY		0x00002000
+#define TEST_AND		0x00004000
+#define TEST_OR			0x00008000
+
+#define TEST_DELIMIT		0x00010000
+#define TEST_OK			0x00020000
+#define TEST_SAME		0x00040000
+
+#define TEST_ACTUAL		0x00100000
+#define TEST_BASELINE		0x00200000
+#define TEST_FAIL		0x00400000
+#define TEST_PASS		0x00800000
+#define TEST_SUMMARY		0x01000000
+
+#define TEST_IGNORE_ERROR	0x02000000
+#define TEST_IGNORE_OVER	0x04000000
+#define TEST_IGNORE_POSITION	0x08000000
+
+#define TEST_CATCH		0x10000000
+#define TEST_VERBOSE		0x20000000
+
+#define TEST_DECOMP		0x40000000
+
+#define TEST_GLOBAL		(TEST_ACTUAL|TEST_AND|TEST_BASELINE|TEST_CATCH|TEST_FAIL|TEST_IGNORE_ERROR|TEST_IGNORE_OVER|TEST_IGNORE_POSITION|TEST_OR|TEST_PASS|TEST_SUMMARY|TEST_VERBOSE)
+
+#ifdef REG_DISCIPLINE
+
+
+#include <stk.h>
+
+typedef struct Disc_s
+{
+	regdisc_t	disc;
+	int		ordinal;
+	Sfio_t*		sp;
+} Disc_t;
+
+static void*
+compf(const regex_t* re, const char* xstr, size_t xlen, regdisc_t* disc)
+{
+	Disc_t*		dp = (Disc_t*)disc;
+
+	return (void*)((char*)0 + ++dp->ordinal);
+}
+
+static int
+execf(const regex_t* re, void* data, const char* xstr, size_t xlen, const char* sstr, size_t slen, char** snxt, regdisc_t* disc)
+{
+	Disc_t*		dp = (Disc_t*)disc;
+
+	sfprintf(dp->sp, "{%-.*s}(%lu:%d)", xlen, xstr, (char*)data - (char*)0, slen);
+	return atoi(xstr);
+}
+
+static void*
+resizef(void* handle, void* data, size_t size)
+{
+	if (!size)
+		return 0;
+	return stkalloc((Sfio_t*)handle, size);
+}
+
+#endif
+
+#ifndef NiL
+#ifdef	__STDC__
+#define NiL		0
+#else
+#define NiL		(char*)0
+#endif
+#endif
+
+#define H(x)		do{if(html)fprintf(stderr,x);}while(0)
+#define T(x)		fprintf(stderr,x)
+
+static void
+help(int html)
+{
+H("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">\n");
+H("<HTML>\n");
+H("<HEAD>\n");
+H("<TITLE>testregex man document</TITLE>\n");
+H("</HEAD>\n");
+H("<BODY bgcolor=white>\n");
+H("<PRE>\n");
+T("NAME\n");
+T("  testregex - regex(3) test harness\n");
+T("\n");
+T("SYNOPSIS\n");
+T("  testregex [ options ]\n");
+T("\n");
+T("DESCRIPTION\n");
+T("  testregex reads regex(3) test specifications, one per line, from the\n");
+T("  standard input and writes one output line for each failed test. A\n");
+T("  summary line is written after all tests are done. Each successful\n");
+T("  test is run again with REG_NOSUB. Unsupported features are noted\n");
+T("  before the first test, and tests requiring these features are\n");
+T("  silently ignored.\n");
+T("\n");
+T("OPTIONS\n");
+T("  -c	catch signals and non-terminating calls\n");
+T("  -e	ignore error return mismatches\n");
+T("  -h	list help on standard error\n");
+T("  -n	do not repeat successful tests with regnexec()\n");
+T("  -o	ignore match[] overrun errors\n");
+T("  -p	ignore negative position mismatches\n");
+T("  -s	use stack instead of malloc\n");
+T("  -x	do not repeat successful tests with REG_NOSUB\n");
+T("  -v	list each test line\n");
+T("  -A	list failed test lines with actual answers\n");
+T("  -B	list all test lines with actual answers\n");
+T("  -F	list failed test lines\n");
+T("  -P	list passed test lines\n");
+T("  -S	output one summary line\n");
+T("\n");
+T("INPUT FORMAT\n");
+T("  Input lines may be blank, a comment beginning with #, or a test\n");
+T("  specification. A specification is five fields separated by one\n");
+T("  or more tabs. NULL denotes the empty string and NIL denotes the\n");
+T("  0 pointer.\n");
+T("\n");
+T("  Field 1: the regex(3) flags to apply, one character per REG_feature\n");
+T("  flag. The test is skipped if REG_feature is not supported by the\n");
+T("  implementation. If the first character is not [BEASKLP] then the\n");
+T("  specification is a global control line. One or more of [BEASKLP] may be\n");
+T("  specified; the test will be repeated for each mode.\n");
+T("\n");
+T("    B 	basic			BRE	(grep, ed, sed)\n");
+T("    E 	REG_EXTENDED		ERE	(egrep)\n");
+T("    A	REG_AUGMENTED		ARE	(egrep with negation)\n");
+T("    S	REG_SHELL		SRE	(sh glob)\n");
+T("    K	REG_SHELL|REG_AUGMENTED	KRE	(ksh glob)\n");
+T("    L	REG_LITERAL		LRE	(fgrep)\n");
+T("\n");
+T("    a	REG_LEFT|REG_RIGHT	implicit ^...$\n");
+T("    b	REG_NOTBOL		lhs does not match ^\n");
+T("    c	REG_COMMENT		ignore space and #...\\n\n");
+T("    d	REG_SHELL_DOT		explicit leading . match\n");
+T("    e	REG_NOTEOL		rhs does not match $\n");
+T("    f	REG_MULTIPLE		multiple \\n separated patterns\n");
+T("    g	FNM_LEADING_DIR		testfnmatch only -- match until /\n");
+T("    h	REG_MULTIREF		multiple digit backref\n");
+T("    i	REG_ICASE		ignore case\n");
+T("    j	REG_SPAN		. matches \\n\n");
+T("    k	REG_ESCAPE		\\ to ecape [...] delimiter\n");
+T("    l	REG_LEFT		implicit ^...\n");
+T("    m	REG_MINIMAL		minimal match\n");
+T("    n	REG_NEWLINE		explicit \\n match\n");
+T("    o	REG_ENCLOSED		(|&) magic inside [@|&](...)\n");
+T("    p	REG_SHELL_PATH		explicit / match\n");
+T("    q	REG_DELIMITED		delimited pattern\n");
+T("    r	REG_RIGHT		implicit ...$\n");
+T("    s	REG_SHELL_ESCAPED	\\ not special\n");
+T("    t	REG_MUSTDELIM		all delimiters must be specified\n");
+T("    u	standard unspecified behavior -- errors not counted\n");
+T("    v	REG_CLASS_ESCAPE	\\ special inside [...]\n");
+T("    w	REG_NOSUB		no subexpression match array\n");
+T("    x	REG_LENIENT		let some errors slide\n");
+T("    y	REG_LEFT		regexec() implicit ^...\n");
+T("    z	REG_NULL		NULL subexpressions ok\n");
+T("    $	                        expand C \\c escapes in fields 2 and 3\n");
+T("    /	                        field 2 is a regsubcomp() expression\n");
+T("    =	                        field 3 is a regdecomp() expression\n");
+T("\n");
+T("  Field 1 control lines:\n");
+T("\n");
+T("    C		set LC_COLLATE and LC_CTYPE to locale in field 2\n");
+T("\n");
+T("    ?test ...	output field 5 if passed and != EXPECTED, silent otherwise\n");
+T("    &test ...	output field 5 if current and previous passed\n");
+T("    |test ...	output field 5 if current passed and previous failed\n");
+T("    ; ...	output field 2 if previous failed\n");
+T("    {test ...	skip if failed until }\n");
+T("    }		end of skip\n");
+T("\n");
+T("    : comment		comment copied as output NOTE\n");
+T("    :comment:test	:comment: ignored\n");
+T("    N[OTE] comment	comment copied as output NOTE\n");
+T("    T[EST] comment	comment\n");
+T("\n");
+T("    number		use number for nmatch (20 by default)\n");
+T("\n");
+T("  Field 2: the regular expression pattern; SAME uses the pattern from\n");
+T("    the previous specification. RE_DUP_MAX inside {...} expands to the\n");
+T("    value from <limits.h>.\n");
+T("\n");
+T("  Field 3: the string to match. X...{RE_DUP_MAX} expands to RE_DUP_MAX\n");
+T("    copies of X.\n");
+T("\n");
+T("  Field 4: the test outcome. This is either one of the posix error\n");
+T("    codes (with REG_ omitted) or the match array, a list of (m,n)\n");
+T("    entries with m and n being first and last+1 positions in the\n");
+T("    field 3 string, or NULL if REG_NOSUB is in effect and success\n");
+T("    is expected. BADPAT is acceptable in place of any regcomp(3)\n");
+T("    error code. The match[] array is initialized to (-2,-2) before\n");
+T("    each test. All array elements from 0 to nmatch-1 must be specified\n");
+T("    in the outcome. Unspecified endpoints (offset -1) are denoted by ?.\n");
+T("    Unset endpoints (offset -2) are denoted by X. {x}(o:n) denotes a\n");
+T("    matched (?{...}) expression, where x is the text enclosed by {...},\n");
+T("    o is the expression ordinal counting from 1, and n is the length of\n");
+T("    the unmatched portion of the subject string. If x starts with a\n");
+T("    number then that is the return value of re_execf(), otherwise 0 is\n");
+T("    returned. RE_DUP_MAX[-+]N expands to the <limits.h> value -+N.\n");
+T("\n");
+T("  Field 5: optional comment appended to the report.\n");
+T("\n");
+T("CAVEAT\n");
+T("    If a regex implementation misbehaves with memory then all bets are off.\n");
+T("\n");
+T("CONTRIBUTORS\n");
+T("  Glenn Fowler    gsf@research.att.com        (ksh strmatch, regex extensions)\n");
+T("  David Korn      dgk@research.att.com        (ksh glob matcher)\n");
+T("  Doug McIlroy    mcilroy@dartmouth.edu       (ast regex/testre in C++)\n");
+T("  Tom Lord        lord@regexps.com            (rx tests)\n");
+T("  Henry Spencer   henry@zoo.toronto.edu       (original public regex)\n");
+T("  Andrew Hume     andrew@research.att.com     (gre tests)\n");
+T("  John Maddock    John_Maddock@compuserve.com (regex++ tests)\n");
+T("  Philip Hazel    ph10@cam.ac.uk              (pcre tests)\n");
+T("  Ville Laurikari vl@iki.fi                   (libtre tests)\n");
+H("</PRE>\n");
+H("</BODY>\n");
+H("</HTML>\n");
+}
+
+#ifndef elementsof
+#define elementsof(x)	(sizeof(x)/sizeof(x[0]))
+#endif
+
+#ifndef streq
+#define streq(a,b)	(*(a)==*(b)&&!strcmp(a,b))
+#endif
+
+#define HUNG		2
+#define NOTEST		(~0)
+
+#ifndef REG_TEST_DEFAULT
+#define REG_TEST_DEFAULT	0
+#endif
+
+#ifndef REG_EXEC_DEFAULT
+#define REG_EXEC_DEFAULT	0
+#endif
+
+static const char* unsupported[] =
+{
+	"BASIC",
+#ifndef REG_EXTENDED
+	"EXTENDED",
+#endif
+#ifndef REG_AUGMENTED
+	"AUGMENTED",
+#endif
+#ifndef REG_SHELL
+	"SHELL",
+#endif
+
+#ifndef REG_CLASS_ESCAPE
+	"CLASS_ESCAPE",
+#endif
+#ifndef REG_COMMENT
+	"COMMENT",
+#endif
+#ifndef REG_DELIMITED
+	"DELIMITED",
+#endif
+#ifndef REG_DISCIPLINE
+	"DISCIPLINE",
+#endif
+#ifndef REG_ESCAPE
+	"ESCAPE",
+#endif
+#ifndef REG_ICASE
+	"ICASE",
+#endif
+#ifndef REG_LEFT
+	"LEFT",
+#endif
+#ifndef REG_LENIENT
+	"LENIENT",
+#endif
+#ifndef REG_LITERAL
+	"LITERAL",
+#endif
+#ifndef REG_MINIMAL
+	"MINIMAL",
+#endif
+#ifndef REG_MULTIPLE
+	"MULTIPLE",
+#endif
+#ifndef REG_MULTIREF
+	"MULTIREF",
+#endif
+#ifndef REG_MUSTDELIM
+	"MUSTDELIM",
+#endif
+#ifndef REG_NEWLINE
+	"NEWLINE",
+#endif
+#ifndef REG_NOTBOL
+	"NOTBOL",
+#endif
+#ifndef REG_NOTEOL
+	"NOTEOL",
+#endif
+#ifndef REG_NULL
+	"NULL",
+#endif
+#ifndef REG_RIGHT
+	"RIGHT",
+#endif
+#ifndef REG_SHELL_DOT
+	"SHELL_DOT",
+#endif
+#ifndef REG_SHELL_ESCAPED
+	"SHELL_ESCAPED",
+#endif
+#ifndef REG_SHELL_GROUP
+	"SHELL_GROUP",
+#endif
+#ifndef REG_SHELL_PATH
+	"SHELL_PATH",
+#endif
+#ifndef REG_SPAN
+	"SPAN",
+#endif
+#if REG_NOSUB & REG_TEST_DEFAULT
+	"SUBMATCH",
+#endif
+#if !_REG_nexec
+	"regnexec",
+#endif
+#if !_REG_subcomp
+	"regsubcomp",
+#endif
+#if !_REG_decomp
+	"redecomp",
+#endif
+	0
+};
+
+#ifndef REG_CLASS_ESCAPE
+#define REG_CLASS_ESCAPE	NOTEST
+#endif
+#ifndef REG_COMMENT
+#define REG_COMMENT	NOTEST
+#endif
+#ifndef REG_DELIMITED
+#define REG_DELIMITED	NOTEST
+#endif
+#ifndef REG_ESCAPE
+#define REG_ESCAPE	NOTEST
+#endif
+#ifndef REG_ICASE
+#define REG_ICASE	NOTEST
+#endif
+#ifndef REG_LEFT
+#define REG_LEFT	NOTEST
+#endif
+#ifndef REG_LENIENT
+#define REG_LENIENT	0
+#endif
+#ifndef REG_MINIMAL
+#define REG_MINIMAL	NOTEST
+#endif
+#ifndef REG_MULTIPLE
+#define REG_MULTIPLE	NOTEST
+#endif
+#ifndef REG_MULTIREF
+#define REG_MULTIREF	NOTEST
+#endif
+#ifndef REG_MUSTDELIM
+#define REG_MUSTDELIM	NOTEST
+#endif
+#ifndef REG_NEWLINE
+#define REG_NEWLINE	NOTEST
+#endif
+#ifndef REG_NOTBOL
+#define REG_NOTBOL	NOTEST
+#endif
+#ifndef REG_NOTEOL
+#define REG_NOTEOL	NOTEST
+#endif
+#ifndef REG_NULL
+#define REG_NULL	NOTEST
+#endif
+#ifndef REG_RIGHT
+#define REG_RIGHT	NOTEST
+#endif
+#ifndef REG_SHELL_DOT
+#define REG_SHELL_DOT	NOTEST
+#endif
+#ifndef REG_SHELL_ESCAPED
+#define REG_SHELL_ESCAPED	NOTEST
+#endif
+#ifndef REG_SHELL_GROUP
+#define REG_SHELL_GROUP	NOTEST
+#endif
+#ifndef REG_SHELL_PATH
+#define REG_SHELL_PATH	NOTEST
+#endif
+#ifndef REG_SPAN
+#define REG_SPAN	NOTEST
+#endif
+
+#define REG_UNKNOWN	(-1)
+
+#ifndef REG_ENEWLINE
+#define REG_ENEWLINE	(REG_UNKNOWN-1)
+#endif
+#ifndef REG_ENULL
+#ifndef REG_EMPTY
+#define REG_ENULL	(REG_UNKNOWN-2)
+#else
+#define REG_ENULL	REG_EMPTY
+#endif
+#endif
+#ifndef REG_ECOUNT
+#define REG_ECOUNT	(REG_UNKNOWN-3)
+#endif
+#ifndef REG_BADESC
+#define REG_BADESC	(REG_UNKNOWN-4)
+#endif
+#ifndef REG_EMEM
+#define REG_EMEM	(REG_UNKNOWN-5)
+#endif
+#ifndef REG_EHUNG
+#define REG_EHUNG	(REG_UNKNOWN-6)
+#endif
+#ifndef REG_EBUS
+#define REG_EBUS	(REG_UNKNOWN-7)
+#endif
+#ifndef REG_EFAULT
+#define REG_EFAULT	(REG_UNKNOWN-8)
+#endif
+#ifndef REG_EFLAGS
+#define REG_EFLAGS	(REG_UNKNOWN-9)
+#endif
+#ifndef REG_EDELIM
+#define REG_EDELIM	(REG_UNKNOWN-9)
+#endif
+
+static const struct { int code; char* name; } codes[] =
+{
+	REG_UNKNOWN,	"UNKNOWN",
+	REG_NOMATCH,	"NOMATCH",
+	REG_BADPAT,	"BADPAT",
+	REG_ECOLLATE,	"ECOLLATE",
+	REG_ECTYPE,	"ECTYPE",
+	REG_EESCAPE,	"EESCAPE",
+	REG_ESUBREG,	"ESUBREG",
+	REG_EBRACK,	"EBRACK",
+	REG_EPAREN,	"EPAREN",
+	REG_EBRACE,	"EBRACE",
+	REG_BADBR,	"BADBR",
+	REG_ERANGE,	"ERANGE",
+	REG_ESPACE,	"ESPACE",
+	REG_BADRPT,	"BADRPT",
+	REG_ENEWLINE,	"ENEWLINE",
+	REG_ENULL,	"ENULL",
+	REG_ECOUNT,	"ECOUNT",
+	REG_BADESC,	"BADESC",
+	REG_EMEM,	"EMEM",
+	REG_EHUNG,	"EHUNG",
+	REG_EBUS,	"EBUS",
+	REG_EFAULT,	"EFAULT",
+	REG_EFLAGS,	"EFLAGS",
+	REG_EDELIM,	"EDELIM",
+};
+
+static struct
+{
+	regmatch_t	NOMATCH;
+	int		errors;
+	int		extracted;
+	int		ignored;
+	int		lineno;
+	int		passed;
+	int		signals;
+	int		unspecified;
+	int		verify;
+	int		warnings;
+	char*		file;
+	char*		stack;
+	char*		which;
+	jmp_buf		gotcha;
+#ifdef REG_DISCIPLINE
+	Disc_t		disc;
+#endif
+} state;
+
+static void
+quote(char* s, int len, unsigned long test)
+{
+	unsigned char*	u = (unsigned char*)s;
+	unsigned char*	e;
+	int		c;
+#ifdef MB_CUR_MAX
+	int		w;
+#endif
+
+	if (!u)
+		printf("NIL");
+	else if (!*u && len <= 1)
+		printf("NULL");
+	else if (test & TEST_EXPAND)
+	{
+		if (len < 0)
+			len = strlen((char*)u);
+		e = u + len;
+		if (test & TEST_DELIMIT)
+			printf("\"");
+		while (u < e)
+			switch (c = *u++)
+			{
+			case '\\':
+				printf("\\\\");
+				break;
+			case '"':
+				if (test & TEST_DELIMIT)
+					printf("\\\"");
+				else
+					printf("\"");
+				break;
+			case '\a':
+				printf("\\a");
+				break;
+			case '\b':
+				printf("\\b");
+				break;
+			case 033:
+				printf("\\e");
+				break;
+			case '\f':
+				printf("\\f");
+				break;
+			case '\n':
+				printf("\\n");
+				break;
+			case '\r':
+				printf("\\r");
+				break;
+			case '\t':
+				printf("\\t");
+				break;
+			case '\v':
+				printf("\\v");
+				break;
+			default:
+#ifdef MB_CUR_MAX
+				s = (char*)u - 1;
+				if ((w = mblen(s, (char*)e - s)) > 1)
+				{
+					u += w - 1;
+					fwrite(s, 1, w, stdout);
+				}
+				else
+#endif
+				if (!iscntrl(c) && isprint(c))
+					putchar(c);
+				else
+					printf("\\x%02x", c);
+				break;
+			}
+		if (test & TEST_DELIMIT)
+			printf("\"");
+	}
+	else
+		printf("%s", s);
+}
+
+static void
+report(char* comment, char* fun, char* re, char* s, int len, char* msg, int flags, unsigned long test)
+{
+	if (state.file)
+		printf("%s:", state.file);
+	printf("%d:", state.lineno);
+	if (re)
+	{
+		printf(" ");
+		quote(re, -1, test|TEST_DELIMIT);
+		if (s)
+		{
+			printf(" versus ");
+			quote(s, len, test|TEST_DELIMIT);
+		}
+	}
+	if (test & TEST_UNSPECIFIED)
+	{
+		state.unspecified++;
+		printf(" unspecified behavior");
+	}
+	else
+		state.errors++;
+	if (state.which)
+		printf(" %s", state.which);
+	if (flags & REG_NOSUB)
+		printf(" NOSUB");
+	if (fun)
+		printf(" %s", fun);
+	if (comment[strlen(comment)-1] == '\n')
+		printf(" %s", comment);
+	else
+	{
+		printf(" %s: ", comment);
+		if (msg)
+			printf("%s: ", msg);
+	}
+}
+
+static void
+error(regex_t* preg, int code)
+{
+	char*	msg;
+	char	buf[256];
+
+	switch (code)
+	{
+	case REG_EBUS:
+		msg = "bus error";
+		break;
+	case REG_EFAULT:
+		msg = "memory fault";
+		break;
+	case REG_EHUNG:
+		msg = "did not terminate";
+		break;
+	default:
+		regerror(code, preg, msg = buf, sizeof buf);
+		break;
+	}
+	printf("%s\n", msg);
+}
+
+static void
+bad(char* comment, char* re, char* s, int len, unsigned long test)
+{
+	printf("bad test case ");
+	report(comment, NiL, re, s, len, NiL, 0, test);
+	exit(1);
+}
+
+static int
+escape(char* s)
+{
+	char*	b;
+	char*	t;
+	char*	q;
+	char*	e;
+	int	c;
+
+	for (b = t = s; *t = *s; s++, t++)
+		if (*s == '\\')
+			switch (*++s)
+			{
+			case '\\':
+				break;
+			case 'a':
+				*t = '\a';
+				break;
+			case 'b':
+				*t = '\b';
+				break;
+			case 'c':
+				if (*t = *++s)
+					*t &= 037;
+				else
+					s--;
+				break;
+			case 'e':
+			case 'E':
+				*t = 033;
+				break;
+			case 'f':
+				*t = '\f';
+				break;
+			case 'n':
+				*t = '\n';
+				break;
+			case 'r':
+				*t = '\r';
+				break;
+			case 's':
+				*t = ' ';
+				break;
+			case 't':
+				*t = '\t';
+				break;
+			case 'v':
+				*t = '\v';
+				break;
+			case 'u':
+			case 'x':
+				c = 0;
+				q = c == 'u' ? (s + 5) : (char*)0;
+				e = s + 1;
+				while (!e || !q || s < q)
+				{
+					switch (*++s)
+					{
+					case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+						c = (c << 4) + *s - 'a' + 10;
+						continue;
+					case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+						c = (c << 4) + *s - 'A' + 10;
+						continue;
+					case '0': case '1': case '2': case '3': case '4':
+					case '5': case '6': case '7': case '8': case '9':
+						c = (c << 4) + *s - '0';
+						continue;
+					case '{':
+					case '[':
+						if (s != e)
+						{
+							s--;
+							break;
+						}
+						e = 0;
+						continue;
+					case '}':
+					case ']':
+						if (e)
+							s--;
+						break;
+					default:
+						s--;
+						break;
+					}
+					break;
+				}
+				*t = c;
+				break;
+			case '0': case '1': case '2': case '3':
+			case '4': case '5': case '6': case '7':
+				c = *s - '0';
+				q = s + 2;
+				while (s < q)
+				{
+					switch (*++s)
+					{
+					case '0': case '1': case '2': case '3':
+					case '4': case '5': case '6': case '7':
+						c = (c << 3) + *s - '0';
+						break;
+					default:
+						q = --s;
+						break;
+					}
+				}
+				*t = c;
+				break;
+			default:
+				*(s + 1) = 0;
+				bad("invalid C \\ escape\n", s - 1, NiL, 0, 0);
+			}
+	return t - b;
+}
+
+static void
+matchoffprint(int off)
+{
+	switch (off)
+	{
+	case -2:
+		printf("X");
+		break;
+	case -1:
+		printf("?");
+		break;
+	default:
+		printf("%d", off);
+		break;
+	}
+}
+
+static void
+matchprint(regmatch_t* match, int nmatch, int nsub, char* ans, unsigned long test)
+{
+	int	i;
+
+	for (; nmatch > nsub + 1; nmatch--)
+		if ((match[nmatch-1].rm_so != -1 || match[nmatch-1].rm_eo != -1) && (!(test & TEST_IGNORE_POSITION) || match[nmatch-1].rm_so >= 0 && match[nmatch-1].rm_eo >= 0))
+			break;
+	for (i = 0; i < nmatch; i++)
+	{
+		printf("(");
+		matchoffprint(match[i].rm_so);
+		printf(",");
+		matchoffprint(match[i].rm_eo);
+		printf(")");
+	}
+	if (!(test & (TEST_ACTUAL|TEST_BASELINE)))
+	{
+		if (ans)
+			printf(" expected: %s", ans);
+		printf("\n");
+	}
+}
+
+static int
+matchcheck(regmatch_t* match, int nmatch, int nsub, char* ans, char* re, char* s, int len, int flags, unsigned long test)
+{
+	char*	p;
+	int	i;
+	int	m;
+	int	n;
+
+	if (streq(ans, "OK"))
+		return test & (TEST_BASELINE|TEST_PASS|TEST_VERIFY);
+	for (i = 0, p = ans; i < nmatch && *p; i++)
+	{
+		if (*p == '{')
+		{
+#ifdef REG_DISCIPLINE
+			char*	x;
+
+			if (!(x = sfstruse(state.disc.sp)))
+				bad("out of space [discipline string]\n", NiL, NiL, 0, 0);
+			if (strcmp(p, x))
+			{
+				if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+					return 0;
+				report("callout failed", NiL, re, s, len, NiL, flags, test);
+				quote(p, -1, test);
+				printf(" expected, ");
+				quote(x, -1, test);
+				printf(" returned\n");
+			}
+#endif
+			break;
+		}
+		if (*p++ != '(')
+			bad("improper answer\n", re, s, -1, test);
+		if (*p == '?')
+		{
+			m = -1;
+			p++;
+		}
+		else if (*p == 'R' && !memcmp(p, "RE_DUP_MAX", 10))
+		{
+			m = RE_DUP_MAX;
+			p += 10;
+			if (*p == '+' || *p == '-')
+				m += strtol(p, &p, 10);
+		}
+		else
+			m = strtol(p, &p, 10);
+		if (*p++ != ',')
+			bad("improper answer\n", re, s, -1, test);
+		if (*p == '?')
+		{
+			n = -1;
+			p++;
+		}
+		else if (*p == 'R' && !memcmp(p, "RE_DUP_MAX", 10))
+		{
+			n = RE_DUP_MAX;
+			p += 10;
+			if (*p == '+' || *p == '-')
+				n += strtol(p, &p, 10);
+		}
+		else
+			n = strtol(p, &p, 10);
+		if (*p++ != ')')
+			bad("improper answer\n", re, s, -1, test);
+		if (m!=match[i].rm_so || n!=match[i].rm_eo)
+		{
+			if (!(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY)))
+			{
+				report("failed: match was", NiL, re, s, len, NiL, flags, test);
+				matchprint(match, nmatch, nsub, ans, test);
+			}
+			return 0;
+		}
+	}
+	for (; i < nmatch; i++)
+	{
+		if (match[i].rm_so!=-1 || match[i].rm_eo!=-1)
+		{
+			if (!(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_VERIFY)))
+			{
+				if ((test & TEST_IGNORE_POSITION) && (match[i].rm_so<0 || match[i].rm_eo<0))
+				{
+					state.ignored++;
+					return 0;
+				}
+				if (!(test & TEST_SUMMARY))
+				{
+					report("failed: match was", NiL, re, s, len, NiL, flags, test);
+					matchprint(match, nmatch, nsub, ans, test);
+				}
+			}
+			return 0;
+		}
+	}
+	if (!(test & TEST_IGNORE_OVER) && match[nmatch].rm_so != state.NOMATCH.rm_so)
+	{
+		if (!(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY)))
+		{
+			report("failed: overran match array", NiL, re, s, len, NiL, flags, test);
+			matchprint(match, nmatch + 1, nsub, NiL, test);
+		}
+		return 0;
+	}
+	return 1;
+}
+
+static void
+sigunblock(int s)
+{
+#ifdef SIG_SETMASK
+	int		op;
+	sigset_t	mask;
+
+	sigemptyset(&mask);
+	if (s)
+	{
+		sigaddset(&mask, s);
+		op = SIG_UNBLOCK;
+	}
+	else op = SIG_SETMASK;
+	sigprocmask(op, &mask, NiL);
+#else
+#ifdef sigmask
+	sigsetmask(s ? (sigsetmask(0L) & ~sigmask(s)) : 0L);
+#endif
+#endif
+}
+
+static void
+gotcha(int sig)
+{
+	int	ret;
+
+	signal(sig, gotcha);
+	alarm(0);
+	state.signals++;
+	switch (sig)
+	{
+	case SIGALRM:
+		ret = REG_EHUNG;
+		break;
+	case SIGBUS:
+		ret = REG_EBUS;
+		break;
+	default:
+		ret = REG_EFAULT;
+		break;
+	}
+	sigunblock(sig);
+	longjmp(state.gotcha, ret);
+}
+
+static char*
+getline(FILE* fp)
+{
+	static char	buf[32 * 1024];
+
+	register char*	s = buf;
+	register char*	e = &buf[sizeof(buf)];
+	register char*	b;
+
+	for (;;)
+	{
+		if (!(b = fgets(s, e - s, fp)))
+			return 0;
+		state.lineno++;
+		s += strlen(s);
+		if (s == b || *--s != '\n' || s == b || *(s - 1) != '\\')
+		{
+			*s = 0;
+			break;
+		}
+		s--;
+	}
+	return buf;
+}
+
+static unsigned long
+note(unsigned long level, char* msg, unsigned long skip, unsigned long test)
+{
+	if (!(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_SUMMARY)) && !skip)
+	{
+		printf("NOTE\t");
+		if (msg)
+			printf("%s: ", msg);
+		printf("skipping lines %d", state.lineno);
+	}
+	return skip | level;
+}
+
+#define TABS(n)		&ts[7-((n)&7)]
+
+static char		ts[] = "\t\t\t\t\t\t\t";
+
+static unsigned long
+extract(int* tabs, char* spec, char* re, char* s, char* ans, char* msg, char* accept, regmatch_t* match, int nmatch, int nsub, unsigned long skip, unsigned long level, unsigned long test)
+{
+	if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_OK|TEST_PASS|TEST_SUMMARY))
+	{
+		state.extracted = 1;
+		if (test & TEST_OK)
+		{
+			state.passed++;
+			if ((test & TEST_VERIFY) && !(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_SUMMARY)))
+			{
+				if (msg && strcmp(msg, "EXPECTED"))
+					printf("NOTE\t%s\n", msg);
+				return skip;
+			}
+			test &= ~(TEST_PASS|TEST_QUERY);
+		}
+		if (test & (TEST_QUERY|TEST_VERIFY))
+		{
+			if (test & TEST_BASELINE)
+				test &= ~(TEST_BASELINE|TEST_PASS);
+			else
+				test |= TEST_PASS;
+			skip |= level;
+		}
+		if (!(test & TEST_OK))
+		{
+			if (test & TEST_UNSPECIFIED)
+				state.unspecified++;
+			else
+				state.errors++;
+		}
+		if (test & (TEST_PASS|TEST_SUMMARY))
+			return skip;
+		test &= ~TEST_DELIMIT;
+		printf("%s%s", spec, TABS(*tabs++));
+		if ((test & (TEST_BASELINE|TEST_SAME)) == (TEST_BASELINE|TEST_SAME))
+			printf("SAME");
+		else
+			quote(re, -1, test);
+		printf("%s", TABS(*tabs++));
+		quote(s, -1, test);
+		printf("%s", TABS(*tabs++));
+		if (!(test & (TEST_ACTUAL|TEST_BASELINE)) || !accept && !match)
+			printf("%s", ans);
+		else if (accept)
+			printf("%s", accept);
+		else
+			matchprint(match, nmatch, nsub, NiL, test);
+		if (msg)
+			printf("%s%s", TABS(*tabs++), msg);
+		putchar('\n');
+	}
+	else if (test & TEST_QUERY)
+		skip = note(level, msg, skip, test);
+	else if (test & TEST_VERIFY)
+		state.extracted = 1;
+	return skip;
+}
+
+static int
+catchfree(regex_t* preg, int flags, int* tabs, char* spec, char* re, char* s, char* ans, char* msg, char* accept, regmatch_t* match, int nmatch, int nsub, unsigned long skip, unsigned long level, unsigned long test)
+{
+	int	eret;
+
+	if (!(test & TEST_CATCH))
+	{
+		regfree(preg);
+		eret = 0;
+	}
+	else if (!(eret = setjmp(state.gotcha)))
+	{
+		alarm(HUNG);
+		regfree(preg);
+		alarm(0);
+	}
+	else if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+		extract(tabs, spec, re, s, ans, msg, NiL, NiL, 0, 0, skip, level, test);
+	else
+	{
+		report("failed", "regfree", re, NiL, -1, msg, flags, test);
+		error(preg, eret);
+	}
+	return eret;
+}
+
+static char*
+expand(char* os, char* ot)
+{
+	char*	s = os;
+	char*	t;
+	int	n = 0;
+	int	r;
+	long	m;
+
+	for (;;)
+	{
+		switch (*s++)
+		{
+		case 0:
+			break;
+		case '{':
+			n++;
+			continue;
+		case '}':
+			n--;
+			continue;
+		case 'R':
+			if (n == 1 && !memcmp(s, "E_DUP_MAX", 9))
+			{
+				s--;
+				for (t = ot; os < s; *t++ = *os++);
+				r = ((t - ot) >= 5 && t[-1] == '{' && t[-2] == '.' && t[-3] == '.' && t[-4] == '.') ? t[-5] : 0;
+				os = ot;
+				m = RE_DUP_MAX;
+				if (*(s += 10) == '+' || *s == '-')
+					m += strtol(s, &s, 10);
+				if (r)
+				{
+					t -= 5;
+					while (m-- > 0)
+						*t++ = r;
+					while (*s && *s++ != '}');
+				}
+				else
+					t += snprintf(t, 32, "%ld", m);
+				while (*t = *s++)
+					t++;
+				break;
+			}
+			continue;
+		default:
+			continue;
+		}
+		break;
+	}
+	return os;
+}
+
+int
+main(int argc, char** argv)
+{
+	int		flags;
+	int		cflags;
+	int		eflags;
+	int		nmatch;
+	int		nexec;
+	int		nstr;
+	int		cret;
+	int		eret;
+	int		nsub;
+	int		i;
+	int		j;
+	int		expected;
+	int		got;
+	int		locale;
+	int		subunitlen;
+	int		testno;
+	unsigned long	level;
+	unsigned long	skip;
+	char*		p;
+	char*		line;
+	char*		spec;
+	char*		re;
+	char*		s;
+	char*		ans;
+	char*		msg;
+	char*		fun;
+	char*		ppat;
+	char*		subunit;
+	char*		version;
+	char*		field[6];
+	char*		delim[6];
+	FILE*		fp;
+	int		tabs[6];
+	char		unit[64];
+	regmatch_t	match[100];
+	regex_t		preg;
+
+	static char	pat[32 * 1024];
+	static char	patbuf[32 * 1024];
+	static char	strbuf[32 * 1024];
+
+	int		nonosub = REG_NOSUB == 0;
+	int		nonexec = 0;
+
+	unsigned long	test = 0;
+
+	static char*	filter[] = { "-", 0 };
+
+	state.NOMATCH.rm_so = state.NOMATCH.rm_eo = -2;
+	p = unit;
+	version = (char*)id + 10;
+	while (p < &unit[sizeof(unit)-1] && (*p = *version++) && !isspace(*p))
+		p++;
+	*p = 0;
+	while ((p = *++argv) && *p == '-')
+		for (;;)
+		{
+			switch (*++p)
+			{
+			case 0:
+				break;
+			case 'c':
+				test |= TEST_CATCH;
+				continue;
+			case 'e':
+				test |= TEST_IGNORE_ERROR;
+				continue;
+			case 'h':
+			case '?':
+				help(0);
+				return 2;
+			case '-':
+				help(p[1] == 'h');
+				return 2;
+			case 'n':
+				nonexec = 1;
+				continue;
+			case 'o':
+				test |= TEST_IGNORE_OVER;
+				continue;
+			case 'p':
+				test |= TEST_IGNORE_POSITION;
+				continue;
+			case 's':
+#ifdef REG_DISCIPLINE
+				if (!(state.stack = stkalloc(stkstd, 0)))
+					fprintf(stderr, "%s: out of space [stack]", unit);
+				state.disc.disc.re_resizef = resizef;
+				state.disc.disc.re_resizehandle = (void*)stkstd;
+#endif
+				continue;
+			case 'x':
+				nonosub = 1;
+				continue;
+			case 'v':
+				test |= TEST_VERBOSE;
+				continue;
+			case 'A':
+				test |= TEST_ACTUAL;
+				continue;
+			case 'B':
+				test |= TEST_BASELINE;
+				continue;
+			case 'F':
+				test |= TEST_FAIL;
+				continue;
+			case 'P':
+				test |= TEST_PASS;
+				continue;
+			case 'S':
+				test |= TEST_SUMMARY;
+				continue;
+			default:
+				fprintf(stderr, "%s: %c: invalid option\n", unit, *p);
+				return 2;
+			}
+			break;
+		}
+	if (!*argv)
+		argv = filter;
+	locale = 0;
+	while (state.file = *argv++)
+	{
+		if (streq(state.file, "-") || streq(state.file, "/dev/stdin") || streq(state.file, "/dev/fd/0"))
+		{
+			state.file = 0;
+			fp = stdin;
+		}
+		else if (!(fp = fopen(state.file, "r")))
+		{
+			fprintf(stderr, "%s: %s: cannot read\n", unit, state.file);
+			return 2;
+		}
+		testno = state.errors = state.ignored = state.lineno = state.passed =
+		state.signals = state.unspecified = state.warnings = 0;
+		skip = 0;
+		level = 1;
+		if (!(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_SUMMARY)))
+		{
+			printf("TEST\t%s ", unit);
+			if (s = state.file)
+			{
+				subunit = p = 0;
+				for (;;)
+				{
+					switch (*s++)
+					{
+					case 0:
+						break;
+					case '/':
+						subunit = s;
+						continue;
+					case '.':
+						p = s - 1;
+						continue;
+					default:
+						continue;
+					}
+					break;
+				}
+				if (!subunit)
+					subunit = state.file;
+				if (p < subunit)
+					p = s - 1;
+				subunitlen = p - subunit;
+				printf("%-.*s ", subunitlen, subunit);
+			}
+			else
+				subunit = 0;
+			for (s = version; *s && (*s != ' ' || *(s + 1) != '$'); s++)
+				putchar(*s);
+			if (test & TEST_CATCH)
+				printf(", catch");
+			if (test & TEST_IGNORE_ERROR)
+				printf(", ignore error code mismatches");
+			if (test & TEST_IGNORE_POSITION)
+				printf(", ignore negative position mismatches");
+#ifdef REG_DISCIPLINE
+			if (state.stack)
+				printf(", stack");
+#endif
+			if (test & TEST_VERBOSE)
+				printf(", verbose");
+			printf("\n");
+#ifdef REG_VERSIONID
+			if (regerror(REG_VERSIONID, NiL, pat, sizeof(pat)) > 0)
+				s = pat;
+			else
+#endif
+#ifdef REG_TEST_VERSION
+			s = REG_TEST_VERSION;
+#else
+			s = "regex";
+#endif
+			printf("NOTE\t%s\n", s);
+			if (elementsof(unsupported) > 1)
+			{
+#if (REG_TEST_DEFAULT & (REG_AUGMENTED|REG_EXTENDED|REG_SHELL)) || !defined(REG_EXTENDED)
+				i = 0;
+#else
+				i = REG_EXTENDED != 0;
+#endif
+				for (got = 0; i < elementsof(unsupported) - 1; i++)
+				{
+					if (!got)
+					{
+						got = 1;
+						printf("NOTE\tunsupported: %s", unsupported[i]);
+					}
+					else
+						printf(",%s", unsupported[i]);
+				}
+				if (got)
+					printf("\n");
+			}
+		}
+#ifdef REG_DISCIPLINE
+		state.disc.disc.re_version = REG_VERSION;
+		state.disc.disc.re_compf = compf;
+		state.disc.disc.re_execf = execf;
+		if (!(state.disc.sp = sfstropen()))
+			bad("out of space [discipline string stream]\n", NiL, NiL, 0, 0);
+		preg.re_disc = &state.disc.disc;
+#endif
+		if (test & TEST_CATCH)
+		{
+			signal(SIGALRM, gotcha);
+			signal(SIGBUS, gotcha);
+			signal(SIGSEGV, gotcha);
+		}
+		while (p = getline(fp))
+		{
+
+		/* parse: */
+
+			line = p;
+			if (*p == ':' && !isspace(*(p + 1)))
+			{
+				while (*++p && *p != ':');
+				if (!*p++)
+				{
+					if (test & TEST_BASELINE)
+						printf("%s\n", line);
+					continue;
+				}
+			}
+			while (isspace(*p))
+				p++;
+			if (*p == 0 || *p == '#' || *p == 'T')
+			{
+				if (test & TEST_BASELINE)
+					printf("%s\n", line);
+				continue;
+			}
+			if (*p == ':' || *p == 'N')
+			{
+				if (test & TEST_BASELINE)
+					printf("%s\n", line);
+				else if (!(test & (TEST_ACTUAL|TEST_FAIL|TEST_PASS|TEST_SUMMARY)))
+				{
+					while (*++p && !isspace(*p));
+					while (isspace(*p))
+						p++;
+					printf("NOTE	%s\n", p);
+				}
+				continue;
+			}
+			j = 0;
+			i = 0;
+			field[i++] = p;
+			for (;;)
+			{
+				switch (*p++)
+				{
+				case 0:
+					p--;
+					j = 0;
+					goto checkfield;
+				case '\t':
+					*(delim[i] = p - 1) = 0;
+					j = 1;
+				checkfield:
+					s = field[i - 1];
+					if (streq(s, "NIL"))
+						field[i - 1] = 0;
+					else if (streq(s, "NULL"))
+						*s = 0;
+					while (*p == '\t')
+					{
+						p++;
+						j++;
+					}
+					tabs[i - 1] = j;
+					if (!*p)
+						break;
+					if (i >= elementsof(field))
+						bad("too many fields\n", NiL, NiL, 0, 0);
+					field[i++] = p;
+					/*FALLTHROUGH*/
+				default:
+					continue;
+				}
+				break;
+			}
+			if (!(spec = field[0]))
+				bad("NIL spec\n", NiL, NiL, 0, 0);
+
+		/* interpret: */
+
+			cflags = REG_TEST_DEFAULT;
+			eflags = REG_EXEC_DEFAULT;
+			test &= TEST_GLOBAL;
+			state.extracted = 0;
+			nmatch = 20;
+			nsub = -1;
+			for (p = spec; *p; p++)
+			{
+				if (isdigit(*p))
+				{
+					nmatch = strtol(p, &p, 10);
+					if (nmatch >= elementsof(match))
+						bad("nmatch must be < 100\n", NiL, NiL, 0, 0);
+					p--;
+					continue;
+				}
+				switch (*p)
+				{
+				case 'A':
+					test |= TEST_ARE;
+					continue;
+				case 'B':
+					test |= TEST_BRE;
+					continue;
+				case 'C':
+					if (!(test & TEST_QUERY) && !(skip & level))
+						bad("locale must be nested\n", NiL, NiL, 0, 0);
+					test &= ~TEST_QUERY;
+					if (locale)
+						bad("locale nesting not supported\n", NiL, NiL, 0, 0);
+					if (i != 2)
+						bad("locale field expected\n", NiL, NiL, 0, 0);
+					if (!(skip & level))
+					{
+#if defined(LC_COLLATE) && defined(LC_CTYPE)
+						s = field[1];
+						if (!s || streq(s, "POSIX"))
+							s = "C";
+						if ((ans = setlocale(LC_COLLATE, s)) && streq(ans, "POSIX"))
+							ans = "C";
+						if (!ans || !streq(ans, s) && streq(s, "C"))
+							ans = 0;
+						else if ((ans = setlocale(LC_CTYPE, s)) && streq(ans, "POSIX"))
+							ans = "C";
+						if (!ans || !streq(ans, s) && streq(s, "C"))
+							skip = note(level, s, skip, test);
+						else
+						{
+							if (!(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_SUMMARY)))
+								printf("NOTE	\"%s\" locale\n", s);
+							locale = level;
+						}
+#else
+						skip = note(level, skip, test, "locales not supported");
+#endif
+					}
+					cflags = NOTEST;
+					continue;
+				case 'E':
+					test |= TEST_ERE;
+					continue;
+				case 'K':
+					test |= TEST_KRE;
+					continue;
+				case 'L':
+					test |= TEST_LRE;
+					continue;
+				case 'S':
+					test |= TEST_SRE;
+					continue;
+
+				case 'a':
+					cflags |= REG_LEFT|REG_RIGHT;
+					continue;
+				case 'b':
+					eflags |= REG_NOTBOL;
+					continue;
+				case 'c':
+					cflags |= REG_COMMENT;
+					continue;
+				case 'd':
+					cflags |= REG_SHELL_DOT;
+					continue;
+				case 'e':
+					eflags |= REG_NOTEOL;
+					continue;
+				case 'f':
+					cflags |= REG_MULTIPLE;
+					continue;
+				case 'g':
+					cflags |= NOTEST;
+					continue;
+				case 'h':
+					cflags |= REG_MULTIREF;
+					continue;
+				case 'i':
+					cflags |= REG_ICASE;
+					continue;
+				case 'j':
+					cflags |= REG_SPAN;
+					continue;
+				case 'k':
+					cflags |= REG_ESCAPE;
+					continue;
+				case 'l':
+					cflags |= REG_LEFT;
+					continue;
+				case 'm':
+					cflags |= REG_MINIMAL;
+					continue;
+				case 'n':
+					cflags |= REG_NEWLINE;
+					continue;
+				case 'o':
+					cflags |= REG_SHELL_GROUP;
+					continue;
+				case 'p':
+					cflags |= REG_SHELL_PATH;
+					continue;
+				case 'q':
+					cflags |= REG_DELIMITED;
+					continue;
+				case 'r':
+					cflags |= REG_RIGHT;
+					continue;
+				case 's':
+					cflags |= REG_SHELL_ESCAPED;
+					continue;
+				case 't':
+					cflags |= REG_MUSTDELIM;
+					continue;
+				case 'u':
+					test |= TEST_UNSPECIFIED;
+					continue;
+				case 'v':
+					cflags |= REG_CLASS_ESCAPE;
+					continue;
+				case 'w':
+					cflags |= REG_NOSUB;
+					continue;
+				case 'x':
+					if (REG_LENIENT)
+						cflags |= REG_LENIENT;
+					else
+						test |= TEST_LENIENT;
+					continue;
+				case 'y':
+					eflags |= REG_LEFT;
+					continue;
+				case 'z':
+					cflags |= REG_NULL;
+					continue;
+
+				case '$':
+					test |= TEST_EXPAND;
+					continue;
+
+				case '/':
+					test |= TEST_SUB;
+					continue;
+
+				case '=':
+					test |= TEST_DECOMP;
+					continue;
+
+				case '?':
+					test |= TEST_VERIFY;
+					test &= ~(TEST_AND|TEST_OR);
+					state.verify = state.passed;
+					continue;
+				case '&':
+					test |= TEST_VERIFY|TEST_AND;
+					test &= ~TEST_OR;
+					continue;
+				case '|':
+					test |= TEST_VERIFY|TEST_OR;
+					test &= ~TEST_AND;
+					continue;
+				case ';':
+					test |= TEST_OR;
+					test &= ~TEST_AND;
+					continue;
+
+				case '{':
+					level <<= 1;
+					if (skip & (level >> 1))
+					{
+						skip |= level;
+						cflags = NOTEST;
+					}
+					else
+					{
+						skip &= ~level;
+						test |= TEST_QUERY;
+					}
+					continue;
+				case '}':
+					if (level == 1)
+						bad("invalid {...} nesting\n", NiL, NiL, 0, 0);
+					if ((skip & level) && !(skip & (level>>1)))
+					{
+						if (!(test & (TEST_BASELINE|TEST_SUMMARY)))
+						{
+							if (test & (TEST_ACTUAL|TEST_FAIL))
+								printf("}\n");
+							else if (!(test & TEST_PASS))
+								printf("-%d\n", state.lineno);
+						}
+					}
+#if defined(LC_COLLATE) && defined(LC_CTYPE)
+					else if (locale & level)
+					{
+						locale = 0;
+						if (!(skip & level))
+						{
+							s = "C";
+							setlocale(LC_COLLATE, s);
+							setlocale(LC_CTYPE, s);
+							if (!(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_SUMMARY)))
+								printf("NOTE	\"%s\" locale\n", s);
+							else if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_PASS))
+								printf("}\n");
+						}
+						else if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL))
+							printf("}\n");
+					}
+#endif
+					level >>= 1;
+					cflags = NOTEST;
+					continue;
+
+				default:
+					bad("bad spec\n", spec, NiL, 0, test);
+					break;
+
+				}
+				break;
+			}
+			if ((cflags|eflags) == NOTEST || (skip & level) && (test & TEST_BASELINE))
+			{
+				if (test & TEST_BASELINE)
+				{
+					while (i > 1)
+						*delim[--i] = '\t';
+					printf("%s\n", line);
+				}
+				continue;
+			}
+			if (test & TEST_OR)
+			{
+				if (!(test & TEST_VERIFY))
+				{
+					test &= ~TEST_OR;
+					if (state.passed == state.verify && i > 1)
+						printf("NOTE\t%s\n", field[1]);
+					continue;
+				}
+				else if (state.passed > state.verify)
+					continue;
+			}
+			else if (test & TEST_AND)
+			{
+				if (state.passed == state.verify)
+					continue;
+				state.passed = state.verify;
+			}
+			if (i < ((test & TEST_DECOMP) ? 3 : 4))
+				bad("too few fields\n", NiL, NiL, 0, test);
+			while (i < elementsof(field))
+				field[i++] = 0;
+			if (re = field[1])
+			{
+				if (streq(re, "SAME"))
+				{
+					re = ppat;
+					test |= TEST_SAME;
+				}
+				else
+				{
+					if (test & TEST_EXPAND)
+						escape(re);
+					re = expand(re, patbuf);
+					strcpy(ppat = pat, re);
+				}
+			}
+			else
+				ppat = 0;
+			nstr = -1;
+			if (s = field[2])
+			{
+				s = expand(s, strbuf);
+				if (test & TEST_EXPAND)
+				{
+					nstr = escape(s);
+#if _REG_nexec
+					if (nstr != strlen(s))
+						nexec = nstr;
+#endif
+				}
+			}
+			if (!(ans = field[(test & TEST_DECOMP) ? 2 : 3]))
+				bad("NIL answer\n", NiL, NiL, 0, test);
+			msg = field[4];
+			fflush(stdout);
+			if (test & TEST_SUB)
+#if _REG_subcomp
+				cflags |= REG_DELIMITED;
+#else
+				continue;
+#endif
+#if !_REG_decomp
+			if (test & TEST_DECOMP)
+				continue;
+#endif
+
+		compile:
+
+			if (state.extracted || (skip & level))
+				continue;
+#if !(REG_TEST_DEFAULT & (REG_AUGMENTED|REG_EXTENDED|REG_SHELL))
+#ifdef REG_EXTENDED
+			if (REG_EXTENDED != 0 && (test & TEST_BRE))
+#else
+			if (test & TEST_BRE)
+#endif
+			{
+				test &= ~TEST_BRE;
+				flags = cflags;
+				state.which = "BRE";
+			}
+			else
+#endif
+#ifdef REG_EXTENDED
+			if (test & TEST_ERE)
+			{
+				test &= ~TEST_ERE;
+				flags = cflags | REG_EXTENDED;
+				state.which = "ERE";
+			}
+			else
+#endif
+#ifdef REG_AUGMENTED
+			if (test & TEST_ARE)
+			{
+				test &= ~TEST_ARE;
+				flags = cflags | REG_AUGMENTED;
+				state.which = "ARE";
+			}
+			else
+#endif
+#ifdef REG_LITERAL
+			if (test & TEST_LRE)
+			{
+				test &= ~TEST_LRE;
+				flags = cflags | REG_LITERAL;
+				state.which = "LRE";
+			}
+			else
+#endif
+#ifdef REG_SHELL
+			if (test & TEST_SRE)
+			{
+				test &= ~TEST_SRE;
+				flags = cflags | REG_SHELL;
+				state.which = "SRE";
+			}
+			else
+#ifdef REG_AUGMENTED
+			if (test & TEST_KRE)
+			{
+				test &= ~TEST_KRE;
+				flags = cflags | REG_SHELL | REG_AUGMENTED;
+				state.which = "KRE";
+			}
+			else
+#endif
+#endif
+			{
+				if (test & (TEST_BASELINE|TEST_PASS|TEST_VERIFY))
+					extract(tabs, line, re, s, ans, msg, NiL, NiL, 0, 0, skip, level, test|TEST_OK);
+				continue;
+			}
+			if ((test & (TEST_QUERY|TEST_VERBOSE|TEST_VERIFY)) == TEST_VERBOSE)
+			{
+				printf("test %-3d %s ", state.lineno, state.which);
+				quote(re, -1, test|TEST_DELIMIT);
+				printf(" ");
+				quote(s, nstr, test|TEST_DELIMIT);
+				printf("\n");
+			}
+
+		nosub:
+			fun = "regcomp";
+#if _REG_nexec
+			if (nstr >= 0 && nstr != strlen(s))
+				nexec = nstr;
+
+			else
+#endif
+				nexec = -1;
+			if (state.extracted || (skip & level))
+				continue;
+			if (!(test & TEST_QUERY))
+				testno++;
+#ifdef REG_DISCIPLINE
+			if (state.stack)
+				stkset(stkstd, state.stack, 0);
+			flags |= REG_DISCIPLINE;
+			state.disc.ordinal = 0;
+			sfstrseek(state.disc.sp, 0, SEEK_SET);
+#endif
+			if (!(test & TEST_CATCH))
+				cret = regcomp(&preg, re, flags);
+			else if (!(cret = setjmp(state.gotcha)))
+			{
+				alarm(HUNG);
+				cret = regcomp(&preg, re, flags);
+				alarm(0);
+			}
+#if _REG_subcomp
+			if (!cret && (test & TEST_SUB))
+			{
+				fun = "regsubcomp";
+				p = re + preg.re_npat;
+				if (!(test & TEST_CATCH))
+					cret = regsubcomp(&preg, p, NiL, 0, 0);
+				else if (!(cret = setjmp(state.gotcha)))
+				{
+					alarm(HUNG);
+					cret = regsubcomp(&preg, p, NiL, 0, 0);
+					alarm(0);
+				}
+				if (!cret && *(p += preg.re_npat) && !(preg.re_sub->re_flags & REG_SUB_LAST))
+				{
+					if (catchfree(&preg, flags, tabs, line, re, s, ans, msg, NiL, NiL, 0, 0, skip, level, test))
+						continue;
+					cret = REG_EFLAGS;
+				}
+			}
+#endif
+#if _REG_decomp
+			if (!cret && (test & TEST_DECOMP))
+			{
+				char	buf[128];
+
+				if ((j = nmatch) > sizeof(buf))
+					j = sizeof(buf);
+				fun = "regdecomp";
+				p = re + preg.re_npat;
+				if (!(test & TEST_CATCH))
+					i = regdecomp(&preg, -1, buf, j);
+				else if (!(cret = setjmp(state.gotcha)))
+				{
+					alarm(HUNG);
+					i = regdecomp(&preg, -1, buf, j);
+					alarm(0);
+				}
+				if (!cret)
+				{
+					catchfree(&preg, flags, tabs, line, re, s, ans, msg, NiL, NiL, 0, 0, skip, level, test);
+					if (i > j)
+					{
+						if (i != (strlen(ans) + 1))
+						{
+							report("failed", fun, re, s, nstr, msg, flags, test);
+							printf(" %d byte buffer supplied, %d byte buffer required\n", j, i);
+						}
+					}
+					else if (strcmp(buf, ans))
+					{
+						report("failed", fun, re, s, nstr, msg, flags, test);
+						quote(ans, -1, test|TEST_DELIMIT);
+						printf(" expected, ");
+						quote(buf, -1, test|TEST_DELIMIT);
+						printf(" returned\n");
+					}
+					continue;
+				}
+			}
+#endif
+			if (!cret)
+			{
+				if (!(flags & REG_NOSUB) && nsub < 0 && *ans == '(')
+				{
+					for (p = ans; *p; p++)
+						if (*p == '(')
+							nsub++;
+						else if (*p == '{')
+							nsub--;
+					if (nsub >= 0)
+					{
+						if (test & TEST_IGNORE_OVER)
+						{
+							if (nmatch > nsub)
+								nmatch = nsub + 1;
+						}
+						else if (nsub != preg.re_nsub)
+						{
+							if (nsub > preg.re_nsub)
+							{
+								if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+									skip = extract(tabs, line, re, s, ans, msg, "OK", NiL, 0, 0, skip, level, test|TEST_DELIMIT);
+								else
+								{
+									report("re_nsub incorrect", fun, re, NiL, -1, msg, flags, test);
+									printf("at least %d expected, %d returned\n", nsub, preg.re_nsub);
+									state.errors++;
+								}
+							}
+							else
+								nsub = preg.re_nsub;
+						}
+					}
+				}
+				if (!(test & (TEST_DECOMP|TEST_SUB)) && *ans && *ans != '(' && !streq(ans, "OK") && !streq(ans, "NOMATCH"))
+				{
+					if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+						skip = extract(tabs, line, re, s, ans, msg, "OK", NiL, 0, 0, skip, level, test|TEST_DELIMIT);
+					else if (!(test & TEST_LENIENT))
+					{
+						report("failed", fun, re, NiL, -1, msg, flags, test);
+						printf("%s expected, OK returned\n", ans);
+					}
+					catchfree(&preg, flags, tabs, line, re, s, ans, msg, NiL, NiL, 0, 0, skip, level, test);
+					continue;
+				}
+			}
+			else
+			{
+				if (test & TEST_LENIENT)
+					/* we'll let it go this time */;
+				else if (!*ans || ans[0]=='(' || cret == REG_BADPAT && streq(ans, "NOMATCH"))
+				{
+					got = 0;
+					for (i = 1; i < elementsof(codes); i++)
+						if (cret==codes[i].code)
+							got = i;
+					if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+						skip = extract(tabs, line, re, s, ans, msg, codes[got].name, NiL, 0, 0, skip, level, test|TEST_DELIMIT);
+					else
+					{
+						report("failed", fun, re, NiL, -1, msg, flags, test);
+						printf("%s returned: ", codes[got].name);
+						error(&preg, cret);
+					}
+				}
+				else
+				{
+					expected = got = 0;
+					for (i = 1; i < elementsof(codes); i++)
+					{
+						if (streq(ans, codes[i].name))
+							expected = i;
+						if (cret==codes[i].code)
+							got = i;
+					}
+					if (!expected)
+					{
+						if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+							skip = extract(tabs, line, re, s, ans, msg, codes[got].name, NiL, 0, 0, skip, level, test|TEST_DELIMIT);
+						else
+						{
+							report("failed: invalid error code", NiL, re, NiL, -1, msg, flags, test);
+							printf("%s expected, %s returned\n", ans, codes[got].name);
+						}
+					}
+					else if (cret != codes[expected].code && cret != REG_BADPAT)
+					{
+						if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+							skip = extract(tabs, line, re, s, ans, msg, codes[got].name, NiL, 0, 0, skip, level, test|TEST_DELIMIT);
+						else if (test & TEST_IGNORE_ERROR)
+							state.ignored++;
+						else
+						{
+							report("should fail and did", fun, re, NiL, -1, msg, flags, test);
+							printf("%s expected, %s returned: ", ans, codes[got].name);
+							state.errors--;
+							state.warnings++;
+							error(&preg, cret);
+						}
+					}
+				}
+				goto compile;
+			}
+
+#if _REG_nexec
+		execute:
+			if (nexec >= 0)
+				fun = "regnexec";
+			else
+#endif
+				fun = "regexec";
+			
+			for (i = 0; i < elementsof(match); i++)
+				match[i] = state.NOMATCH;
+
+#if _REG_nexec
+			if (nexec >= 0)
+			{
+				eret = regnexec(&preg, s, nexec, nmatch, match, eflags);
+				s[nexec] = 0;
+			}
+			else
+#endif
+			{
+				if (!(test & TEST_CATCH))
+					eret = regexec(&preg, s, nmatch, match, eflags);
+				else if (!(eret = setjmp(state.gotcha)))
+				{
+					alarm(HUNG);
+					eret = regexec(&preg, s, nmatch, match, eflags);
+					alarm(0);
+				}
+			}
+#if _REG_subcomp
+			if ((test & TEST_SUB) && !eret)
+			{
+				fun = "regsubexec";
+				if (!(test & TEST_CATCH))
+					eret = regsubexec(&preg, s, nmatch, match);
+				else if (!(eret = setjmp(state.gotcha)))
+				{
+					alarm(HUNG);
+					eret = regsubexec(&preg, s, nmatch, match);
+					alarm(0);
+				}
+			}
+#endif
+			if (flags & REG_NOSUB)
+			{
+				if (eret)
+				{
+					if (eret != REG_NOMATCH || !streq(ans, "NOMATCH"))
+					{
+						if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+							skip = extract(tabs, line, re, s, ans, msg, "NOMATCH", NiL, 0, 0, skip, level, test|TEST_DELIMIT);
+						else
+						{
+							report("REG_NOSUB failed", fun, re, s, nstr, msg, flags, test);
+							error(&preg, eret);
+						}
+					}
+				}
+				else if (streq(ans, "NOMATCH"))
+				{
+					if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+						skip = extract(tabs, line, re, s, ans, msg, NiL, match, nmatch, nsub, skip, level, test|TEST_DELIMIT);
+					else
+					{
+						report("should fail and didn't", fun, re, s, nstr, msg, flags, test);
+						error(&preg, eret);
+					}
+				}
+			}
+			else if (eret)
+			{
+				if (eret != REG_NOMATCH || !streq(ans, "NOMATCH"))
+				{
+					if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+						skip = extract(tabs, line, re, s, ans, msg, "NOMATCH", NiL, 0, nsub, skip, level, test|TEST_DELIMIT);
+					else
+					{
+						report("failed", fun, re, s, nstr, msg, flags, test);
+						if (eret != REG_NOMATCH)
+							error(&preg, eret);
+						else if (*ans)
+							printf("expected: %s\n", ans);
+						else
+							printf("\n");
+					}
+				}
+			}
+			else if (streq(ans, "NOMATCH"))
+			{
+				if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+					skip = extract(tabs, line, re, s, ans, msg, NiL, match, nmatch, nsub, skip, level, test|TEST_DELIMIT);
+				else
+				{
+					report("should fail and didn't", fun, re, s, nstr, msg, flags, test);
+					matchprint(match, nmatch, nsub, NiL, test);
+				}
+			}
+#if _REG_subcomp
+			else if (test & TEST_SUB)
+			{
+				p = preg.re_sub->re_buf;
+				if (strcmp(p, ans))
+				{
+					report("failed", fun, re, s, nstr, msg, flags, test);
+					quote(ans, -1, test|TEST_DELIMIT);
+					printf(" expected, ");
+					quote(p, -1, test|TEST_DELIMIT);
+					printf(" returned\n");
+				}
+			}
+#endif
+			else if (!*ans)
+			{
+				if (match[0].rm_so != state.NOMATCH.rm_so)
+				{
+					if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+						skip = extract(tabs, line, re, s, ans, msg, NiL, NiL, 0, 0, skip, level, test);
+					else
+					{
+						report("failed: no match but match array assigned", NiL, re, s, nstr, msg, flags, test);
+						matchprint(match, nmatch, nsub, NiL, test);
+					}
+				}
+			}
+			else if (matchcheck(match, nmatch, nsub, ans, re, s, nstr, flags, test))
+			{
+#if _REG_nexec
+				if (nexec < 0 && !nonexec)
+				{
+					nexec = nstr >= 0 ? nstr : strlen(s);
+					s[nexec] = '\n';
+					testno++;
+					goto execute;
+				}
+#endif
+				if (!(test & (TEST_DECOMP|TEST_SUB|TEST_VERIFY)) && !nonosub)
+				{
+					if (catchfree(&preg, flags, tabs, line, re, s, ans, msg, NiL, NiL, 0, 0, skip, level, test))
+						continue;
+					flags |= REG_NOSUB;
+					goto nosub;
+				}
+				if (test & (TEST_BASELINE|TEST_PASS|TEST_VERIFY))
+					skip = extract(tabs, line, re, s, ans, msg, NiL, match, nmatch, nsub, skip, level, test|TEST_OK);
+			}
+			else if (test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS|TEST_QUERY|TEST_SUMMARY|TEST_VERIFY))
+				skip = extract(tabs, line, re, s, ans, msg, NiL, match, nmatch, nsub, skip, level, test|TEST_DELIMIT);
+			if (catchfree(&preg, flags, tabs, line, re, s, ans, msg, NiL, NiL, 0, 0, skip, level, test))
+				continue;
+			goto compile;
+		}
+		if (test & TEST_SUMMARY)
+			printf("tests=%-4d errors=%-4d warnings=%-2d ignored=%-2d unspecified=%-2d signals=%d\n", testno, state.errors, state.warnings, state.ignored, state.unspecified, state.signals);
+		else if (!(test & (TEST_ACTUAL|TEST_BASELINE|TEST_FAIL|TEST_PASS)))
+		{
+			printf("TEST\t%s", unit);
+			if (subunit)
+				printf(" %-.*s", subunitlen, subunit);
+			printf(", %d test%s", testno, testno == 1 ? "" : "s");
+			if (state.ignored)
+				printf(", %d ignored mismatche%s", state.ignored, state.ignored == 1 ? "" : "s");
+			if (state.warnings)
+				printf(", %d warning%s", state.warnings, state.warnings == 1 ? "" : "s");
+			if (state.unspecified)
+				printf(", %d unspecified difference%s", state.unspecified, state.unspecified == 1 ? "" : "s");
+			if (state.signals)
+				printf(", %d signal%s", state.signals, state.signals == 1 ? "" : "s");
+			printf(", %d error%s\n", state.errors, state.errors == 1 ? "" : "s");
+		}
+		if (fp != stdin)
+			fclose(fp);
+	}
+	return 0;
+}