]> granicus.if.org Git - nethack/commitdiff
Unix command line parsing
authorPatR <rankin@nethack.org>
Fri, 18 Feb 2022 22:38:24 +0000 (14:38 -0800)
committerPatR <rankin@nethack.org>
Fri, 18 Feb 2022 22:38:24 +0000 (14:38 -0800)
Move a bunch of stuff out of main() into new early_options(): '-dpath'
playground directory handling, '-s ...' show scores instead of playing,
and the 'argcheck()' options:  --version, --showpaths, --dumpenums,
and --debug (not to be confused with -D).  Also introduce
| --nethackrc=filename
| --no-nethackrc
to control RC file without using NETHACKOPTIONS so that that is still
available for setting other options.  They can start with either one
or two dashes.  --no-nethackrc is just --nethackrc=/dev/null under the
hood.  '-dpath' can now be '--directory=path' or '--directory path'
but the old syntax should still work.  '-s ...' can be '--scores ...'.

Basic call sequence in unixmain relating to options is now
|main() {
|  early_options(argc, argv[]);
|  initoptions(); /* process sysconf, .nethackrc, NETHACKOPTIONS */
|  process_options(possibly_modified_argc, possibly_modified_argv[]);
|}
Options processed by early_options() that don't terminate the program
are moved to the end of argv[], with argc reduced accordingly.  Then
process_options() only sees the ones that early_options() declines to
handle.

Most early options were using plain exit() instead of nh_terminate()
so not performing any nethack-specific cleanup.  However, since they
run before the game starts, there wasn't much cleanup being overlooked.

chdirx() takes a boolean as second argument but all its callers were
passing int (with value of 1 or 0, so it still worked after being
implicitly fixed by prototype).  Change them to pass TRUE or FALSE.

argcheck() was refusing (argc,argv[]) with count of 1 but then it was
checking 0..N-1 rather than 1..N-1, so it tested whether argv[0] was
an argument instead of skipping that as the program name.  Change to
allow count of 1 with modified argv that has an option name in argv[0].
That happens to fit well with how early_options() wanted to use it.

doc/fixes3-7-0.txt
src/allmain.c
sys/unix/unixmain.c

index 1d692a7f883c615aefb24e9f094141078e513b88..fdff91e234c97c808dd33a0731c4f905b7785f84 100644 (file)
@@ -1,4 +1,4 @@
-NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.788 $ $NHDT-Date: 1644610217 2022/02/11 20:10:17 $
+NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.799 $ $NHDT-Date: 1645223893 2022/02/18 22:38:13 $
 
 General Fixes and Modified Features
 -----------------------------------
@@ -1232,6 +1232,7 @@ Unix: add "ec2-user" to the list of user names 'sysconf' classifies as generic
 Unix: work-around a build issue in ubuntu 21.10 by using ifdef to skip the
        define of warn_unused_result to empty string in tradstdc.h whenever
        __linux__ is defined during build unless GCC_URWARN is also defined
+Unix: re-do command line parsing
 user_sounds: move the message hook from inside individual window display ports
        to the core where it allows MSGTYP_NOSHOW msgtyp's to still trigger
        sounds to correct a reported github issue; also fixes a past reported
@@ -1470,6 +1471,9 @@ tty: if a message is marked urgent, override message suppression initiated
        by user having typed ESC at previous --More-- prompt
 Unix: can define NOSUSPEND in config.h or src/Makefile's CFLAGS to prevent
        unixconf.h from enabling SUSPEND without need to modify unixconf.h
+Unix: support --nethackrc=filename on the command line; same effect as
+       NETHACKOPTIONS='@filename' but leaves NETHACKOPTIONS available for
+       specifying options; --no-nethackrc is same as --nethackrc=/dev/null
 X11: implement 'selectsaved', restore via menu of saved games
 X11: echo getline prompt and response (wishes, applying names) to message
        window and dumplog message history
