Revamp of command-line handling. Most command line options should
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Sun, 4 Aug 2002 21:18:56 +0000 (21:18 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Sun, 4 Aug 2002 21:18:56 +0000 (21:18 +0000)
now be processed in cmdline.c, which is called from all utilities
(well, not Pageant or PuTTYgen). This should mean we get to
standardise almost all options across almost all tools. Also one
major change: `-load' is now the preferred option for loading a
saved session in PuTTY proper. `@session' still works but is
deprecated.

git-svn-id: svn://svn.tartarus.org/sgt/putty@1799 cda61777-01e9-0310-a592-d414129be87e

Recipe
cmdline.c [new file with mode: 0644]
console.c
plink.c
psftp.c
putty.h
scp.c
ssh.c
window.c
winstuff.h
winutils.c [new file with mode: 0644]

diff --git a/Recipe b/Recipe
index b0b849b..a80ed80 100644 (file)
--- a/Recipe
+++ b/Recipe
@@ -90,7 +90,7 @@
 
 # GUI front end and terminal emulator (putty, puttytel).
 GUITERM  = window windlg winctrls terminal sizetip wcwidth unicode
-         + logging printing
+         + logging printing winutils
 
 # Non-SSH back ends (putty, puttytel, plink).
 NONSSH   = telnet raw rlogin ldisc
@@ -105,7 +105,7 @@ SFTP     = sftp int64 logging
 
 # Miscellaneous objects appearing in all the network utilities (not
 # Pageant or PuTTYgen).
-MISC     = misc version winstore settings tree234 winnet proxy
+MISC     = misc version winstore settings tree234 winnet proxy cmdline
 
 # Standard libraries, and the same with WinSocks 1 and 2.
 LIBS     = advapi32.lib user32.lib gdi32.lib comctl32.lib comdlg32.lib
diff --git a/cmdline.c b/cmdline.c
new file mode 100644 (file)
index 0000000..f26625b
--- /dev/null
+++ b/cmdline.c
@@ -0,0 +1,246 @@
+#include <windows.h>\r
+#include <stdio.h>\r
+#include <assert.h>\r
+#include <stdlib.h>\r
+#include "putty.h"\r
+\r
+/*\r
+ * Some command-line parameters need to be saved up until after\r
+ * we've loaded the saved session which will form the basis of our\r
+ * eventual running configuration. For this we use the macro\r
+ * SAVEABLE, which notices if the `need_save' parameter is set and\r
+ * saves the parameter and value on a list.\r
+ * \r
+ * We also assign priorities to saved parameters, just to slightly\r
+ * ameliorate silly ordering problems. For example, if you specify\r
+ * a saved session to load, it will be loaded _before_ all your\r
+ * local modifications such as -L are evaluated; and if you specify\r
+ * a protocol and a port, the protocol is set up first so that the\r
+ * port can override its choice of port number.\r
+ */\r
+\r
+#define NPRIORITIES 3\r
+\r
+struct cmdline_saved_param {\r
+    char *p, *value;\r
+};\r
+struct cmdline_saved_param_set {\r
+    struct cmdline_saved_param *params;\r
+    int nsaved, savesize;\r
+};\r
+\r
+/*\r
+ * C guarantees this structure will be initialised to all zero at\r
+ * program start, which is exactly what we want.\r
+ */\r
+static struct cmdline_saved_param_set saves[NPRIORITIES];\r
+\r
+static void cmdline_save_param(char *p, char *value, int pri)\r
+{\r
+    if (saves[pri].nsaved >= saves[pri].savesize) {\r
+       saves[pri].savesize = saves[pri].nsaved + 32;\r
+       saves[pri].params =\r
+           srealloc(saves[pri].params,\r
+                    saves[pri].savesize*sizeof(*saves[pri].params));\r
+    }\r
+    saves[pri].params[saves[pri].nsaved].p = p;\r
+    saves[pri].params[saves[pri].nsaved].value = value;\r
+    saves[pri].nsaved++;\r
+}\r
+\r
+#define SAVEABLE(pri) do { \\r
+    if (need_save) { cmdline_save_param(p, value, pri); return ret; } \\r
+} while (0)\r
+\r
+char *cmdline_password = NULL;\r
+\r
+static int cmdline_get_line(const char *prompt, char *str,\r
+                            int maxlen, int is_pw)\r
+{\r
+    static int tried_once = 0;\r
+\r
+    assert(is_pw && cmdline_password);\r
+\r
+    if (tried_once) {\r
+       return 0;\r
+    } else {\r
+       strncpy(str, cmdline_password, maxlen);\r
+       str[maxlen - 1] = '\0';\r
+       tried_once = 1;\r
+       return 1;\r
+    }\r
+}\r
+\r
+/*\r
+ * Here we have a flags word which describes the capabilities of\r
+ * the particular tool on whose behalf we're running. We will\r
+ * refuse certain command-line options if a particular tool\r
+ * inherently can't do anything sensible. For example, the file\r
+ * transfer tools (psftp, pscp) can't do a great deal with protocol\r
+ * selections (ever tried running scp over telnet?) or with port\r
+ * forwarding (even if it wasn't a hideously bad idea, they don't\r
+ * have the select() infrastructure to make them work).\r
+ */\r
+int cmdline_tooltype = 0;\r
+\r
+static int cmdline_check_unavailable(int flag, char *p)\r
+{\r
+    if (cmdline_tooltype & flag) {\r
+       cmdline_error("option \"%s\" not available in this tool", p);\r
+       return 1;\r
+    }\r
+    return 0;\r
+}\r
+\r
+#define UNAVAILABLE_IN(flag) do { \\r
+    if (cmdline_check_unavailable(flag, p)) return ret; \\r
+} while (0)\r
+\r
+/*\r
+ * Process a standard command-line parameter. `p' is the parameter\r
+ * in question; `value' is the subsequent element of argv, which\r
+ * may or may not be required as an operand to the parameter.\r
+ * Return value is 2 if both arguments were used; 1 if only p was\r
+ * used; 0 if the parameter wasn't one we recognised; -2 if it\r
+ * should have been 2 but value was NULL.\r
+ */\r
+\r
+#define RETURN(x) do { \\r
+    if ((x) == 2 && !value) return -2; \\r
+    ret = x; \\r
+} while (0)\r
+\r
+int cmdline_process_param(char *p, char *value, int need_save)\r
+{\r
+    int ret = 0;\r
+\r
+    if (!strcmp(p, "-load")) {\r
+       RETURN(2);\r
+       SAVEABLE(0);                   /* very high priority */\r
+       do_defaults(value, &cfg);\r
+       return 2;\r
+    }\r
+    if (!strcmp(p, "-ssh")) {\r
+       RETURN(1);\r
+       UNAVAILABLE_IN(TOOLTYPE_FILETRANSFER);\r
+       SAVEABLE(1);\r
+       default_protocol = cfg.protocol = PROT_SSH;\r
+       default_port = cfg.port = 22;\r
+       return 1;\r
+    }\r
+    if (!strcmp(p, "-telnet")) {\r
+       RETURN(1);\r
+       UNAVAILABLE_IN(TOOLTYPE_FILETRANSFER);\r
+       SAVEABLE(1);\r
+       default_protocol = cfg.protocol = PROT_TELNET;\r
+       default_port = cfg.port = 23;\r
+       return 1;\r
+    }\r
+    if (!strcmp(p, "-rlogin")) {\r
+       RETURN(1);\r
+       UNAVAILABLE_IN(TOOLTYPE_FILETRANSFER);\r
+       SAVEABLE(1);\r
+       default_protocol = cfg.protocol = PROT_RLOGIN;\r
+       default_port = cfg.port = 513;\r
+       return 1;\r
+    }\r
+    if (!strcmp(p, "-raw")) {\r
+       RETURN(1);\r
+       UNAVAILABLE_IN(TOOLTYPE_FILETRANSFER);\r
+       SAVEABLE(1);\r
+       default_protocol = cfg.protocol = PROT_RAW;\r
+    }\r
+    if (!strcmp(p, "-v")) {\r
+       RETURN(1);\r
+       flags |= FLAG_VERBOSE;\r
+    }\r
+    if (!strcmp(p, "-l")) {\r
+       RETURN(2);\r
+       SAVEABLE(1);\r
+       strncpy(cfg.username, value, sizeof(cfg.username));\r
+       cfg.username[sizeof(cfg.username) - 1] = '\0';\r
+    }\r
+    if ((!strcmp(p, "-L") || !strcmp(p, "-R"))) {\r
+       char *fwd, *ptr, *q;\r
+       int i=0;\r
+       RETURN(2);\r
+       UNAVAILABLE_IN(TOOLTYPE_FILETRANSFER);\r
+       SAVEABLE(1);\r
+       fwd = value;\r
+       ptr = cfg.portfwd;\r
+       /* if multiple forwards, find end of list */\r
+       if (ptr[0]=='R' || ptr[0]=='L') {\r
+           for (i = 0; i < sizeof(cfg.portfwd) - 2; i++)\r
+               if (ptr[i]=='\000' && ptr[i+1]=='\000')\r
+                   break;\r
+           ptr = ptr + i + 1;  /* point to next forward slot */\r
+       }\r
+       ptr[0] = p[1];  /* insert a 'L' or 'R' at the start */\r
+       if (strlen(fwd) > sizeof(cfg.portfwd) - i - 2) {\r
+           cmdline_error("out of space for port forwardings");\r
+           return ret;\r
+       }\r
+       strncpy(ptr+1, fwd, sizeof(cfg.portfwd) - i);\r
+       q = strchr(ptr, ':');\r
+       if (q) *q = '\t';      /* replace first : with \t */\r
+       cfg.portfwd[sizeof(cfg.portfwd) - 1] = '\0';\r
+       cfg.portfwd[sizeof(cfg.portfwd) - 2] = '\0';\r
+       ptr[strlen(ptr)+1] = '\000';    /* append two '\000' */\r
+    }\r
+    if (!strcmp(p, "-m")) {\r
+       char *filename, *command;\r
+       int cmdlen, cmdsize;\r
+       FILE *fp;\r
+       int c, d;\r
+\r
+       RETURN(2);\r
+       UNAVAILABLE_IN(TOOLTYPE_FILETRANSFER);\r
+       SAVEABLE(1);\r
+\r
+       filename = value;\r
+\r
+       cmdlen = cmdsize = 0;\r
+       command = NULL;\r
+       fp = fopen(filename, "r");\r
+       if (!fp) {\r
+           cmdline_error("unable to open command "\r
+                         "file \"%s\"", filename);\r
+           return ret;\r
+       }\r
+       do {\r
+           c = fgetc(fp);\r
+           d = c;\r
+           if (c == EOF)\r
+               d = 0;\r
+           if (cmdlen >= cmdsize) {\r
+               cmdsize = cmdlen + 512;\r
+               command = srealloc(command, cmdsize);\r
+           }\r
+           command[cmdlen++] = d;\r
+       } while (c != EOF);\r
+       cfg.remote_cmd_ptr = command;\r
+       cfg.remote_cmd_ptr2 = NULL;\r
+       cfg.nopty = TRUE;      /* command => no terminal */\r
+    }\r
+    if (!strcmp(p, "-P")) {\r
+       RETURN(2);\r
+       SAVEABLE(2);                   /* lower priority than -ssh,-telnet */\r
+       cfg.port = atoi(value);\r
+    }\r
+    if (!strcmp(p, "-pw")) {\r
+       RETURN(2);\r
+       cmdline_password = value;\r
+       ssh_get_line = cmdline_get_line;\r
+       ssh_getline_pw_only = TRUE;\r
+    }\r
+    return ret;                               /* unrecognised */\r
+}\r
+\r
+void cmdline_run_saved(void)\r
+{\r
+    int pri, i;\r
+    for (pri = 0; pri < NPRIORITIES; pri++)\r
+       for (i = 0; i < saves[pri].nsaved; i++)\r
+           cmdline_process_param(saves[pri].params[i].p,\r
+                                 saves[pri].params[i].value, 0);\r
+}\r
index 1078986..e49eaa0 100644 (file)
--- a/console.c
+++ b/console.c
@@ -261,27 +261,12 @@ void logevent(char *string)
 {
 }
 