index 7fcf18b19da42d57570d10979a015c67d6df0c82..7e8839799a3c62f953f2cc17ec6195e750cf702d 100644 (file)
@@ -1,4 +1,4 @@
-/* NetHack 3.7 allmain.c       $NHDT-Date: 1644517022 2022/02/10 18:17:02 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.174 $ */
+/* NetHack 3.7 allmain.c       $NHDT-Date: 1645223894 2022/02/18 22:38:14 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.177 $ */
 /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */
 /*-Copyright (c) Robert Patrick Rankin, 2012. */
 /* NetHack may be freely redistributed.  See license for details. */
@@ -845,7 +845,6 @@ extern int windows_early_options(const char *);
  *    1 = found and skip past this argument
  *    2 = found and trigger immediate exit
  */
-
 int
 argcheck(int argc, char *argv[], enum earlyarg e_arg)
 {
@@ -858,7 +857,7 @@ argcheck(int argc, char *argv[], enum earlyarg e_arg)
         if (earlyopts[idx].e == e_arg)
             break;
     }
-    if ((idx >= SIZE(earlyopts)) || (argc <= 1))
+    if (idx >= SIZE(earlyopts) || argc < 1)
         return FALSE;
 
     for (i = 0; i < argc; ++i) {
index 8ba7321292f23a95dcb73e44e13f1425b9497514..c62254060c667d7f182fcf682f4fe9f6e4a7f618 100644 (file)
@@ -1,4 +1,4 @@
-/* NetHack 3.7 unixmain.c      $NHDT-Date: 1644866265 2022/02/14 19:17:45 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.96 $ */
+/* NetHack 3.7 unixmain.c      $NHDT-Date: 1645223897 2022/02/18 22:38:17 $  $NHDT-Branch: NetHack-3.7 $:$NHDT-Revision: 1.97 $ */
 /* Copyright (c) Stichting Mathematisch Centrum, Amsterdam, 1985. */
 /*-Copyright (c) Robert Patrick Rankin, 2011. */
 /* NetHack may be freely redistributed.  See license for details. */
@@ -30,7 +30,14 @@ extern struct passwd *getpwnam(const char *);
 static void chdirx(const char *, boolean);
 #endif /* CHDIR */
 static boolean whoami(void);
+static char *lopt(char *, int, const char *, const char *, int *, char ***);
 static void process_options(int, char **);
+static void consume_arg(int, int *, char ***);
+static void consume_two_args(int, int *, char ***);
+static void early_options(int *, char ***, char **);
+static void opt_terminate(void) NORETURN;
+static void opt_showpaths(const char *);
+static void scores_only(int, char **, const char *) NORETURN;
 
 #ifdef _M_UNIX
 extern void check_sco_console(void);
@@ -48,9 +55,7 @@ static struct passwd *get_unix_pw(void);
 int
 main(int argc, char *argv[])
 {
-#ifdef CHDIR
-    char *dir;
-#endif
+    char *dir = NULL;
     NHFILE *nhfp;
     boolean exact_username;
     boolean resuming = FALSE; /* assume new game */
@@ -108,89 +113,23 @@ main(int argc, char *argv[])
     if (!dir)
         dir = nh_getenv("HACKDIR");
 #endif /* CHDIR */
+    /* handle -dalthackdir, -s <score stuff>, --version, --showpaths */
+    early_options(&argc, &argv, &dir);
 
-    if (argc > 1) {
-        if (argcheck(argc, argv, ARG_VERSION) == 2)
-            exit(EXIT_SUCCESS);
-
-#ifndef NODUMPENUMS
-        if (argcheck(argc, argv, ARG_DUMPENUMS) == 2)
-            exit(EXIT_SUCCESS);
-#endif
-        if (argcheck(argc, argv, ARG_SHOWPATHS) == 2) {
-#ifdef CHDIR
-            chdirx((char *) 0, 0);
-#endif
-            iflags.initoptions_noterminate = TRUE;
-            initoptions();
-            iflags.initoptions_noterminate = FALSE;
-            reveal_paths();
-            exit(EXIT_SUCCESS);
-        }
-        if (argcheck(argc, argv, ARG_DEBUG) == 1) {
-            argc--;
-            argv++;
-        }
-#ifdef CHDIR
-        if (argc > 1 && !strncmp(argv[1], "-d", 2) && argv[1][2] != 'e') {
-            /* avoid matching "-dec" for DECgraphics; since the man page
-             * says -d directory, hope nobody's using -desomething_else
-             */
-            argc--;
-            argv++;
-            dir = argv[0] + 2;
-            if (*dir == '=' || *dir == ':')
-                dir++;
-            if (!*dir && argc > 1) {
-                argc--;
-                argv++;
-                dir = argv[0];
-            }
-            if (!*dir)
-                error("Flag -d must be followed by a directory name.");
-        }
-    }
-#endif /* CHDIR */
-
-    if (argc > 1) {
-        /*
-         * Now we know the directory containing 'record' and
-         * may do a prscore().  Exclude `-style' - it's a Qt option.
-         */
-        if (!strncmp(argv[1], "-s", 2) && strncmp(argv[1], "-style", 6)) {
 #ifdef CHDIR
-            chdirx(dir, 0);
-#endif
-#ifdef SYSCF
-            initoptions();
-#endif
-#ifdef PANICTRACE
-            ARGV0 = g.hname; /* save for possible stack trace */
-#ifndef NO_SIGNAL
-            panictrace_setsignals(TRUE);
-#endif
-#endif
-            prscore(argc, argv);
-            /* FIXME: shouldn't this be using nh_terminate() to free
-               up any memory allocated by initoptions() */
-            exit(EXIT_SUCCESS);
-        }
-    } /* argc > 1 */
-
-/*
- * Change directories before we initialize the window system so
- * we can find the tile file.
- */
-#ifdef CHDIR
-    chdirx(dir, 1);
+    /*
+     * Change directories before we initialize the window system so
+     * we can find the tile file.
+     */
+    chdirx(dir, TRUE);
 #endif
-
 #ifdef _M_UNIX
     check_sco_console();
 #endif
 #ifdef __linux__
     check_linux_console();
 #endif
+
     initoptions();
 #ifdef PANICTRACE
     ARGV0 = g.hname; /* save for possible stack trace */
@@ -346,12 +285,94 @@ main(int argc, char *argv[])
     return 0;
 }
 
+static char ArgVal_novalue[] = "[nothing]"; /* note: not 'const' */
+
+enum cmdlinearg {
+    ArgValRequired = 0, ArgValOptional = 1,
+    ArgValDisallowed = 2, ArgVal_mask = (1 | 2),
+    ArgNamOneLetter = 4, ArgNam_mask = 4,
+    ArgErrSilent = 0, ArgErrComplain = 8, ArgErr_mask = 8
+};
+
+/* approximate 'getopt_long()' for one option; all the comments refer to
+   "-windowtype" but the code isn't specific to that  */
+static char *
+lopt(
+    char *arg,           /* command line token; beginning matches 'optname' */
+    int lflags,          /* cmdlinearg | errorhandling */
+    const char *optname, /* option's name; "-windowtype" in examples below */
+    const char *origarg, /* 'arg' might have had a dash prefix removed */
+    int *argc_p,         /* argc that can have changes passed to caller */
+    char ***argv_p)      /* argv[] ditto */
+{
+    int argc = *argc_p;
+    char **argv = *argv_p;
+    char *p, *nextarg = (argc > 1 && argv[1][0] != '-') ? argv[1] : 0;
+    int l, opttype = (lflags & ArgVal_mask);
+    boolean oneletterok = ((lflags & ArgNam_mask) == ArgNamOneLetter),
+            complain = ((lflags & ArgErr_mask) == ArgErrComplain);
+
+    /* first letter must match */
+    if (arg[1] != optname[1]) {
+ loptbail:
+        if (complain)
+            config_error_add("Unknown option: %.60s", origarg);
+        return (char *) 0;
+ loptnotallowed:
+        if (complain)
+            config_error_add("Value not allowed: %.60s", origarg);
+        return (char *) 0;
+ loptrequired:
+        if (complain)
+            config_error_add("Missing required value: %.60s", origarg);
+        return (char *) 0;
+    }
+
+    if ((p = index(arg, '=')) == 0)
+        p = index(arg, ':');
+    if (p && opttype == ArgValDisallowed)
+        goto loptnotallowed;
+
+    l = (int) (p ? (p - arg) : strlen(arg));
+    if (!strncmp(arg, optname, l)) {
+        /* "-windowtype[=foo]" */
+        if (p)
+            ++p; /* past '=' or ':' */
+        else if (opttype == ArgValRequired)
+            p = eos(arg); /* we have "-w[indowtype]" w/o "=foo"
+                           * so we'll take foo from next element */
+        else
+            return ArgVal_novalue;
+    } else if (oneletterok) {
+        /* "-w..." but not "-w[indowtype[=foo]]" */
+        if (!p) {
+            p = &arg[2]; /* past 'w' of "-wfoo" */
+        } else {
+            /* "-w...=foo" but not "-w[indowtype]=foo" */
+            goto loptbail;
+        }
+    } else {
+        goto loptbail;
+    }
+    if (!p || !*p) {
+        /* "-w[indowtype]" w/o '='/':' if there is a next element, use
+           it for "foo"; if not, supply a non-Null bogus value */
+        if (nextarg && (opttype == ArgValRequired
+                        || opttype == ArgValOptional))
+            p = nextarg, --(*argc_p), ++(*argv_p);
+        else if (opttype == ArgValRequired)
+            goto loptrequired;
+        else
+            p = ArgVal_novalue; /* there is no next element */
+    }
+    return p;
+}
+
 /* caveat: argv elements might be arbitrary long */
 static void
 process_options(int argc, char *argv[])
 {
-    static char novalue[] = "[nothing]"; /* note: not 'const' */
-    char *p, *arg, *origarg;
+    char *arg, *origarg;
     int i, l;
 
     config_error_init(FALSE, "command line", FALSE);
@@ -373,9 +394,10 @@ process_options(int argc, char *argv[])
             && (arg[3] != '\0' && arg[3] != '=' && arg[3] != ':'))
             ++arg;
         l = (int) strlen(arg);
-        /* must supply at least 4 chars to match "-XXXgraphics" */
-        if (l < 4)
-            l = 4;
+        if (l < 6 && !strncmp(arg, "-no-", 4))
+            l = 6;
+        else if (l < 4)
+            l = 4; /* must supply at least 4 chars to match "-XXXgraphics" */
 
         switch (arg[1]) {
         case 'D':
@@ -392,11 +414,18 @@ process_options(int argc, char *argv[])
         case 'X':
             discover = TRUE, wizard = FALSE;
             break;
-#ifdef NEWS
         case 'n':
-            iflags.news = FALSE;
-            break;
+#ifdef NEWS
+            if (!arg[2] || !strcmp(arg, "-no-news")) {
+                iflags.news = FALSE;
+                break;
+            } else if (!strcmp(arg, "-news")) {
+                /* in case RC has !news, allow 'nethack -news' to override */
+                iflags.news = TRUE;
+                break;
+            }
 #endif
+            break;
         case 'u':
             if (arg[2]) {
                 (void) strncpy(g.plname, arg + 2, sizeof g.plname - 1);
@@ -407,7 +436,7 @@ process_options(int argc, char *argv[])
                 (void) strncpy(g.plname, argv[0], sizeof g.plname - 1);
                 g.plnamelen = 0;
             } else {
-                config_error_add("Player name expected after -u");
+                config_error_add("Character name expected after -u");
             }
             break;
         case 'I':
@@ -444,34 +473,11 @@ process_options(int argc, char *argv[])
             break;
         case 'w': /* windowtype: "-wfoo" or "-w[indowtype]=foo"
                    * or "-w[indowtype]:foo" or "-w[indowtype] foo" */
-            if ((p = index(arg, '=')) == 0)
-                p = index(arg, ':');
-            l = (int) (p ? (p - arg) : strlen(arg));
-            if (!strncmp(arg, "-windowtype", l)) {
-                /* "-windowtype[=foo]" */
-                if (p)
-                    ++p; /* past '=' or ':' */
-                else
-                    p = eos(arg); /* we have "-w[indowtype]" w/o "=foo"
-                                   * so we'll take foo from next element */
-            } else {
-                /* "-w..." but not "-w[indowtype[=foo]]" */
-                if (!p) {
-                    p = &arg[2]; /* past 'w' of "-wfoo" */
-                } else {
-                    /* "-w...=foo" but not "-w[indowtype]=foo" */
-                    config_error_add("Unknown option: %.60s", origarg);
-                    continue;
-                }
-            }
-            if (!*p) {
-                /* "-w[indowtype]" w/o '='/':' use next element for "foo" */
-                if (argc > 1)
-                    --argc, ++argv, p = argv[0];
-                else
-                    p = novalue; /* there is no next element */
-            }
-            choose_windows(p);
+            arg = lopt(arg,
+                       (ArgValRequired | ArgNamOneLetter | ArgErrComplain),
+                       "-windowtype", origarg, &argc, &argv);
+            if (arg)
+                choose_windows(arg);
             break;
         case '@':
             flags.randomall = 1;
@@ -492,14 +498,22 @@ process_options(int argc, char *argv[])
         }
     }
 
+    if (argc > 1) {
+        int mxplyrs = atoi(argv[1]);
+        boolean mx_ok = mxplyrs != 0;
 #ifdef SYSCF
-    if (argc > 1)
-        config_error_add("MAXPLAYERS are set in sysconf file.\n");
+        config_error_add("%s%s%s",
+                         mx_ok ? "MAXPLAYERS are set in sysconf file"
+                               : "Expected MAXPLAYERS, found \"",
+                         mx_ok ? "" : argv[1], mx_ok ? "" : "\"");
 #else
-    /* XXX This is deprecated in favor of SYSCF with MAXPLAYERS */
-    if (argc > 1)
-        g.locknum = atoi(argv[1]);
+        /* XXX This is deprecated in favor of SYSCF with MAXPLAYERS */
+        if (mx_ok)
+            g.locknum = mxplyrs;
+        else
+            config_error_add("Invalid MAXPLATERS \"%s\"", argv[1]);
 #endif
+    }
 #ifdef MAX_NR_OF_PLAYERS
     /* limit to compile-time limit */
     if (!g.locknum || g.locknum > MAX_NR_OF_PLAYERS)
@@ -512,6 +526,207 @@ process_options(int argc, char *argv[])
 #endif
     /* empty or "N errors on command line" */
     config_error_done();