-char *console_password = NULL;
-
 int console_get_line(const char *prompt, char *str,
                            int maxlen, int is_pw)
 {
     HANDLE hin, hout;
     DWORD savemode, newmode, i;
 
-    if (is_pw && console_password) {
-       static int tried_once = 0;
-
-       if (tried_once) {
-           return 0;
-       } else {
-           strncpy(str, console_password, maxlen);
-           str[maxlen - 1] = '\0';
-           tried_once = 1;
-           return 1;
-       }
-    }
-
     if (console_batch_mode) {
        if (maxlen > 0)
            str[0] = '\0';
diff --git a/plink.c b/plink.c
index a76570e..91484c6 100644 (file)
--- a/plink.c
+++ b/plink.c
@@ -40,6 +40,16 @@ void connection_fatal(char *p, ...)
     WSACleanup();
     cleanup_exit(1);
 }
+void cmdline_error(char *p, ...)
+{
+    va_list ap;
+    fprintf(stderr, "plink: ");
+    va_start(ap, p);
+    vfprintf(stderr, p, ap);
+    va_end(ap);
+    fputc('\n', stderr);
+    exit(1);
+}
 
 static char *password = NULL;
 
@@ -221,7 +231,6 @@ int main(int argc, char **argv)
     int skcount, sksize;
     int connopen;
     int exitcode;
-    char extra_portfwd[sizeof(cfg.portfwd)];
 
     ssh_get_line = console_get_line;
 
@@ -261,80 +270,18 @@ int main(int argc, char **argv)
     while (--argc) {
        char *p = *++argv;
        if (*p == '-') {
-           if (!strcmp(p, "-ssh")) {
-               default_protocol = cfg.protocol = PROT_SSH;
-               default_port = cfg.port = 22;
-           } else if (!strcmp(p, "-telnet")) {
-               default_protocol = cfg.protocol = PROT_TELNET;
-               default_port = cfg.port = 23;
-           } else if (!strcmp(p, "-rlogin")) {
-               default_protocol = cfg.protocol = PROT_RLOGIN;
-               default_port = cfg.port = 513;
-           } else if (!strcmp(p, "-raw")) {
-               default_protocol = cfg.protocol = PROT_RAW;
+           int ret = cmdline_process_param(p, (argc > 1 ? argv[1] : NULL), 1);
+           if (ret == -2) {
+               fprintf(stderr,
+                       "plink: option \"%s\" requires an argument\n", p);
+           } else if (ret == 2) {
+               --argc, ++argv;
+           } else if (ret == 1) {
+               continue;
            } else if (!strcmp(p, "-batch")) {
-               console_batch_mode = TRUE;
-           } else if (!strcmp(p, "-v")) {
-               flags |= FLAG_VERBOSE;
+               console_batch_mode = 1;
            } else if (!strcmp(p, "-log")) {
                logfile = "putty.log";
-           } else if (!strcmp(p, "-pw") && argc > 1) {
-               --argc, console_password = *++argv;
-           } else if (!strcmp(p, "-l") && argc > 1) {
-               char *username;
-               --argc, username = *++argv;
-               strncpy(cfg.username, username, sizeof(cfg.username));
-               cfg.username[sizeof(cfg.username) - 1] = '\0';
-           } else if ((!strcmp(p, "-L") || !strcmp(p, "-R")) && argc > 1) {
-               char *fwd, *ptr, *q;
-               int i=0;
-               --argc, fwd = *++argv;
-               ptr = extra_portfwd;
-               /* if multiple forwards, find end of list */
-               if (ptr[0]=='R' || ptr[0]=='L') {
-                   for (i = 0; i < sizeof(extra_portfwd) - 2; i++)
-                       if (ptr[i]=='\000' && ptr[i+1]=='\000')
-                           break;
-                   ptr = ptr + i + 1;  /* point to next forward slot */
-               }
-               ptr[0] = p[1];  /* insert a 'L' or 'R' at the start */
-               strncpy(ptr+1, fwd, sizeof(extra_portfwd) - i);
-               q = strchr(ptr, ':');
-               if (q) *q = '\t';      /* replace first : with \t */
-               ptr[strlen(ptr)+1] = '\000';    /* append two '\000' */
-               extra_portfwd[sizeof(extra_portfwd) - 1] = '\0';
-           } else if (!strcmp(p, "-m") && argc > 1) {
-               char *filename, *command;
-               int cmdlen, cmdsize;
-               FILE *fp;
-               int c, d;
-
-               --argc, filename = *++argv;
-
-               cmdlen = cmdsize = 0;
-               command = NULL;
-               fp = fopen(filename, "r");
-               if (!fp) {
-                   fprintf(stderr, "plink: unable to open command "
-                           "file \"%s\"\n", filename);
-                   return 1;
-               }
-               do {
-                   c = fgetc(fp);
-                   d = c;
-                   if (c == EOF)
-                       d = 0;
-                   if (cmdlen >= cmdsize) {
-                       cmdsize = cmdlen + 512;
-                       command = srealloc(command, cmdsize);
-                   }
-                   command[cmdlen++] = d;
-               } while (c != EOF);
-               cfg.remote_cmd_ptr = command;
-               cfg.remote_cmd_ptr2 = NULL;
-               cfg.nopty = TRUE;      /* command => no terminal */
-           } else if (!strcmp(p, "-P") && argc > 1) {
-               --argc, portnumber = atoi(*++argv);
            }
        } else if (*p) {
            if (!*cfg.host) {
@@ -482,6 +429,11 @@ int main(int argc, char **argv)
     }
 
     /*
+     * Perform command-line overrides on session configuration.
+     */
+    cmdline_run_saved();
+
+    /*
      * Trim a colon suffix off the hostname if it's there.
      */
     cfg.host[strcspn(cfg.host, ":")] = '\0';
@@ -509,30 +461,6 @@ int main(int argc, char **argv)
     }
 
     /*
-     * Add extra port forwardings (accumulated on command line) to
-     * cfg.
-     */
-    {
-       int i;
-       char *p;
-       p = extra_portfwd;
-       i = 0;
-       while (cfg.portfwd[i])
-           i += strlen(cfg.portfwd+i) + 1;
-       while (*p) {
-           if (strlen(p)+2 > sizeof(cfg.portfwd)-i) {
-               fprintf(stderr, "Internal fault: not enough space for all"
-                       " port forwardings\n");
-               break;
-           }
-           strncpy(cfg.portfwd+i, p, sizeof(cfg.portfwd)-i-1);
-           i += strlen(cfg.portfwd+i) + 1;
-           cfg.portfwd[i] = '\0';
-           p += strlen(p)+1;
-       }
-    }
-
-    /*
      * Select port.
      */
     if (portnumber != -1)
diff --git a/psftp.c b/psftp.c
index ae509f3..d498b07 100644 (file)
--- a/psftp.c
+++ b/psftp.c
@@ -1688,6 +1688,11 @@ static int psftp_connect(char *userhost, char *user, int portnumber)
     }
 
     /*
+     * Enact command-line overrides.
+     */
+    cmdline_run_saved();
+
+    /*
      * Trim leading whitespace off the hostname if it's there.
      */
     {
@@ -1789,6 +1794,17 @@ static int psftp_connect(char *userhost, char *user, int portnumber)
     return 0;
 }
 
+void cmdline_error(char *p, ...)
+{
+    va_list ap;
+    fprintf(stderr, "pscp: ");
+    va_start(ap, p);
+    vfprintf(stderr, p, ap);
+    va_end(ap);
+    fputc('\n', stderr);
+    exit(1);
+}
+
 /*
  * Main program. Parse arguments etc.
  */
@@ -1802,6 +1818,7 @@ int main(int argc, char *argv[])
     char *batchfile = NULL;
 
     flags = FLAG_STDERR | FLAG_INTERACTIVE;
+    cmdline_tooltype = TOOLTYPE_FILETRANSFER;
     ssh_get_line = &console_get_line;
     init_winsock();
     sk_init();
@@ -1809,29 +1826,35 @@ int main(int argc, char *argv[])
     userhost = user = NULL;
 
     for (i = 1; i < argc; i++) {
+       int ret;
        if (argv[i][0] != '-') {
-           if (userhost)
-               usage();
-           else
-               userhost = dupstr(argv[i]);
-       } else if (strcmp(argv[i], "-v") == 0) {
-           verbose = 1, flags |= FLAG_VERBOSE;
+            if (userhost)
+                usage();
+            else
+                userhost = dupstr(argv[i]);
+           continue;
+       }
+       ret = cmdline_process_param(argv[i], i+1<argc?argv[i+1]:NULL, 1);
+       if (ret == -2) {
+           cmdline_error("option \"%s\" requires an argument", argv[i]);
+       } else if (ret == 2) {
+           i++;               /* skip next argument */
+       } else if (ret == 1) {
+           /* We have our own verbosity in addition to `flags'. */
+           if (flags & FLAG_VERBOSE)
+               verbose = 1;
        } else if (strcmp(argv[i], "-h") == 0 ||
                   strcmp(argv[i], "-?") == 0) {
            usage();
        } else if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) {
            user = argv[++i];
-       } else if (strcmp(argv[i], "-P") == 0 && i + 1 < argc) {
-           portnumber = atoi(argv[++i]);
-       } else if (strcmp(argv[i], "-pw") == 0 && i + 1 < argc) {
-           console_password = argv[++i];
+       } else if (strcmp(argv[i], "-batch") == 0) {
+           console_batch_mode = 1;
        } else if (strcmp(argv[i], "-b") == 0 && i + 1 < argc) {
            mode = 1;
            batchfile = argv[++i];
        } else if (strcmp(argv[i], "-bc") == 0) {
            modeflags = modeflags | 1;
-       } else if (strcmp(argv[i], "-batch") == 0) {
-           console_batch_mode = TRUE;
        } else if (strcmp(argv[i], "-be") == 0) {
            modeflags = modeflags | 2;
        } else if (strcmp(argv[i], "--") == 0) {
diff --git a/putty.h b/putty.h
index e2e4d0d..9994b85 100644 (file)
--- a/putty.h
+++ b/putty.h
@@ -534,11 +534,14 @@ extern Backend rlogin_backend;
 extern Backend telnet_backend;
 
 /*
- * Exports from ssh.c.
+ * Exports from ssh.c. (NB the getline variables have to be GLOBAL
+ * so that PuTTYtel will still compile - otherwise it would depend
+ * on ssh.c.)
  */
 
-extern int (*ssh_get_line) (const char *prompt, char *str, int maxlen,
+GLOBAL int (*ssh_get_line) (const char *prompt, char *str, int maxlen,
                            int is_pw);
+GLOBAL int ssh_getline_pw_only;
 extern Backend ssh_backend;
 
 /*
@@ -616,7 +619,6 @@ int wc_unescape(char *output, const char *wildcard);
  * windlg.c).
  */
 extern int console_batch_mode;
-extern char *console_password;
 int console_get_line(const char *prompt, char *str, int maxlen, int is_pw);
 
 /*
@@ -631,4 +633,17 @@ printer_job *printer_start_job(char *printer);
 void printer_job_data(printer_job *, void *, int);
 void printer_finish_job(printer_job *);
 
+/*
+ * Exports from cmdline.c (and also cmdline_error(), which is
+ * defined differently in various places and required _by_
+ * cmdline.c).
+ */
+int cmdline_process_param(char *, char *, int);
+void cmdline_run_saved(void);
+extern char *cmdline_password;
+#define TOOLTYPE_FILETRANSFER 1
+extern int cmdline_tooltype;
+
+void cmdline_error(char *, ...);
+
 #endif
diff --git a/scp.c b/scp.c
index a11d53c..13b8080 100644 (file)
--- a/scp.c
+++ b/scp.c
@@ -453,6 +453,11 @@ static void do_cmd(char *host, char *user, char *cmd)
     }
 
     /*
+     * Enact command-line overrides.
+     */
+    cmdline_run_saved();
+
+    /*
      * Trim leading whitespace off the hostname if it's there.
      */
     {
@@ -2100,6 +2105,17 @@ static void usage(void)
     cleanup_exit(1);
 }
 
+void cmdline_error(char *p, ...)
+{
+    va_list ap;
+    fprintf(stderr, "pscp: ");
+    va_start(ap, p);
+    vfprintf(stderr, p, ap);
+    va_end(ap);
+    fputc('\n', stderr);
+    exit(1);
+}
+
 /*
  *  Main program (no, really?)
  */
@@ -2110,38 +2126,43 @@ int main(int argc, char *argv[])
     default_protocol = PROT_TELNET;
 
     flags = FLAG_STDERR;
+    cmdline_tooltype = TOOLTYPE_FILETRANSFER;
     ssh_get_line = &console_get_line;
     init_winsock();
     sk_init();
 
     for (i = 1; i < argc; i++) {
+       int ret;
        if (argv[i][0] != '-')
            break;
-       if (strcmp(argv[i], "-v") == 0)
-           verbose = 1, flags |= FLAG_VERBOSE;
-       else if (strcmp(argv[i], "-r") == 0)
+       ret = cmdline_process_param(argv[i], i+1<argc?argv[i+1]:NULL, 1);
+       if (ret == -2) {
+           cmdline_error("option \"%s\" requires an argument", argv[i]);
+       } else if (ret == 2) {
+           i++;               /* skip next argument */
+       } else if (ret == 1) {
+           /* We have our own verbosity in addition to `flags'. */
+           if (flags & FLAG_VERBOSE)
+               verbose = 1;
+       } else if (strcmp(argv[i], "-r") == 0) {
            recursive = 1;
-       else if (strcmp(argv[i], "-p") == 0)
+       } else if (strcmp(argv[i], "-p") == 0) {
            preserve = 1;
-       else if (strcmp(argv[i], "-q") == 0)
+       } else if (strcmp(argv[i], "-q") == 0) {
            statistics = 0;
-       else if (strcmp(argv[i], "-batch") == 0)
-           console_batch_mode = 1;
-       else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "-?") == 0)
+       } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "-?") == 0) {
            usage();
-       else if (strcmp(argv[i], "-P") == 0 && i + 1 < argc)
-           portnumber = atoi(argv[++i]);
-       else if (strcmp(argv[i], "-pw") == 0 && i + 1 < argc)
-           console_password = argv[++i];
-       else if (strcmp(argv[i], "-gui") == 0 && i + 1 < argc) {
+       } else if (strcmp(argv[i], "-gui") == 0 && i + 1 < argc) {
            gui_hwnd = argv[++i];
            gui_mode = 1;
            console_batch_mode = TRUE;
-       } else if (strcmp(argv[i], "-ls") == 0)
+        } else if (strcmp(argv[i], "-ls") == 0) {
            list = 1;
-       else if (strcmp(argv[i], "-unsafe") == 0)
+       } else if (strcmp(argv[i], "-batch") == 0) {
+           console_batch_mode = 1;
+       } else if (strcmp(argv[i], "-unsafe") == 0) {
            scp_unsafe_mode = 1;
-       else if (strcmp(argv[i], "--") == 0) {
+       else if (strcmp(argv[i], "--") == 0) {
            i++;
            break;
        } else
diff --git a/ssh.c b/ssh.c
index b727456..1885088 100644 (file)
--- a/ssh.c
+++ b/ssh.c
@@ -515,8 +515,6 @@ static const struct ssh_compress *sccomp = NULL;
 static const struct ssh_kex *kex = NULL;
 static const struct ssh_signkey *hostkey = NULL;
 static unsigned char ssh2_session_id[20];
-int (*ssh_get_line) (const char *prompt, char *str, int maxlen,
-                    int is_pw) = NULL;
 
 static char *savedhost;
 static int savedport;
@@ -2228,7 +2226,7 @@ static int do_ssh1_login(unsigned char *in, int inlen, int ispkt)
        static int pos = 0;
        static char c;
        if ((flags & FLAG_INTERACTIVE) && !*cfg.username) {
-           if (ssh_get_line) {
+           if (ssh_get_line && !ssh_getline_pw_only) {
                if (!ssh_get_line("login as: ",
                                  username, sizeof(username), FALSE)) {
                    /*
@@ -4098,7 +4096,7 @@ static void do_ssh2_authconn(unsigned char *in, int inlen, int ispkt)
             * it again.
             */
        } else if ((flags & FLAG_INTERACTIVE) && !*cfg.username) {
-           if (ssh_get_line) {
+           if (ssh_get_line && !ssh_getline_pw_only) {
                if (!ssh_get_line("login as: ",
                                  username, sizeof(username), FALSE)) {
                    /*
@@ -4327,6 +4325,7 @@ static void do_ssh2_authconn(unsigned char *in, int inlen, int ispkt)
                static int authed = FALSE;
                void *r;
 
+               ssh_pkt_ctx &= ~SSH2_PKTCTX_AUTH_MASK;
                ssh_pkt_ctx |= SSH2_PKTCTX_PUBLICKEY;
 
                tried_agent = TRUE;
@@ -4469,6 +4468,7 @@ static void do_ssh2_authconn(unsigned char *in, int inlen, int ispkt)
 
                tried_pubkey_config = TRUE;
 
+               ssh_pkt_ctx &= ~SSH2_PKTCTX_AUTH_MASK;
                ssh_pkt_ctx |= SSH2_PKTCTX_PUBLICKEY;
 
                /*
@@ -4523,6 +4523,7 @@ static void do_ssh2_authconn(unsigned char *in, int inlen, int ispkt)
                type = AUTH_TYPE_KEYBOARD_INTERACTIVE;
                tried_keyb_inter = TRUE;
 
+               ssh_pkt_ctx &= ~SSH2_PKTCTX_AUTH_MASK;
                ssh_pkt_ctx |= SSH2_PKTCTX_KBDINTER;
 
                ssh2_pkt_init(SSH2_MSG_USERAUTH_REQUEST);
@@ -4551,6 +4552,7 @@ static void do_ssh2_authconn(unsigned char *in, int inlen, int ispkt)
                type = AUTH_TYPE_KEYBOARD_INTERACTIVE;
                tried_keyb_inter = TRUE;
 
+               ssh_pkt_ctx &= ~SSH2_PKTCTX_AUTH_MASK;
                ssh_pkt_ctx |= SSH2_PKTCTX_KBDINTER;
 
                if (curr_prompt == 0) {
@@ -4601,6 +4603,7 @@ static void do_ssh2_authconn(unsigned char *in, int inlen, int ispkt)
 
            if (!method && can_passwd) {
                method = AUTH_PASSWORD;
+               ssh_pkt_ctx &= ~SSH2_PKTCTX_AUTH_MASK;
                ssh_pkt_ctx |= SSH2_PKTCTX_PASSWORD;
                sprintf(pwprompt, "%.90s@%.90s's password: ", username,
                        savedhost);
index 10b456a..987a845 100644 (file)
--- a/window.c
+++ b/window.c
@@ -256,6 +256,7 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show)
      */
     {
        char *p;
+       int got_host = 0;
 
        default_protocol = DEFAULT_PROTOCOL;
        default_port = DEFAULT_PORT;
@@ -264,54 +265,16 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show)
        do_defaults(NULL, &cfg);
 
        p = cmdline;
-       while (*p && isspace(*p))
-           p++;
 
        /*
-        * Process command line options first. Yes, this can be
-        * done better, and it will be as soon as I have the
-        * energy...
+        * Process a couple of command-line options which are more
+        * easily dealt with before the line is broken up into
+        * words. These are the soon-to-be-defunct @sessionname and
+        * the internal-use-only &sharedmemoryhandle, neither of
+        * which are combined with anything else.
         */
-       while (*p == '-') {
-           char *q = p + strcspn(p, " \t");
+       while (*p && isspace(*p))
            p++;
-           if (q == p + 3 &&
-               tolower(p[0]) == 's' &&
-               tolower(p[1]) == 's' && tolower(p[2]) == 'h') {
-               default_protocol = cfg.protocol = PROT_SSH;
-               default_port = cfg.port = 22;
-           } else if (q == p + 7 &&
-                      tolower(p[0]) == 'c' &&
-                      tolower(p[1]) == 'l' &&
-                      tolower(p[2]) == 'e' &&
-                      tolower(p[3]) == 'a' &&
-                      tolower(p[4]) == 'n' &&
-                      tolower(p[5]) == 'u' && tolower(p[6]) == 'p') {
-               /*
-                * `putty -cleanup'. Remove all registry entries
-                * associated with PuTTY, and also find and delete
-                * the random seed file.
-                */
-               if (MessageBox(NULL,
-                              "This procedure will remove ALL Registry\n"
-                              "entries associated with PuTTY, and will\n"
-                              "also remove the PuTTY random seed file.\n"
-                              "\n"
-                              "THIS PROCESS WILL DESTROY YOUR SAVED\n"
-                              "SESSIONS. Are you really sure you want\n"
-                              "to continue?",
-                              "PuTTY Warning",
-                              MB_YESNO | MB_ICONWARNING) == IDYES) {
-                   cleanup_all();
-               }
-               exit(0);
-           }
-           p = q + strspn(q, " \t");
-       }
-
-       /*
-        * An initial @ means to activate a saved session.
-        */
        if (*p == '@') {
            int i = strlen(p);
            while (i > 1 && isspace(p[i - 1]))
@@ -341,53 +304,107 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show)
                WSACleanup();
                return 0;
            }
-       } else if (*p) {
-           char *q = p;
+       } else {
            /*
-            * If the hostname starts with "telnet:", set the
-            * protocol to Telnet and process the string as a
-            * Telnet URL.
+            * Otherwise, break up the command line and deal with
+            * it sensibly.
             */
-           if (!strncmp(q, "telnet:", 7)) {
-               char c;
-
-               q += 7;
-               if (q[0] == '/' && q[1] == '/')
-                   q += 2;
-               cfg.protocol = PROT_TELNET;
-               p = q;
-               while (*p && *p != ':' && *p != '/')
-                   p++;
-               c = *p;
-               if (*p)
-                   *p++ = '\0';
-               if (c == ':')
-                   cfg.port = atoi(p);
-               else
-                   cfg.port = -1;
-               strncpy(cfg.host, q, sizeof(cfg.host) - 1);
-               cfg.host[sizeof(cfg.host) - 1] = '\0';
-           } else {
-               while (*p && !isspace(*p))
-                   p++;
-               if (*p)
-                   *p++ = '\0';
-               strncpy(cfg.host, q, sizeof(cfg.host) - 1);
-               cfg.host[sizeof(cfg.host) - 1] = '\0';
-               while (*p && isspace(*p))
-                   p++;
-               if (*p)
-                   cfg.port = atoi(p);
-               else
-                   cfg.port = -1;
-           }
-       } else {
-           if (!do_config()) {
-               WSACleanup();
-               return 0;
+           int argc, i;
+           char **argv;
+           
+           split_into_argv(cmdline, &argc, &argv);
+
+           for (i = 0; i < argc; i++) {
+               char *p = argv[i];
+               int ret;
+
+               ret = cmdline_process_param(p, i+1<argc?argv[i+1]:NULL, 1);
+               if (ret == -2) {
+                   cmdline_error("option \"%s\" requires an argument", p);
+               } else if (ret == 2) {
+                   i++;               /* skip next argument */
+               } else if (ret == 1) {
+                   continue;          /* nothing further needs doing */
+               } else if (!strcmp(p, "-cleanup")) {
+                   /*
+                    * `putty -cleanup'. Remove all registry
+                    * entries associated with PuTTY, and also find
+                    * and delete the random seed file.
+                    */
+                   if (MessageBox(NULL,
+                                  "This procedure will remove ALL Registry\n"
+                                  "entries associated with PuTTY, and will\n"
+                                  "also remove the PuTTY random seed file.\n"
+                                  "\n"
+                                  "THIS PROCESS WILL DESTROY YOUR SAVED\n"
+                                  "SESSIONS. Are you really sure you want\n"
+                                  "to continue?",
+                                  "PuTTY Warning",
+                                  MB_YESNO | MB_ICONWARNING) == IDYES) {
+                       cleanup_all();
+                   }
+                   exit(0);
+               } else if (*p != '-') {
+                   char *q = p;
+                   if (got_host) {
+                       /*
+                        * If we already have a host name, treat
+                        * this argument as a port number. NB we
+                        * have to treat this as a saved -P
+                        * argument, so that it will be deferred
+                        * until it's a good moment to run it.
+                        */
+                       int ret = cmdline_process_param("-P", p, 1);
+                       assert(ret == 2);
+                   } else if (!strncmp(q, "telnet:", 7)) {
+                       /*
+                        * If the hostname starts with "telnet:",
+                        * set the protocol to Telnet and process
+                        * the string as a Telnet URL.
+                        */
+                       char c;
+
+                       q += 7;
+                       if (q[0] == '/' && q[1] == '/')
+                           q += 2;
+                       cfg.protocol = PROT_TELNET;
+                       p = q;
+                       while (*p && *p != ':' && *p != '/')
+                           p++;
+                       c = *p;
+                       if (*p)
+                           *p++ = '\0';
+                       if (c == ':')
+                           cfg.port = atoi(p);
+                       else
+                           cfg.port = -1;
+                       strncpy(cfg.host, q, sizeof(cfg.host) - 1);
+                       cfg.host[sizeof(cfg.host) - 1] = '\0';
+                       got_host = 1;
+                   } else {
+                       /*
+                        * Otherwise, treat this argument as a host
+                        * name.
+                        */
+                       while (*p && !isspace(*p))
+                           p++;
+                       if (*p)
+                           *p++ = '\0';
+                       strncpy(cfg.host, q, sizeof(cfg.host) - 1);
+                       cfg.host[sizeof(cfg.host) - 1] = '\0';
+                       got_host = 1;
+                   }
+               }
            }
        }
 
+       cmdline_run_saved();
+
+       if (!*cfg.host && !do_config()) {
+           WSACleanup();
+           return 0;
+       }
+
        /*
         * Trim leading whitespace off the hostname if it's there.
         */
@@ -844,6 +861,21 @@ void connection_fatal(char *fmt, ...)
 }
 
 /*
+ * Report an error at the command-line parsing stage.
+ */
+void cmdline_error(char *fmt, ...)
+{
+    va_list ap;
+    char stuff[200];
+
+    va_start(ap, fmt);
+    vsprintf(stuff, fmt, ap);
+    va_end(ap);
+    MessageBox(hwnd, stuff, "PuTTY Command Line Error", MB_ICONERROR | MB_OK);
+    exit(1);
+}
+
+/*
  * Actually do the job requested by a WM_NETEVENT
  */
 static void enact_pending_netevent(void)
index 21560ba..240dcb7 100644 (file)
@@ -30,6 +30,11 @@ struct ctlpos {
 };
 
 /*
+ * Exports from winutils.c.
+ */
+void split_into_argv(char *, int *, char ***);
+
+/*
  * Private structure for prefslist state. Only in the header file
  * so that we can delegate allocation to callers.
  */
diff --git a/winutils.c b/winutils.c
new file mode 100644 (file)
index 0000000..f47eb1b
--- /dev/null
@@ -0,0 +1,434 @@
+/*
+ * winutils.c: miscellaneous Windows utilities
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#define lenof(x) ( sizeof((x)) / sizeof(*(x)) )
+
+#ifdef TESTMODE
+/* Definitions to allow this module to be compiled standalone for testing. */
+#define smalloc malloc
+#endif
+
+/*
+ * Split a complete command line into argc/argv, attempting to do
+ * it exactly the same way Windows itself would do it (so that
+ * console utilities, which receive argc and argv from Windows,
+ * will have their command lines processed in the same way as GUI
+ * utilities which get a whole command line and must break it
+ * themselves).
+ * 
+ * Does not modify the input command line (just in case).
+ */
+void split_into_argv(const char *cmdline, int *argc, char ***argv)
+{
+    const char *p;
+    char *outputline, *q;
+    char **outputargv;
+    int outputargc;
+
+    /*
+     * At first glance the rules appeared to be:
+     *
+     *  - Single quotes are not special characters.
+     *
+     *  - Double quotes are removed, but within them spaces cease
+     *    to be special.
+     *
+     *  - Backslashes are _only_ special when a sequence of them
+     *    appear just before a double quote. In this situation,
+     *    they are treated like C backslashes: so \" just gives a
+     *    literal quote, \\" gives a literal backslash and then
+     *    opens or closes a double-quoted segment, \\\" gives a
+     *    literal backslash and then a literal quote, \\\\" gives
+     *    two literal backslashes and then opens/closes a
+     *    double-quoted segment, and so forth. Note that this
+     *    behaviour is identical inside and outside double quotes.
+     *
+     *  - Two successive double quotes become one literal double
+     *    quote, but only _inside_ a double-quoted segment.
+     *    Outside, they just form an empty double-quoted segment
+     *    (which may cause an empty argument word).
+     *
+     *  - That only leaves the interesting question of what happens
+     *    when one or more backslashes precedes two or more double
+     *    quotes, starting inside a double-quoted string. And the
+     *    answer to that appears somewhat bizarre. Here I tabulate
+     *    number of backslashes (across the top) against number of
+     *    quotes (down the left), and indicate how many backslashes
+     *    are output, how many quotes are output, and whether a
+     *    quoted segment is open at the end of the sequence:
+     * 
+     *                      backslashes
+     * 
+     *               0         1      2      3      4
+     * 
+     *         0   0,0,y  |  1,0,y  2,0,y  3,0,y  4,0,y
+     *            --------+-----------------------------
+     *         1   0,0,n  |  0,1,y  1,0,n  1,1,y  2,0,n
+     *    q    2   0,1,n  |  0,1,n  1,1,n  1,1,n  2,1,n
+     *    u    3   0,1,y  |  0,2,n  1,1,y  1,2,n  2,1,y
+     *    o    4   0,1,n  |  0,2,y  1,1,n  1,2,y  2,1,n
+     *    t    5   0,2,n  |  0,2,n  1,2,n  1,2,n  2,2,n
+     *    e    6   0,2,y  |  0,3,n  1,2,y  1,3,n  2,2,y
+     *    s    7   0,2,n  |  0,3,y  1,2,n  1,3,y  2,2,n
+     *         8   0,3,n  |  0,3,n  1,3,n  1,3,n  2,3,n
+     *         9   0,3,y  |  0,4,n  1,3,y  1,4,n  2,3,y
+     *        10   0,3,n  |  0,4,y  1,3,n  1,4,y  2,3,n
+     *        11   0,4,n  |  0,4,n  1,4,n  1,4,n  2,4,n
+     * 
+     * 
+     *      [Test fragment was of the form "a\\\"""b c" d.]
+     * 
+     * There is very weird mod-3 behaviour going on here in the
+     * number of quotes, and it even applies when there aren't any
+     * backslashes! How ghastly.
+     * 
+     * With a bit of thought, this extremely odd diagram suddenly
+     * coalesced itself into a coherent, if still ghastly, model of
+     * how things work:
+     * 
+     *  - As before, backslashes are only special when one or more
+     *    of them appear contiguously before at least one double
+     *    quote. In this situation the backslashes do exactly what
+     *    you'd expect: each one quotes the next thing in front of
+     *    it, so you end up with n/2 literal backslashes (if n is
+     *    even) or (n-1)/2 literal backslashes and a literal quote
+     *    (if n is odd). In the latter case the double quote
+     *    character right after the backslashes is used up.
+     * 
+     *  - After that, any remaining double quotes are processed. A
+     *    string of contiguous unescaped double quotes has a mod-3
+     *    behaviour:
+     * 
+     *     * inside a quoted segment, a quote ends the segment.
+     *     * _immediately_ after ending a quoted segment, a quote
+     *       simply produces a literal quote.
+     *     * otherwise, outside a quoted segment, a quote begins a
+     *       quoted segment.
+     * 
+     *    So, for example, if we started inside a quoted segment
+     *    then two contiguous quotes would close the segment and
+     *    produce a literal quote; three would close the segment,
+     *    produce a literal quote, and open a new segment. If we
+     *    started outside a quoted segment, then two contiguous
+     *    quotes would open and then close a segment, producing no
+     *    output (but potentially creating a zero-length argument);
+     *    but three quotes would open and close a segment and then
+     *    produce a literal quote.
+     */
+
+    /*
+     * This will guaranteeably be big enough; we can realloc it
+     * down later.
+     */
+    outputline = malloc(1+strlen(cmdline));
+    outputargv = malloc(sizeof(char *) * (strlen(cmdline)+1 / 2));
+
+    p = cmdline; q = outputline; outputargc = 0;
+
+    while (*p) {
+       int quote;
+
+       /* Skip whitespace searching for start of argument. */
+       while (*p && isspace(*p)) p++;
+       if (!*p) break;
+
+       /* We have an argument; start it. */
+       outputargv[outputargc++] = q;
+       quote = 0;
+
+       /* Copy data into the argument until it's finished. */
+       while (*p) {
+           if (!quote && isspace(*p))
+               break;                 /* argument is finished */
+
+           if (*p == '"' || *p == '\\') {
+               /*
+                * We have a sequence of zero or more backslashes
+                * followed by a sequence of zero or more quotes.
+                * Count up how many of each, and then deal with
+                * them as appropriate.
+                */
+               int i, slashes = 0, quotes = 0;
+               while (*p == '\\') slashes++, p++;
+               while (*p == '"') quotes++, p++;
+
+               if (!quotes) {
+                   /*
+                    * Special case: if there are no quotes,
+                    * slashes are not special at all, so just copy
+                    * n slashes to the output string.
+                    */
+                   while (slashes--) *q++ = '\\';
+               } else {
+                   /* Slashes annihilate in pairs. */
+                   while (slashes >= 2) slashes -= 2, *q++ = '\\';
+
+                   /* One remaining slash takes out the first quote. */
+                   if (slashes) quotes--, *q++ = '"';
+
+                   if (quotes > 0) {
+                       /* Outside a quote segment, a quote starts one. */
+                       if (!quote) quotes--, quote = 1;
+
+                       /* Now we produce (n+1)/3 literal quotes... */
+                       for (i = 3; i <= quotes+1; i += 3) *q++ = '"';
+
+                       /* ... and end in a quote segment iff 3 divides n. */
+                       quote = (quotes % 3 == 0);
+                   }
+               }
+           } else {
+               *q++ = *p++;
+           }
+       }
+
+       /* At the end of an argument, just append a trailing NUL. */
+       *q++ = '\0';
+    }
+
+    outputargv = realloc(outputargv, sizeof(char *) * outputargc);
+
+    if (argc) *argc = outputargc;
+    if (argv) *argv = outputargv;
+}
+
+#ifdef TESTMODE
+
+const struct argv_test {
+    const char *cmdline;
+    const char *argv[10];
+} argv_tests[] = {
+    /*
+     * We generate this set of tests by invoking ourself with
+     * `-generate'.
+     */
+    {"ab c\" d", {"ab", "c d", NULL}},
+    {"a\"b c\" d", {"ab c", "d", NULL}},
+    {"a\"\"b c\" d", {"ab", "c d", NULL}},
+    {"a\"\"\"b c\" d", {"a\"b", "c d", NULL}},
+    {"a\"\"\"\"b c\" d", {"a\"b c", "d", NULL}},
+    {"a\"\"\"\"\"b c\" d", {"a\"b", "c d", NULL}},
+    {"a\"\"\"\"\"\"b c\" d", {"a\"\"b", "c d", NULL}},
+    {"a\"\"\"\"\"\"\"b c\" d", {"a\"\"b c", "d", NULL}},
+    {"a\"\"\"\"\"\"\"\"b c\" d", {"a\"\"b", "c d", NULL}},
+    {"a\\b c\" d", {"a\\b", "c d", NULL}},
+    {"a\\\"b c\" d", {"a\"b", "c d", NULL}},
+    {"a\\\"\"b c\" d", {"a\"b c", "d", NULL}},
+    {"a\\\"\"\"b c\" d", {"a\"b", "c d", NULL}},
+    {"a\\\"\"\"\"b c\" d", {"a\"\"b", "c d", NULL}},
+    {"a\\\"\"\"\"\"b c\" d", {"a\"\"b c", "d", NULL}},
+    {"a\\\"\"\"\"\"\"b c\" d", {"a\"\"b", "c d", NULL}},
+    {"a\\\"\"\"\"\"\"\"b c\" d", {"a\"\"\"b", "c d", NULL}},
+    {"a\\\"\"\"\"\"\"\"\"b c\" d", {"a\"\"\"b c", "d", NULL}},
+    {"a\\\\b c\" d", {"a\\\\b", "c d", NULL}},
+    {"a\\\\\"b c\" d", {"a\\b c", "d", NULL}},
+    {"a\\\\\"\"b c\" d", {"a\\b", "c d", NULL}},
+    {"a\\\\\"\"\"b c\" d", {"a\\\"b", "c d", NULL}},
+    {"a\\\\\"\"\"\"b c\" d", {"a\\\"b c", "d", NULL}},
+    {"a\\\\\"\"\"\"\"b c\" d", {"a\\\"b", "c d", NULL}},
+    {"a\\\\\"\"\"\"\"\"b c\" d", {"a\\\"\"b", "c d", NULL}},
+    {"a\\\\\"\"\"\"\"\"\"b c\" d", {"a\\\"\"b c", "d", NULL}},
+    {"a\\\\\"\"\"\"\"\"\"\"b c\" d", {"a\\\"\"b", "c d", NULL}},
+    {"a\\\\\\b c\" d", {"a\\\\\\b", "c d", NULL}},
+    {"a\\\\\\\"b c\" d", {"a\\\"b", "c d", NULL}},
+    {"a\\\\\\\"\"b c\" d", {"a\\\"b c", "d", NULL}},
+    {"a\\\\\\\"\"\"b c\" d", {"a\\\"b", "c d", NULL}},
+    {"a\\\\\\\"\"\"\"b c\" d", {"a\\\"\"b", "c d", NULL}},
+    {"a\\\\\\\"\"\"\"\"b c\" d", {"a\\\"\"b c", "d", NULL}},
+    {"a\\\\\\\"\"\"\"\"\"b c\" d", {"a\\\"\"b", "c d", NULL}},
+    {"a\\\\\\\"\"\"\"\"\"\"b c\" d", {"a\\\"\"\"b", "c d", NULL}},
+    {"a\\\\\\\"\"\"\"\"\"\"\"b c\" d", {"a\\\"\"\"b c", "d", NULL}},
+    {"a\\\\\\\\b c\" d", {"a\\\\\\\\b", "c d", NULL}},
+    {"a\\\\\\\\\"b c\" d", {"a\\\\b c", "d", NULL}},
+    {"a\\\\\\\\\"\"b c\" d", {"a\\\\b", "c d", NULL}},
+    {"a\\\\\\\\\"\"\"b c\" d", {"a\\\\\"b", "c d", NULL}},
+    {"a\\\\\\\\\"\"\"\"b c\" d", {"a\\\\\"b c", "d", NULL}},
+    {"a\\\\\\\\\"\"\"\"\"b c\" d", {"a\\\\\"b", "c d", NULL}},
+    {"a\\\\\\\\\"\"\"\"\"\"b c\" d", {"a\\\\\"\"b", "c d", NULL}},
+    {"a\\\\\\\\\"\"\"\"\"\"\"b c\" d", {"a\\\\\"\"b c", "d", NULL}},
+    {"a\\\\\\\\\"\"\"\"\"\"\"\"b c\" d", {"a\\\\\"\"b", "c d", NULL}},
+    {"\"ab c\" d", {"ab c", "d", NULL}},
+    {"\"a\"b c\" d", {"ab", "c d", NULL}},
+    {"\"a\"\"b c\" d", {"a\"b", "c d", NULL}},
+    {"\"a\"\"\"b c\" d", {"a\"b c", "d", NULL}},
+    {"\"a\"\"\"\"b c\" d", {"a\"b", "c d", NULL}},
+    {"\"a\"\"\"\"\"b c\" d", {"a\"\"b", "c d", NULL}},
+    {"\"a\"\"\"\"\"\"b c\" d", {"a\"\"b c", "d", NULL}},
+    {"\"a\"\"\"\"\"\"\"b c\" d", {"a\"\"b", "c d", NULL}},
+    {"\"a\"\"\"\"\"\"\"\"b c\" d", {"a\"\"\"b", "c d", NULL}},
+    {"\"a\\b c\" d", {"a\\b c", "d", NULL}},
+    {"\"a\\\"b c\" d", {"a\"b c", "d", NULL}},
+    {"\"a\\\"\"b c\" d", {"a\"b", "c d", NULL}},
+    {"\"a\\\"\"\"b c\" d", {"a\"\"b", "c d", NULL}},
+    {"\"a\\\"\"\"\"b c\" d", {"a\"\"b c", "d", NULL}},
+    {"\"a\\\"\"\"\"\"b c\" d", {"a\"\"b", "c d", NULL}},
+    {"\"a\\\"\"\"\"\"\"b c\" d", {"a\"\"\"b", "c d", NULL}},
+    {"\"a\\\"\"\"\"\"\"\"b c\" d", {"a\"\"\"b c", "d", NULL}},
+    {"\"a\\\"\"\"\"\"\"\"\"b c\" d", {"a\"\"\"b", "c d", NULL}},
+    {"\"a\\\\b c\" d", {"a\\\\b c", "d", NULL}},
+    {"\"a\\\\\"b c\" d", {"a\\b", "c d", NULL}},
+    {"\"a\\\\\"\"b c\" d", {"a\\\"b", "c d", NULL}},
+    {"\"a\\\\\"\"\"b c\" d", {"a\\\"b c", "d", NULL}},
+    {"\"a\\\\\"\"\"\"b c\" d", {"a\\\"b", "c d", NULL}},
+    {"\"a\\\\\"\"\"\"\"b c\" d", {"a\\\"\"b", "c d", NULL}},
+    {"\"a\\\\\"\"\"\"\"\"b c\" d", {"a\\\"\"b c", "d", NULL}},
+    {"\"a\\\\\"\"\"\"\"\"\"b c\" d", {"a\\\"\"b", "c d", NULL}},
+    {"\"a\\\\\"\"\"\"\"\"\"\"b c\" d", {"a\\\"\"\"b", "c d", NULL}},
+    {"\"a\\\\\\b c\" d", {"a\\\\\\b c", "d", NULL}},
+    {"\"a\\\\\\\"b c\" d", {"a\\\"b c", "d", NULL}},
+    {"\"a\\\\\\\"\"b c\" d", {"a\\\"b", "c d", NULL}},
+    {"\"a\\\\\\\"\"\"b c\" d", {"a\\\"\"b", "c d", NULL}},
+    {"\"a\\\\\\\"\"\"\"b c\" d", {"a\\\"\"b c", "d", NULL}},
+    {"\"a\\\\\\\"\"\"\"\"b c\" d", {"a\\\"\"b", "c d", NULL}},
+    {"\"a\\\\\\\"\"\"\"\"\"b c\" d", {"a\\\"\"\"b", "c d", NULL}},
+    {"\"a\\\\\\\"\"\"\"\"\"\"b c\" d", {"a\\\"\"\"b c", "d", NULL}},
+    {"\"a\\\\\\\"\"\"\"\"\"\"\"b c\" d", {"a\\\"\"\"b", "c d", NULL}},
+    {"\"a\\\\\\\\b c\" d", {"a\\\\\\\\b c", "d", NULL}},
+    {"\"a\\\\\\\\\"b c\" d", {"a\\\\b", "c d", NULL}},
+    {"\"a\\\\\\\\\"\"b c\" d", {"a\\\\\"b", "c d", NULL}},
+    {"\"a\\\\\\\\\"\"\"b c\" d", {"a\\\\\"b c", "d", NULL}},
+    {"\"a\\\\\\\\\"\"\"\"b c\" d", {"a\\\\\"b", "c d", NULL}},
+    {"\"a\\\\\\\\\"\"\"\"\"b c\" d", {"a\\\\\"\"b", "c d", NULL}},
+    {"\"a\\\\\\\\\"\"\"\"\"\"b c\" d", {"a\\\\\"\"b c", "d", NULL}},
+    {"\"a\\\\\\\\\"\"\"\"\"\"\"b c\" d", {"a\\\\\"\"b", "c d", NULL}},
+    {"\"a\\\\\\\\\"\"\"\"\"\"\"\"b c\" d", {"a\\\\\"\"\"b", "c d", NULL}},
+};
+
+int main(int argc, char **argv)
+{
+    int i, j;
+
+    if (argc > 1) {
+       /*
+        * Generation of tests.
+        * 
+        * Given `-splat <args>', we print out a C-style
+        * representation of each argument (in the form "a", "b",
+        * NULL), backslash-escaping each backslash and double
+        * quote.
+        * 
+        * Given `-split <string>', we first doctor `string' by
+        * turning forward slashes into backslashes, single quotes
+        * into double quotes and underscores into spaces; and then
+        * we feed the resulting string to ourself with `-splat'.
+        * 
+        * Given `-generate', we concoct a variety of fun test
+        * cases, encode them in quote-safe form (mapping \, " and
+        * space to /, ' and _ respectively) and feed each one to
+        * `-split'.
+        */
+       if (!strcmp(argv[1], "-splat")) {
+           int i;
+           char *p;
+           for (i = 2; i < argc; i++) {
+               putchar('"');
+               for (p = argv[i]; *p; p++) {
+                   if (*p == '\\' || *p == '"')
+                       putchar('\\');
+                   putchar(*p);
+               }
+               printf("\", ");
+           }
+           printf("NULL");
+           return 0;
+       }
+
+       if (!strcmp(argv[1], "-split") && argc > 2) {
+           char *str = malloc(20 + strlen(argv[0]) + strlen(argv[2]));
+           char *p, *q;
+
+           q = str + sprintf(str, "%s -splat ", argv[0]);
+           printf("    {\"");
+           for (p = argv[2]; *p; p++, q++) {
+               switch (*p) {
+                 case '/':  printf("\\\\"); *q = '\\'; break;
+                 case '\'': printf("\\\""); *q = '"';  break;
+                 case '_':  printf(" ");    *q = ' ';  break;
+                 default:   putchar(*p);    *q = *p;   break;
+               }
+           }
+           *p = '\0';
+           printf("\", {");
+           fflush(stdout);
+
+           system(str);
+
+           printf("}},\n");
+
+           return 0;
+       }
+
+       if (!strcmp(argv[1], "-generate")) {
+           char *teststr, *p;
+           int i, initialquote, backslashes, quotes;
+
+           teststr = malloc(200 + strlen(argv[0]));
+
+           for (initialquote = 0; initialquote <= 1; initialquote++) {
+               for (backslashes = 0; backslashes < 5; backslashes++) {
+                   for (quotes = 0; quotes < 9; quotes++) {
+                       p = teststr + sprintf(teststr, "%s -split ", argv[0]);
+                       if (initialquote) *p++ = '\'';
+                       *p++ = 'a';
+                       for (i = 0; i < backslashes; i++) *p++ = '/';
+                       for (i = 0; i < quotes; i++) *p++ = '\'';
+                       *p++ = 'b';
+                       *p++ = '_';
+                       *p++ = 'c';
+                       *p++ = '\'';
+                       *p++ = '_';
+                       *p++ = 'd';
+                       *p = '\0';
+
+                       system(teststr);
+                   }
+               }
+           }
+           return 0;
+       }
+
+       fprintf(stderr, "unrecognised option: \"%s\"\n", argv[1]);
+       return 1;
+    }
+
+    /*
+     * If we get here, we were invoked with no arguments, so just
+     * run the tests.
+     */
+
+    for (i = 0; i < lenof(argv_tests); i++) {
+       int ac;
+       char **av;
+
+       split_into_argv(argv_tests[i].cmdline, &ac, &av);
+
+       for (j = 0; j < ac && argv_tests[i].argv[j]; j++) {
+           if (strcmp(av[j], argv_tests[i].argv[j])) {
+               printf("failed test %d (|%s|) arg %d: |%s| should be |%s|\n",
+                      i, argv_tests[i].cmdline,
+                      j, av[j], argv_tests[i].argv[j]);
+           }
+#ifdef VERBOSE
+           else {
+               printf("test %d (|%s|) arg %d: |%s| == |%s|\n",
+                      i, argv_tests[i].cmdline,
+                      j, av[j], argv_tests[i].argv[j]);
+           }
+#endif
+       }
+       if (j < ac)
+           printf("failed test %d (|%s|): %d args returned, should be %d\n",
+                  i, argv_tests[i].cmdline, ac, j);
+       if (argv_tests[i].argv[j])
+           printf("failed test %d (|%s|): %d args returned, should be more\n",
+                  i, argv_tests[i].cmdline, ac);
+    }
+
+    return 0;
+}
+
+#endif
\ No newline at end of file