+    return;
+}
+
+/* move argv[ndx] to end of argv[] array, then reduce argc to hide it;
+   prevents process_options() from encountering it after early_options()
+   has processed it; elements get reordered but all remain intact */
+static void
+consume_arg(int ndx, int *ac_p, char ***av_p)
+{
+    char *gone, **av = *av_p;
+    int i, ac = *ac_p;
+
+    /* "-one -two -three -four" -> "-two -three -four -one" */
+    if (ac > 2) {
+        gone = av[ndx];
+        for (i = ndx + 1; i < ac; ++i)
+            av[i - 1] = av[i];
+        av[ac - 1] = gone;
+    }
+    --(*ac_p);
+}
+
+/* consume two tokens for '-argname value' w/o '=' or ':' */
+static void
+consume_two_args(int ndx, int *ac_p, char ***av_p)
+{
+    /* when consuming "-two arg" from "-two arg -three -four",
+       the *ac_p manipulation results in "-three -four -two arg"
+       rather than the "-three -four arg -two" that would happen
+       with just two ordinary consume_arg() calls */
+    consume_arg(ndx, ac_p, av_p);
+    ++(*ac_p); /* bring the final slot back into view */
+    consume_arg(ndx, ac_p, av_p);
+    --(*ac_p); /* take away restored slot */
+}
+
+/* process some command line arguments before loading options */
+static void
+early_options(int *argc_p, char ***argv_p, char **hackdir_p)
+{
+    char **argv, *arg, *origarg;
+    int argc, oldargc, ndx = 0, consumed = 0;
+
+    config_error_init(FALSE, "command line", FALSE);
+
+    /*
+     * Both *argc_p and *argv_p account for the program name as (*argv_p)[0];
+     * local argc and argv impicitly discard that (by starting 'ndx' at 1).
+     * argcheck() doesn't mind, prscore() (via scores_only()) does.
+     */
+    for (ndx = 1; ndx < *argc_p; ndx += (consumed ? 0 : 1)) {
+        consumed = 0;
+        argc = *argc_p - ndx;
+        argv = *argv_p + ndx;
+
+        arg = origarg = argv[0];
+        /* skip any args intended for deferred options */
+        if (*arg != '-')
+            continue;
+        /* allow second dash if arg name is longer than one character */
+        if (arg[0] == '-' && arg[1] == '-' && arg[2] != '\0'
+            && (arg[3] != '\0' && arg[3] != '=' && arg[3] != ':'))
+            ++arg;
+
+        switch (arg[1]) { /* char after leading dash */
+        case 'd':
+            if (argcheck(argc, argv, ARG_DEBUG) == 1) {
+                consume_arg(ndx, argc_p, argv_p), consumed = 1;
+#ifndef NODUMPENUMS
+            } else if (argcheck(argc, argv, ARG_DUMPENUMS) == 2) {
+                opt_terminate();
+                /*NOTREACHED*/
+#endif
+            } else {
+#ifdef CHDIR
+                oldargc = argc;
+                arg = lopt(arg,
+                           (ArgValRequired | ArgNamOneLetter | ArgErrSilent),
+                           "-directory", origarg, &argc, &argv);
+                if (!arg)
+                    error("Flag -d must be followed by a directory name.");
+                if (*arg != 'e') { /* avoid matching -decgraphics or -debug */
+                    *hackdir_p = arg;
+                    if (oldargc == argc)
+                        consume_arg(ndx, argc_p, argv_p), consumed = 1;
+                    else
+                        consume_two_args(ndx, argc_p, argv_p), consumed = 2;
+                }
+#endif /* CHDIR */
+            }
+            break;
+        case 'n':
+            oldargc = argc;
+            if (!strcmp(arg, "-no-nethackrc")) /* no abbreviation allowed */
+                arg = nhStr("/dev/null");
+            else
+                arg = lopt(arg, (ArgValRequired | ArgErrComplain),
+                           "-nethackrc", origarg, &argc, &argv);
+            if (arg) {
+                g.cmdline_rcfile = dupstr(arg);
+                if (oldargc == argc)
+                    consume_arg(ndx, argc_p, argv_p), consumed = 1;
+                else
+                    consume_two_args(ndx, argc_p, argv_p), consumed = 2;
+            }
+            break;
+        case 's':
+            if (argcheck(argc, argv, ARG_SHOWPATHS) == 2) {
+                opt_showpaths(*hackdir_p);
+                opt_terminate();
+                /*NOTREACHED*/
+            }
+            /* check for "-s" request to show scores */
+            if (lopt(arg,
+                     (ArgValDisallowed | ArgNamOneLetter | ArgErrComplain),
+                     "-scores", origarg, &argc, &argv)) {
+                /* at this point, argv[0] contains "-scores" or a leading
+                   substring of it; prscore() (via scores_only()) expects
+                   that to be in argv[1] so we adjust the pointer to make
+                   that be the case; if there are any non-early args waiting
+                   to be passed along to process_options(), the resulting
+                   argv[0] will be one of those rather than the program
+                   name but prscore() doesn't care */
+                scores_only(argc + 1, argv - 1, *hackdir_p);
+                /*NOTREACHED*/
+            }
+            break;
+        case 'v':
+            if (argcheck(argc, argv, ARG_VERSION) == 2) {
+                opt_terminate();
+                /*NOTREACHED*/
+            }
+            break;
+        default:
+            break;
+        }
+    }
+    /* empty or "N errors on command line" */
+    config_error_done();
+    return;
+}
+
+/* for command-line options that perform some immediate action and then
+   terminate the program without starting play, like 'nethack --version'
+   or 'nethack -s Zelda'; do some cleanup before that termination */
+static void
+opt_terminate(void)
+{
+    config_error_done(); /* free memory allocated by config_error_init() */
+
+    nh_terminate(EXIT_SUCCESS);
+    /*NOTREACHED*/
+}
+
+/* show the sysconf file name, playground directory, run-time configuration
+   file name, dumplog file name if applicable, and some other things */
+static void
+opt_showpaths(const char *dir)
+{
+#ifdef CHDIR
+    chdirx(dir, FALSE);
+#else
+    nhUse(dir);
+#endif
+    iflags.initoptions_noterminate = TRUE;
+    initoptions();
+    iflags.initoptions_noterminate = FALSE;
+    reveal_paths();
+}
+
+/* handle "-s <score options> [character-names]" to show all the entries
+   in the high scores file ('record') belonging to particular characters;
+   nethack will end after doing so without starting play */
+static void
+scores_only(int argc, char **argv, const char *dir)
+{
+    /* do this now rather than waiting for final termination, in case there
+       is an error summary coming */
+    config_error_done();
+
+#ifdef CHDIR
+    chdirx(dir, FALSE);
+#else
+    nhUse(dir);
+#endif
+#ifdef SYSCF
+    iflags.initoptions_noterminate = TRUE;
+    initoptions(); /* sysconf options affect whether panictrace is enabled */
+    iflags.initoptions_noterminate = FALSE;
+#endif
+#ifdef PANICTRACE
+    ARGV0 = g.hname; /* save for possible stack trace */
+#ifndef NO_SIGNAL
+    panictrace_setsignals(TRUE);
+#endif
+#endif
+
+    prscore(argc, argv);
+
+    nh_terminate(EXIT_SUCCESS); /* bypass opt_terminate() */
+    /*NOTREACHED*/
 }
 
 #ifdef CHDIR
@@ -535,24 +750,26 @@ chdirx(const char *dir, boolean wr)
 #ifdef VAR_PLAYGROUND
         int len = strlen(VAR_PLAYGROUND);
 
+        /* FIXME: this allocation never gets freed.
+         */
         g.fqn_prefix[SCOREPREFIX] = (char *) alloc(len + 2);
         Strcpy(g.fqn_prefix[SCOREPREFIX], VAR_PLAYGROUND);
         if (g.fqn_prefix[SCOREPREFIX][len - 1] != '/') {
             g.fqn_prefix[SCOREPREFIX][len] = '/';
             g.fqn_prefix[SCOREPREFIX][len + 1] = '\0';
         }
-
 #endif
     }
 
 #ifdef HACKDIR
-    if (dir == (const char *) 0)
+    if (!dir)
         dir = HACKDIR;
 #endif
 
     if (dir && chdir(dir) < 0) {
         perror(dir);
         error("Cannot chdir to %s.", dir);
+        /*NOTREACHED*/
     }
 
     /* warn the player if we can't write the record file
@@ -561,6 +778,10 @@ chdirx(const char *dir, boolean wr)
      */
     if (wr) {
 #ifdef VAR_PLAYGROUND
+        /* FIXME: if termination cleanup ever frees fqn_prefix[0..N-1],
+         * these will need to use dupstr() so that they have distinct
+         * values that can be freed separately.
+         */
         g.fqn_prefix[LEVELPREFIX] = g.fqn_prefix[SCOREPREFIX];
         g.fqn_prefix[SAVEPREFIX] = g.fqn_prefix[SCOREPREFIX];
         g.fqn_prefix[BONESPREFIX] = g.fqn_prefix[SCOREPREFIX];
@@ -569,6 +790,7 @@ chdirx(const char *dir, boolean wr)
 #endif
         check_recordfile(dir);
     }
+    return;
 }
 #endif /* CHDIR */