]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Migration to dispatched meta-commands done
authorlarrybr <larrybr@noemail.net>
Sat, 10 Jul 2021 03:42:25 +0000 (03:42 +0000)
committerlarrybr <larrybr@noemail.net>
Sat, 10 Jul 2021 03:42:25 +0000 (03:42 +0000)
FossilOrigin-Name: ac4267da196020b41f736b72e8ddc97e8e83be5e97e23caabd107a5a6d832921

manifest
manifest.uuid
src/shell.c.in
tool/mkshellc.tcl

index 78bb53122211ea8c48ea073604b43b23509a433e..880d94f6f9486b860b6c75c24198c9d02d86ecc0 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C General\scleanup\sof\smkshellc.tcl,\s+\shelp\sand\soptions,\sdecruft
-D 2021-07-07T18:43:01.981
+C Migration\sto\sdispatched\smeta-commands\sdone
+D 2021-07-10T03:42:25.739
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -545,7 +545,7 @@ F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c
 F src/resolve.c b379c5ffe3b692e9c64fa37817cc0efa204b7c9468a818309dde85fd132d9d81
 F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92
 F src/select.c 4fa607bab6bcc580f12dbaf9c800b2250a1e408f10321a1d3bcb1dd30c447e62
-F src/shell.c.in ce74a51f548a268436cba07b0082c9b36161ac7f8bdc4c476f88f2d506ba248c
+F src/shell.c.in e295faf6461b1f8051bc9cbeaf6af32479d8d760bdf6abf634dc3f9223ab8ced
 F src/sqlite.h.in ecf5aa981da30c33da3e9f353bf3ebf055d3c380c80d6a4f954e58d18ccd6df1
 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8
 F src/sqlite3ext.h e97f4e9b509408fea4c4e9bef5a41608dfac343b4d3c7a990dedde1e19af9510
@@ -1853,7 +1853,7 @@ F tool/mkopcodec.tcl d1b6362bd3aa80d5520d4d6f3765badf01f6c43c
 F tool/mkopcodeh.tcl 130b88697da6ec5b89b41844d955d08fb62c2552e889dec8c7bcecb28d8f50bd
 F tool/mkopts.tcl 680f785fdb09729fd9ac50632413da4eadbdf9071535e3f26d03795828ab07fa
 F tool/mkpragmatab.tcl ae5585ae76ca26e4d6ccd5ea9cdebaf5efefb318bf989497a0e846cd711d9ab1
-F tool/mkshellc.tcl 6ceab05bff08e395ae578569e549c3dd233cd37a522b443db85fd343c75544bb
+F tool/mkshellc.tcl 8d154d752af008add2ae41d6bff4753d275e8d457620adde7d09380010bf0e67
 F tool/mksourceid.c 36aa8020014aed0836fd13c51d6dc9219b0df1761d6b5f58ff5b616211b079b9
 F tool/mkspeedsql.tcl a1a334d288f7adfe6e996f2e712becf076745c97
 F tool/mksqlite3c-noext.tcl 4f7cfef5152b0c91920355cbfc1d608a4ad242cb819f1aea07f6d0274f584a7f
@@ -1919,7 +1919,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P d1952ff42b26e66d8df45dcf7ba37378d0818b1db3d6df9352db1e30b9ca844e
-R 270aae5b4c33c9f46f9515239be09688
+P 8bdd5fbf127e886d4d8dad2775c37d6591e8e24916250774f04dc8cf5951a8a9
+Q +eb8af9a494fb68c0a1c600b3ac71467645b51b296fc6e2116d7d855319d59a59
+R f6369e9d4f17df61dcbdb205a971c4d7
 U larrybr
-Z 2e037882aa30e7c815ca24758c397b3b
+Z 50d34173cba64575d4cc49626deb4b67
index 9eab40ff3709e62c8b0ea27608d5efb7de38e00f..2ab2bf2d5109bfed5f1b8e7e799ce31ba2a44e4c 100644 (file)
@@ -1 +1 @@
-8bdd5fbf127e886d4d8dad2775c37d6591e8e24916250774f04dc8cf5951a8a9
\ No newline at end of file
+ac4267da196020b41f736b72e8ddc97e8e83be5e97e23caabd107a5a6d832921
\ No newline at end of file
index f4312d3508bb012534cd2f883b708f5f47d3b058..be3d9ed03bf47a7a4b9eb72e6933989cc8e3c9ec 100644 (file)
 #define _CRT_SECURE_NO_WARNINGS
 #endif
 
+/*
+** Optionally #include a user-defined header, whereby compilation options
+** may be set prior to where they take effect, but after platform setup. 
+** If SQLITE_CUSTOM_INCLUDE=? is defined, its value names the #include
+** file. Note that this macro has a like effect on sqlite3.c compilation.
+*/
+#ifdef SQLITE_CUSTOM_INCLUDE
+# define INC_STRINGIFY_(f) #f
+# define INC_STRINGIFY(f) INC_STRINGIFY_(f)
+# include INC_STRINGIFY(SQLITE_CUSTOM_INCLUDE)
+#endif
+
 /*
 ** Determine if we are dealing with WinRT, which provides only a subset of
 ** the full Win32 API.
@@ -5652,25 +5664,6 @@ static int lintFkeyIndexes(
   return rc;
 }
 
-/*
-** Implementation of ".lint" dot command.
-*/
-static int lintDotCommand(
-  ShellState *pState,             /* Current shell tool state */
-  char **azArg,                   /* Array of arguments passed to dot command */
-  int nArg                        /* Number of entries in azArg[] */
-){
-  int n;
-  n = (nArg>=2 ? strlen30(azArg[1]) : 0);
-  if( n<1 || sqlite3_strnicmp(azArg[1], "fkey-indexes", n) ) goto usage;
-  return lintFkeyIndexes(pState, azArg, nArg);
-
- usage:
-  raw_printf(stderr, "Usage %s sub-command ?switches...?\n", azArg[0]);
-  raw_printf(stderr, "Where sub-commands are:\n");
-  raw_printf(stderr, "    fkey-indexes\n");
-  return SQLITE_ERROR;
-}
 
 #if !defined SQLITE_OMIT_VIRTUALTABLE
 static void shellPrepare(
@@ -6813,463 +6806,144 @@ static RecoverTable *recoverOrphanTable(
   }
   return pTab;
 }
+#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */
 
-/*
-** This function is called to recover data from the database. A script
-** to construct a new database containing all recovered data is output
-** on stream pState->out.
-*/
-static int recoverDatabaseCmd(ShellState *pState, int nArg, char **azArg){
-  int rc = SQLITE_OK;
-  sqlite3_stmt *pLoop = 0;        /* Loop through all root pages */
-  sqlite3_stmt *pPages = 0;       /* Loop through all pages in a group */
-  sqlite3_stmt *pCells = 0;       /* Loop through all cells in a page */
-  const char *zRecoveryDb = "";   /* Name of "recovery" database */
-  const char *zLostAndFound = "lost_and_found";
-  int i;
-  int nOrphan = -1;
-  RecoverTable *pOrphan = 0;
-
-  int bFreelist = 1;              /* 0 if --freelist-corrupt is specified */
-  int bRowids = 1;                /* 0 if --no-rowids */
-  for(i=1; i<nArg; i++){
-    char *z = azArg[i];
-    int n;
-    if( z[0]=='-' && z[1]=='-' ) z++;
-    n = strlen30(z);
-    if( n<=17 && memcmp("-freelist-corrupt", z, n)==0 ){
-      bFreelist = 0;
-    }else
-    if( n<=12 && memcmp("-recovery-db", z, n)==0 && i<(nArg-1) ){
-      i++;
-      zRecoveryDb = azArg[i];
-    }else
-    if( n<=15 && memcmp("-lost-and-found", z, n)==0 && i<(nArg-1) ){
-      i++;
-      zLostAndFound = azArg[i];
-    }else
-    if( n<=10 && memcmp("-no-rowids", z, n)==0 ){
-      bRowids = 0;
-    }
-    else{
-      utf8_printf(stderr, "unexpected option: %s\n", azArg[i]); 
-      showHelp(pState->out, azArg[0]);
-      return 1;
+static int writeDb( char *azArg[], int nArg, ShellState *p ){
+  int rc = 0;
+  const char *zDestFile = 0;
+  const char *zDb = 0;
+  sqlite3 *pDest;
+  sqlite3_backup *pBackup;
+  int j;
+  int bAsync = 0;
+  const char *zVfs = 0;
+  for(j=1; j<nArg; j++){
+    const char *z = azArg[j];
+    if( z[0]=='-' ){
+      if( z[1]=='-' ) z++;
+      if( strcmp(z, "-append")==0 ){
+        zVfs = "apndvfs";
+      }else
+        if( strcmp(z, "-async")==0 ){
+          bAsync = 1;
+        }else
+          {
+            utf8_printf(stderr, "unknown option: %s\n", azArg[j]);
+            return INVALID_ARGS;
+          }
+    }else if( zDestFile==0 ){
+      zDestFile = azArg[j];
+    }else if( zDb==0 ){
+      zDb = zDestFile;
+      zDestFile = azArg[j];
+    }else{
+      raw_printf(stderr, "Usage: .backup ?DB? ?OPTIONS? FILENAME\n");
+      return INVALID_ARGS;
     }
   }
-
-  shellExecPrintf(pState->db, &rc,
-    /* Attach an in-memory database named 'recovery'. Create an indexed 
-    ** cache of the sqlite_dbptr virtual table. */
-    "PRAGMA writable_schema = on;"
-    "ATTACH %Q AS recovery;"
-    "DROP TABLE IF EXISTS recovery.dbptr;"
-    "DROP TABLE IF EXISTS recovery.freelist;"
-    "DROP TABLE IF EXISTS recovery.map;"
-    "DROP TABLE IF EXISTS recovery.schema;"
-    "CREATE TABLE recovery.freelist(pgno INTEGER PRIMARY KEY);", zRecoveryDb
-  );
-
-  if( bFreelist ){
-    shellExec(pState->db, &rc,
-      "WITH trunk(pgno) AS ("
-      "  SELECT shell_int32("
-      "      (SELECT data FROM sqlite_dbpage WHERE pgno=1), 8) AS x "
-      "      WHERE x>0"
-      "    UNION"
-      "  SELECT shell_int32("
-      "      (SELECT data FROM sqlite_dbpage WHERE pgno=trunk.pgno), 0) AS x "
-      "      FROM trunk WHERE x>0"
-      "),"
-      "freelist(data, n, freepgno) AS ("
-      "  SELECT data, min(16384, shell_int32(data, 1)-1), t.pgno "
-      "      FROM trunk t, sqlite_dbpage s WHERE s.pgno=t.pgno"
-      "    UNION ALL"
-      "  SELECT data, n-1, shell_int32(data, 2+n) "
-      "      FROM freelist WHERE n>=0"
-      ")"
-      "REPLACE INTO recovery.freelist SELECT freepgno FROM freelist;"
-    );
+  if( zDestFile==0 ){
+    raw_printf(stderr, "missing FILENAME argument on .backup\n");
+    return INVALID_ARGS;
+  }
+  if( zDb==0 ) zDb = "main";
+  rc = sqlite3_open_v2(zDestFile, &pDest, 
+                       SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, zVfs);
+  if( rc!=SQLITE_OK ){
+    utf8_printf(stderr, "Error: cannot open \"%s\"\n", zDestFile);
+    close_db(pDest);
+    return 1;
+  }
+  if( bAsync ){
+    sqlite3_exec(pDest, "PRAGMA synchronous=OFF; PRAGMA journal_mode=OFF;",
+                 0, 0, 0);
+  }
+  open_db(p, 0);
+  pBackup = sqlite3_backup_init(pDest, "main", p->db, zDb);
+  if( pBackup==0 ){
+    utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(pDest));
+    close_db(pDest);
+    return 1;
+  }
+  while(  (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK ){}
+  sqlite3_backup_finish(pBackup);
+  if( rc==SQLITE_DONE ){
+    rc = 0;
+  }else{
+    utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(pDest));
+    rc = 1;
   }
+  close_db(pDest);
+  return rc;
+}
 
-  /* If this is an auto-vacuum database, add all pointer-map pages to
-  ** the freelist table. Do this regardless of whether or not 
-  ** --freelist-corrupt was specified.  */
-  shellExec(pState->db, &rc, 
-    "WITH ptrmap(pgno) AS ("
-    "  SELECT 2 WHERE shell_int32("
-    "    (SELECT data FROM sqlite_dbpage WHERE pgno=1), 13"
-    "  )"
-    "    UNION ALL "
-    "  SELECT pgno+1+(SELECT page_size FROM pragma_page_size)/5 AS pp "
-    "  FROM ptrmap WHERE pp<=(SELECT page_count FROM pragma_page_count)"
-    ")"
-    "REPLACE INTO recovery.freelist SELECT pgno FROM ptrmap"
-  );
+/*
+ * Define meta-commands and provide for their dispatch and .help text.
+ * These should be kept in command name order for coding convenience
+ * except where meta-commands share implementation. (The ordering
+ * required for dispatch and help text is effected regardless.)
+ */
 
-  shellExec(pState->db, &rc, 
-    "CREATE TABLE recovery.dbptr("
-    "      pgno, child, PRIMARY KEY(child, pgno)"
-    ") WITHOUT ROWID;"
-    "INSERT OR IGNORE INTO recovery.dbptr(pgno, child) "
-    "    SELECT * FROM sqlite_dbptr"
-    "      WHERE pgno NOT IN freelist AND child NOT IN freelist;"
+DISPATCH_CONFIG[
+  RETURN_TYPE=int
+  STORAGE_CLASS=static
+  ARGS_SIGNATURE=char *$arg4\[\], int $arg5, ShellState *$arg6
+  DISPATCH_ENTRY={ "$cmd", ${cmd}Command, $arg1, $arg2, $arg3 },
+  CMD_CAPTURE_RE=^\s*{\s*"(\w+)"
+  DISPATCHEE_NAME=${cmd}Command
+  DC_ARG1_DEFAULT=[string length $cmd]
+  DC_ARG2_DEFAULT=0
+  DC_ARG3_DEFAULT=0
+  DC_ARG4_DEFAULT=azArg
+  DC_ARG5_DEFAULT=nArg
+  DC_ARG6_DEFAULT=p
+  DC_ARG_COUNT=7
+];
 
-    /* Delete any pointer to page 1. This ensures that page 1 is considered
-    ** a root page, regardless of how corrupt the db is. */
-    "DELETE FROM recovery.dbptr WHERE child = 1;"
 
-    /* Delete all pointers to any pages that have more than one pointer
-    ** to them. Such pages will be treated as root pages when recovering
-    ** data.  */
-    "DELETE FROM recovery.dbptr WHERE child IN ("
-    "  SELECT child FROM recovery.dbptr GROUP BY child HAVING count(*)>1"
-    ");"
+CONDITION_COMMAND(seeargs defined(SQLITE_GIMME_SEEARGS));
+/*****************
+ * The .seeargs command
+ */
+COLLECT_HELP_TEXT[
+  ".seeargs                 Echo arguments separated by |",
+  "    A near-dummy command for use as a template (to vanish soon)",
+];
+DISPATCHABLE_COMMAND( seeargs ? 0 0 azArg nArg p ){
+  int rc = 0;
+  for (rc=1; rc<nArg; ++rc)
+    raw_printf(p->out, "%s%s", azArg[rc], (rc==nArg-1)? "\n" : "|");
+  return rc;
+}
 
-    /* Create the "map" table that will (eventually) contain instructions
-    ** for dealing with each page in the db that contains one or more 
-    ** records. */
-    "CREATE TABLE recovery.map("
-      "pgno INTEGER PRIMARY KEY, maxlen INT, intkey, root INT"
-    ");"
-
-    /* Populate table [map]. If there are circular loops of pages in the
-    ** database, the following adds all pages in such a loop to the map
-    ** as individual root pages. This could be handled better.  */
-    "WITH pages(i, maxlen) AS ("
-    "  SELECT page_count, ("
-    "    SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=page_count"
-    "  ) FROM pragma_page_count WHERE page_count>0"
-    "    UNION ALL"
-    "  SELECT i-1, ("
-    "    SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=i-1"
-    "  ) FROM pages WHERE i>=2"
-    ")"
-    "INSERT INTO recovery.map(pgno, maxlen, intkey, root) "
-    "  SELECT i, maxlen, NULL, ("
-    "    WITH p(orig, pgno, parent) AS ("
-    "      SELECT 0, i, (SELECT pgno FROM recovery.dbptr WHERE child=i)"
-    "        UNION "
-    "      SELECT i, p.parent, "
-    "        (SELECT pgno FROM recovery.dbptr WHERE child=p.parent) FROM p"
-    "    )"
-    "    SELECT pgno FROM p WHERE (parent IS NULL OR pgno = orig)"
-    ") "
-    "FROM pages WHERE maxlen IS NOT NULL AND i NOT IN freelist;"
-    "UPDATE recovery.map AS o SET intkey = ("
-    "  SELECT substr(data, 1, 1)==X'0D' FROM sqlite_dbpage WHERE pgno=o.pgno"
-    ");"
-
-    /* Extract data from page 1 and any linked pages into table
-    ** recovery.schema. With the same schema as an sqlite_schema table.  */
-    "CREATE TABLE recovery.schema(type, name, tbl_name, rootpage, sql);"
-    "INSERT INTO recovery.schema SELECT "
-    "  max(CASE WHEN field=0 THEN value ELSE NULL END),"
-    "  max(CASE WHEN field=1 THEN value ELSE NULL END),"
-    "  max(CASE WHEN field=2 THEN value ELSE NULL END),"
-    "  max(CASE WHEN field=3 THEN value ELSE NULL END),"
-    "  max(CASE WHEN field=4 THEN value ELSE NULL END)"
-    "FROM sqlite_dbdata WHERE pgno IN ("
-    "  SELECT pgno FROM recovery.map WHERE root=1"
-    ")"
-    "GROUP BY pgno, cell;"
-    "CREATE INDEX recovery.schema_rootpage ON schema(rootpage);"
-  );
-
-  /* Open a transaction, then print out all non-virtual, non-"sqlite_%" 
-  ** CREATE TABLE statements that extracted from the existing schema.  */
-  if( rc==SQLITE_OK ){
-    sqlite3_stmt *pStmt = 0;
-    /* ".recover" might output content in an order which causes immediate
-    ** foreign key constraints to be violated. So disable foreign-key
-    ** constraint enforcement to prevent problems when running the output
-    ** script. */
-    raw_printf(pState->out, "PRAGMA foreign_keys=OFF;\n");
-    raw_printf(pState->out, "BEGIN;\n");
-    raw_printf(pState->out, "PRAGMA writable_schema = on;\n");
-    shellPrepare(pState->db, &rc,
-        "SELECT sql FROM recovery.schema "
-        "WHERE type='table' AND sql LIKE 'create table%'", &pStmt
-    );
-    while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
-      const char *zCreateTable = (const char*)sqlite3_column_text(pStmt, 0);
-      raw_printf(pState->out, "CREATE TABLE IF NOT EXISTS %s;\n", 
-          &zCreateTable[12]
-      );
-    }
-    shellFinalize(&rc, pStmt);
-  }
-
-  /* Figure out if an orphan table will be required. And if so, how many
-  ** user columns it should contain */
-  shellPrepare(pState->db, &rc, 
-      "SELECT coalesce(max(maxlen), -2) FROM recovery.map WHERE root>1"
-      , &pLoop
-  );
-  if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){
-    nOrphan = sqlite3_column_int(pLoop, 0);
-  }
-  shellFinalize(&rc, pLoop);
-  pLoop = 0;
-
-  shellPrepare(pState->db, &rc,
-      "SELECT pgno FROM recovery.map WHERE root=?", &pPages
-  );
-
-  shellPrepare(pState->db, &rc,
-      "SELECT max(field), group_concat(shell_escape_crnl(quote"
-      "(case when (? AND field<0) then NULL else value end)"
-      "), ', ')"
-      ", min(field) "
-      "FROM sqlite_dbdata WHERE pgno = ? AND field != ?"
-      "GROUP BY cell", &pCells
-  );
-
-  /* Loop through each root page. */
-  shellPrepare(pState->db, &rc, 
-      "SELECT root, intkey, max(maxlen) FROM recovery.map" 
-      " WHERE root>1 GROUP BY root, intkey ORDER BY root=("
-      "  SELECT rootpage FROM recovery.schema WHERE name='sqlite_sequence'"
-      ")", &pLoop
-  );
-  while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){
-    int iRoot = sqlite3_column_int(pLoop, 0);
-    int bIntkey = sqlite3_column_int(pLoop, 1);
-    int nCol = sqlite3_column_int(pLoop, 2);
-    int bNoop = 0;
-    RecoverTable *pTab;
-
-    assert( bIntkey==0 || bIntkey==1 );
-    pTab = recoverFindTable(pState, &rc, iRoot, bIntkey, nCol, &bNoop);
-    if( bNoop || rc ) continue;
-    if( pTab==0 ){
-      if( pOrphan==0 ){
-        pOrphan = recoverOrphanTable(pState, &rc, zLostAndFound, nOrphan);
-      }
-      pTab = pOrphan;
-      if( pTab==0 ) break;
-    }
-
-    if( 0==sqlite3_stricmp(pTab->zQuoted, "\"sqlite_sequence\"") ){
-      raw_printf(pState->out, "DELETE FROM sqlite_sequence;\n");
-    }
-    sqlite3_bind_int(pPages, 1, iRoot);
-    if( bRowids==0 && pTab->iPk<0 ){
-      sqlite3_bind_int(pCells, 1, 1);
-    }else{
-      sqlite3_bind_int(pCells, 1, 0);
-    }
-    sqlite3_bind_int(pCells, 3, pTab->iPk);
-
-    while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPages) ){
-      int iPgno = sqlite3_column_int(pPages, 0);
-      sqlite3_bind_int(pCells, 2, iPgno);
-      while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pCells) ){
-        int nField = sqlite3_column_int(pCells, 0);
-        int iMin = sqlite3_column_int(pCells, 2);
-        const char *zVal = (const char*)sqlite3_column_text(pCells, 1);
-
-        RecoverTable *pTab2 = pTab;
-        if( pTab!=pOrphan && (iMin<0)!=bIntkey ){
-          if( pOrphan==0 ){
-            pOrphan = recoverOrphanTable(pState, &rc, zLostAndFound, nOrphan);
-          }
-          pTab2 = pOrphan;
-          if( pTab2==0 ) break;
-        }
-
-        nField = nField+1;
-        if( pTab2==pOrphan ){
-          raw_printf(pState->out, 
-              "INSERT INTO %s VALUES(%d, %d, %d, %s%s%s);\n",
-              pTab2->zQuoted, iRoot, iPgno, nField,
-              iMin<0 ? "" : "NULL, ", zVal, pTab2->azlCol[nField]
-          );
-        }else{
-          raw_printf(pState->out, "INSERT INTO %s(%s) VALUES( %s );\n", 
-              pTab2->zQuoted, pTab2->azlCol[nField], zVal
-          );
-        }
-      }
-      shellReset(&rc, pCells);
-    }
-    shellReset(&rc, pPages);
-    if( pTab!=pOrphan ) recoverFreeTable(pTab);
-  }
-  shellFinalize(&rc, pLoop);
-  shellFinalize(&rc, pPages);
-  shellFinalize(&rc, pCells);
-  recoverFreeTable(pOrphan);
-
-  /* The rest of the schema */
-  if( rc==SQLITE_OK ){
-    sqlite3_stmt *pStmt = 0;
-    shellPrepare(pState->db, &rc, 
-        "SELECT sql, name FROM recovery.schema "
-        "WHERE sql NOT LIKE 'create table%'", &pStmt
-    );
-    while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
-      const char *zSql = (const char*)sqlite3_column_text(pStmt, 0);
-      if( sqlite3_strnicmp(zSql, "create virt", 11)==0 ){
-        const char *zName = (const char*)sqlite3_column_text(pStmt, 1);
-        char *zPrint = shellMPrintf(&rc, 
-          "INSERT INTO sqlite_schema VALUES('table', %Q, %Q, 0, %Q)",
-          zName, zName, zSql
-        );
-        raw_printf(pState->out, "%s;\n", zPrint);
-        sqlite3_free(zPrint);
-      }else{
-        raw_printf(pState->out, "%s;\n", zSql);
-      }
-    }
-    shellFinalize(&rc, pStmt);
-  }
-
-  if( rc==SQLITE_OK ){
-    raw_printf(pState->out, "PRAGMA writable_schema = off;\n");
-    raw_printf(pState->out, "COMMIT;\n");
-  }
-  sqlite3_exec(pState->db, "DETACH recovery", 0, 0, 0);
-  return rc;
-}
-#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */
-
-static int writeDb( char *azArg[], int nArg, ShellState *p ){
-  int rc = 0;
-  const char *zDestFile = 0;
-  const char *zDb = 0;
-  sqlite3 *pDest;
-  sqlite3_backup *pBackup;
-  int j;
-  int bAsync = 0;
-  const char *zVfs = 0;
-  for(j=1; j<nArg; j++){
-    const char *z = azArg[j];
-    if( z[0]=='-' ){
-      if( z[1]=='-' ) z++;
-      if( strcmp(z, "-append")==0 ){
-        zVfs = "apndvfs";
-      }else
-        if( strcmp(z, "-async")==0 ){
-          bAsync = 1;
-        }else
-          {
-            utf8_printf(stderr, "unknown option: %s\n", azArg[j]);
-            return INVALID_ARGS;
-          }
-    }else if( zDestFile==0 ){
-      zDestFile = azArg[j];
-    }else if( zDb==0 ){
-      zDb = zDestFile;
-      zDestFile = azArg[j];
-    }else{
-      raw_printf(stderr, "Usage: .backup ?DB? ?OPTIONS? FILENAME\n");
-      return INVALID_ARGS;
-    }
-  }
-  if( zDestFile==0 ){
-    raw_printf(stderr, "missing FILENAME argument on .backup\n");
-    return INVALID_ARGS;
-  }
-  if( zDb==0 ) zDb = "main";
-  rc = sqlite3_open_v2(zDestFile, &pDest, 
-                       SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, zVfs);
-  if( rc!=SQLITE_OK ){
-    utf8_printf(stderr, "Error: cannot open \"%s\"\n", zDestFile);
-    close_db(pDest);
-    return 1;
-  }
-  if( bAsync ){
-    sqlite3_exec(pDest, "PRAGMA synchronous=OFF; PRAGMA journal_mode=OFF;",
-                 0, 0, 0);
-  }
-  open_db(p, 0);
-  pBackup = sqlite3_backup_init(pDest, "main", p->db, zDb);
-  if( pBackup==0 ){
-    utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(pDest));
-    close_db(pDest);
-    return 1;
-  }
-  while(  (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK ){}
-  sqlite3_backup_finish(pBackup);
-  if( rc==SQLITE_DONE ){
-    rc = 0;
-  }else{
-    utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(pDest));
-    rc = 1;
-  }
-  close_db(pDest);
-  return rc;
-}
-
-/*
- * Define meta-commands and provide for their dispatch and .help text.
- * These should be kept in command name order for coding convenience
- * except where meta-commands share implementation. (The ordering
- * required for dispatch and help text is effected regardless.)
- */
-
-DISPATCH_CONFIG[
-  RETURN_TYPE=int
-  STORAGE_CLASS=static
-  ARGS_SIGNATURE=char *$arg4\[\], int $arg5, ShellState *$arg6
-  DISPATCH_ENTRY={ "$cmd", ${cmd}Command, $arg1, $arg2, $arg3 },
-  CMD_CAPTURE_RE=^\s*{\s*"(\w+)"
-  DISPATCHEE_NAME=${cmd}Command
-  DC_ARG1_DEFAULT=[string length $cmd]
-  DC_ARG2_DEFAULT=0
-  DC_ARG3_DEFAULT=0
-  DC_ARG4_DEFAULT=azArg
-  DC_ARG5_DEFAULT=nArg
-  DC_ARG6_DEFAULT=p
-  DC_ARG_COUNT=7
-];
-
-
-CONDITION_COMMAND(seeargs defined(SQLITE_GIMME_SEEARGS));
-/*****************
- * The .seeargs command
- */
-COLLECT_HELP_TEXT[
-  ".seeargs                 Echo arguments separated by |",
-  "    A near-dummy command for use as a template (to vanish soon)",
-];
-DISPATCHABLE_COMMAND( seeargs ? 0 0 azArg nArg p ){
-  int rc = 0;
-  for (rc=1; rc<nArg; ++rc)
-    raw_printf(p->out, "%s%s", azArg[rc], (rc==nArg-1)? "\n" : "|");
-  return rc;
-}
-
-CONDITION_COMMAND(archive !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB));
-/*****************
- * The .archive command
- */
-COLLECT_HELP_TEXT[
-  ".archive ...             Manage SQL archives",
-  "   Each command must have exactly one of the following options:",
-  "     -c, --create               Create a new archive",
-  "     -u, --update               Add or update files with changed mtime",
-  "     -i, --insert               Like -u but always add even if unchanged",
-  "     -t, --list                 List contents of archive",
-  "     -x, --extract              Extract files from archive",
-  "   Optional arguments:",
-  "     -v, --verbose              Print each filename as it is processed",
-  "     -f FILE, --file FILE       Use archive FILE (default is current db)",
-  "     -a FILE, --append FILE     Open FILE using the apndvfs VFS",
-  "     -C DIR, --directory DIR    Read/extract files from directory DIR",
-  "     -n, --dryrun               Show the SQL that would have occurred",
-  "   Examples:",
-  "     .ar -cf ARCHIVE foo bar  # Create ARCHIVE from files foo and bar",
-  "     .ar -tf ARCHIVE          # List members of ARCHIVE",
-  "     .ar -xvf ARCHIVE         # Verbosely extract files from ARCHIVE",
-  "   See also:",
-  "      http://sqlite.org/cli.html#sqlite_archive_support",
-];
-DISPATCHABLE_COMMAND( archive ? 3 0 azArg nArg p ){
-  open_db(p, 0);
-  return arDotCommand(p, 0, azArg, nArg);
-}
+CONDITION_COMMAND(archive !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB));
+/*****************
+ * The .archive command
+ */
+COLLECT_HELP_TEXT[
+  ".archive ...             Manage SQL archives",
+  "   Each command must have exactly one of the following options:",
+  "     -c, --create               Create a new archive",
+  "     -u, --update               Add or update files with changed mtime",
+  "     -i, --insert               Like -u but always add even if unchanged",
+  "     -t, --list                 List contents of archive",
+  "     -x, --extract              Extract files from archive",
+  "   Optional arguments:",
+  "     -v, --verbose              Print each filename as it is processed",
+  "     -f FILE, --file FILE       Use archive FILE (default is current db)",
+  "     -a FILE, --append FILE     Open FILE using the apndvfs VFS",
+  "     -C DIR, --directory DIR    Read/extract files from directory DIR",
+  "     -n, --dryrun               Show the SQL that would have occurred",
+  "   Examples:",
+  "     .ar -cf ARCHIVE foo bar  # Create ARCHIVE from files foo and bar",
+  "     .ar -tf ARCHIVE          # List members of ARCHIVE",
+  "     .ar -xvf ARCHIVE         # Verbosely extract files from ARCHIVE",
+  "   See also:",
+  "      http://sqlite.org/cli.html#sqlite_archive_support",
+];
+DISPATCHABLE_COMMAND( archive ? 3 0 azArg nArg p ){
+  open_db(p, 0);
+  return arDotCommand(p, 0, azArg, nArg);
+}
 
 /*****************
  * The .auth command
@@ -7665,1605 +7339,1969 @@ DISPATCHABLE_COMMAND( eqp ? 0 0 ){
 }
 
 /*****************
- * The . command
- */
-COLLECT_HELP_TEXT[
-];
-DISPATCHABLE_COMMAND( ? ? 1 1 ){
-}
-
-
-/*****************
- * The .help command
+ * The .expert and .explain commands
  */
+CONDITION_COMMAND( expert !defined(SQLITE_OMIT_VIRTUALTABLE) );
 COLLECT_HELP_TEXT[
-  ".help ?-all? ?PATTERN?   Show help text for PATTERN",
+  ".expert                  Suggest indexes for queries",
+  ".explain ?on|off|auto?   Change the EXPLAIN formatting mode.  Default: auto",
 ];
-DISPATCHABLE_COMMAND( help 3 0 0 ){
-  if( nArg>=2 ){
-    if( showHelp(p->out, azArg[1])==0 ){
-      utf8_printf(p->out, "Nothing matches '%s'\n", azArg[1]);
+DISPATCHABLE_COMMAND( explain ? 1 2 ){
+  /* The ".explain" command is automatic now.  It is largely
+  ** pointless, retained purely for backwards compatibility */
+  int val = 1;
+  if( nArg==2 ){
+    if( strcmp(azArg[1],"auto")==0 ){
+      val = 99;
+    }else{
+      val =  booleanValue(azArg[1]);
     }
-  }else{
-    showHelp(p->out, 0);
   }
-  /* Help pleas never fail! */
+  if( val==1 && p->mode!=MODE_Explain ){
+    p->normalMode = p->mode;
+    p->mode = MODE_Explain;
+    p->autoExplain = 0;
+  }else if( val==0 ){
+    if( p->mode==MODE_Explain ) p->mode = p->normalMode;
+    p->autoExplain = 0;
+  }else if( val==99 ){
+    if( p->mode==MODE_Explain ) p->mode = p->normalMode;
+    p->autoExplain = 1;
+  }
+}
+DISPATCHABLE_COMMAND( expert ? 1 1 ){
+  open_db(p, 0);
+  expertDotCommand(p, azArg, nArg);
   return 0;
 }
 
-/* Define and populate command dispatch table. */
-static struct DispatchEntry {
-  const char * cmdName;
-  int (*cmdDoer)(char *azArg[], int nArg, ShellState *);
-  unsigned char minLen, minArgs, maxArgs;
-} command_table[] = {
-  EMIT_DISPATCH(2);
-  { 0, 0, 0, -1, -1 }
-};
-static unsigned numCommands = sizeof(command_table)/sizeof(struct DispatchEntry) - 1;
-
 /*****************
- * Command dispatcher
+ * The .excel, .once and .output commands
+ * These share much implementation, so they stick together.
  */
+COLLECT_HELP_TEXT[
+  ".excel                   Display the output of next command in spreadsheet",
+  "   --bom                   Prefix the file with a UTF8 byte-order mark",
+  ".once ?OPTIONS? ?FILE?   Output for the next SQL command only to FILE",
+  "   If FILE begins with '|' then open it as a command to be piped into.",
+  "   Options:",
+  "     --bom                 Prefix output with a UTF8 byte-order mark",
+  "     -e                    Send output to the system text editor",
+  "     -x       Send output as CSV to a spreadsheet (same as \".excel\")",
+  ".output ?FILE?           Send output to FILE or stdout if FILE is omitted",
+  "   If FILE begins with '|' then open it as a command to be piped into.",
+  "   Options:",
+  "     --bom                 Prefix output with a UTF8 byte-order mark",
+  "     -e                    Send output to the system text editor",
+  "     -x       Send output as CSV to a spreadsheet (same as \".excel\")",
+];
+static int outputRedirs(char *[], int, ShellState *, int bOnce, int eMode);
+DISPATCHABLE_COMMAND( excel ? 1 2 ){
+  return outputRedirs(azArg, nArg, p, 2, 'x');
+}
+DISPATCHABLE_COMMAND( once ? 1 6 ){
+  return outputRedirs(azArg, nArg, p, 1, 0);
+}
+DISPATCHABLE_COMMAND( output ? 1 6 ){
+  return outputRedirs(azArg, nArg, p, 0, 0);
+}
 
-int dispatchCommand(char *azArg[], int nArg, ShellState *pSS)
-{
-  const char *cmdName = azArg[0];
-  int cmdLen = strlen30(cmdName);
-  struct DispatchEntry *pde = 0;
-  int ixb = 0, ixe = numCommands-1;
-  while( ixb <= ixe ){
-    int ixm = (ixb+ixe)/2;
-    int md = strncmp(cmdName, command_table[ixm].cmdName, cmdLen);
-    if( md>0 ){
-      ixb = ixm+1;
-    }else if( md<0 ){
-      ixe = ixm-1;
-    }else{
-      if( command_table[ixm].minLen > cmdLen ){
-        return NO_SUCH_COMMAND;
+static int outputRedirs(char *azArg[], int nArg, ShellState *p,
+                        int bOnce, int eMode){
+  /* bOnce => 0: .output, 1: .once, 2: .excel */
+  /* eMode => 'x' for excel, else 0 */
+  int rc = 0;
+  char *zFile = 0;
+  int bTxtMode = 0;
+  int i;
+  int bBOM = 0;
+  for(i=1; i<nArg; i++){
+    char *z = azArg[i];
+    if( z[0]=='-' ){
+      if( z[1]=='-' ) z++;
+      if( strcmp(z,"-bom")==0 ){
+        bBOM = 1;
+      }else if( bOnce!=2 && strcmp(z,"-x")==0 ){
+        eMode = 'x';  /* spreadsheet */
+      }else if( bOnce!=2 && strcmp(z,"-e")==0 ){
+        eMode = 'e';  /* text editor */
+      }else{
+        utf8_printf(p->out,"ERROR: unknown option: \"%s\".  Usage:\n",azArg[i]);
+        showHelp(p->out, azArg[0]);
+        return 1;
       }
-      pde = &command_table[ixm];
-      break;
+    }else if( zFile==0 && eMode!='e' && eMode!='x' ){
+      zFile = sqlite3_mprintf("%s", z);
+      if( zFile[0]=='|' ){
+        while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
+        break;
+      }
+    }else{
+      utf8_printf(p->out,"ERROR: extra parameter: \"%s\".  Usage:\n", azArg[i]);
+      showHelp(p->out, azArg[0]);
+      sqlite3_free(zFile);
+      return 1;
     }
   }
-  if( 0==pde ){
-    return NO_SUCH_COMMAND;
+  if( zFile==0 ) zFile = sqlite3_mprintf("stdout");
+  if( bOnce ){
+    p->outCount = 2;
+  }else{
+    p->outCount = 0;
   }
-  if((pde->minArgs > 0 && pde->minArgs > nArg)||(pde->maxArgs > 0 && pde->maxArgs < nArg)){
-    return INVALID_ARGS;
+  output_reset(p);
+#ifndef SQLITE_NOHAVE_SYSTEM
+  if( eMode=='e' || eMode=='x' ){
+    p->doXdgOpen = 1;
+    outputModePush(p);
+    if( eMode=='x' ){
+      /* spreadsheet mode.  Output as CSV. */
+      newTempFile(p, "csv");
+      ShellClearFlag(p, SHFLG_Echo);
+      p->mode = MODE_Csv;
+      sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma);
+      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_CrLf);
+    }else{
+      /* text editor mode */
+      newTempFile(p, "txt");
+      bTxtMode = 1;
+    }
+    sqlite3_free(zFile);
+    zFile = sqlite3_mprintf("%s", p->zTempFile);
   }
-  return (pde->cmdDoer)(azArg, nArg, pSS);
+#endif /* SQLITE_NOHAVE_SYSTEM */
+  if( zFile[0]=='|' ){
+#ifdef SQLITE_OMIT_POPEN
+    raw_printf(stderr, "Error: pipes are not supported in this OS\n");
+    rc = 1;
+    p->out = stdout;
+#else
+    p->out = popen(zFile + 1, "w");
+    if( p->out==0 ){
+      utf8_printf(stderr,"Error: cannot open pipe \"%s\"\n", zFile + 1);
+      p->out = stdout;
+      rc = 1;
+    }else{
+      if( bBOM ) fprintf(p->out,"\357\273\277");
+      sqlite3_snprintf(sizeof(p->outfile), p->outfile, "%s", zFile);
+    }
+#endif
+  }else{
+    p->out = output_file_open(zFile, bTxtMode);
+    if( p->out==0 ){
+      if( strcmp(zFile,"off")!=0 ){
+        utf8_printf(stderr,"Error: cannot write to \"%s\"\n", zFile);
+      }
+      p->out = stdout;
+      rc = 1;
+    } else {
+      if( bBOM ) fprintf(p->out,"\357\273\277");
+      sqlite3_snprintf(sizeof(p->outfile), p->outfile, "%s", zFile);
+    }
+  }
+  sqlite3_free(zFile);
+  return rc;
 }
 
 
-/*
-** If an input line begins with "." then invoke this routine to
-** process that line.
-**
-** Return 1 on error, 2 to exit, and 0 otherwise.
-*/
-static int do_meta_command(char *zLine, ShellState *p){
-  int h = 1;
-  int nArg = 0;
-  int n, c;
-  int rc = 0;
-  char *azArg[52];
+/*****************
+ * The .filectrl and fullschema commands
+ */
+COLLECT_HELP_TEXT[
+  ".filectrl CMD ...        Run various sqlite3_file_control() operations",
+  "   --schema SCHEMA         Use SCHEMA instead of \"main\"",
+  "   --help                  Show CMD details",
+  ".fullschema ?--indent?   Show schema and the content of sqlite_stat tables",
+];
+DISPATCHABLE_COMMAND( filectrl ? 2 0 ){
+  static const struct {
+    const char *zCtrlName;   /* Name of a test-control option */
+    int ctrlCode;            /* Integer code for that option */
+    const char *zUsage;      /* Usage notes */
+  } aCtrl[] = {
+    { "chunk_size",     SQLITE_FCNTL_CHUNK_SIZE,      "SIZE"           },
+    { "data_version",   SQLITE_FCNTL_DATA_VERSION,    ""               },
+    { "has_moved",      SQLITE_FCNTL_HAS_MOVED,       ""               },  
+    { "lock_timeout",   SQLITE_FCNTL_LOCK_TIMEOUT,    "MILLISEC"       },
+    { "persist_wal",    SQLITE_FCNTL_PERSIST_WAL,     "[BOOLEAN]"      },
+ /* { "pragma",         SQLITE_FCNTL_PRAGMA,          "NAME ARG"       },*/
+    { "psow",       SQLITE_FCNTL_POWERSAFE_OVERWRITE, "[BOOLEAN]"      },
+    { "reserve_bytes",  SQLITE_FCNTL_RESERVE_BYTES,   "[N]"            },
+    { "size_limit",     SQLITE_FCNTL_SIZE_LIMIT,      "[LIMIT]"        },
+    { "tempfilename",   SQLITE_FCNTL_TEMPFILENAME,    ""               },
+ /* { "win32_av_retry", SQLITE_FCNTL_WIN32_AV_RETRY,  "COUNT DELAY"    },*/
+  };
+  int filectrl = -1;
+  int iCtrl = -1;
+  sqlite3_int64 iRes = 0;  /* Integer result to display if rc2==1 */
+  int isOk = 0;            /* 0: usage  1: %lld  2: no-result */
+  int n2, i;
+  const char *zCmd = 0;
+  const char *zSchema = 0;
 
-#ifndef SQLITE_OMIT_VIRTUALTABLE
-  if( p->expert.pExpert ){
-    expertFinish(p, 1, 0);
+  open_db(p, 0);
+  zCmd = nArg>=2 ? azArg[1] : "help";
+
+  if( zCmd[0]=='-' 
+      && (strcmp(zCmd,"--schema")==0 || strcmp(zCmd,"-schema")==0)
+      && nArg>=4
+      ){
+    zSchema = azArg[2];
+    for(i=3; i<nArg; i++) azArg[i-2] = azArg[i];
+    nArg -= 2;
+    zCmd = azArg[1];
   }
-#endif
 
-  /* Parse the input line into tokens.
-  */
-  while( zLine[h] && nArg<ArraySize(azArg)-1 ){
-    while( IsSpace(zLine[h]) ){ h++; }
-    if( zLine[h]==0 ) break;
-    if( zLine[h]=='\'' || zLine[h]=='"' ){
-      int delim = zLine[h++];
-      azArg[nArg++] = &zLine[h];
-      while( zLine[h] && zLine[h]!=delim ){
-        if( zLine[h]=='\\' && delim=='"' && zLine[h+1]!=0 ) h++;
-        h++;
+  /* The argument can optionally begin with "-" or "--" */
+  if( zCmd[0]=='-' && zCmd[1] ){
+    zCmd++;
+    if( zCmd[0]=='-' && zCmd[1] ) zCmd++;
+  }
+
+  /* --help lists all file-controls */
+  if( strcmp(zCmd,"help")==0 ){
+    utf8_printf(p->out, "Available file-controls:\n");
+    for(i=0; i<ArraySize(aCtrl); i++){
+      utf8_printf(p->out, "  .filectrl %s %s\n",
+                  aCtrl[i].zCtrlName, aCtrl[i].zUsage);
+    }
+    return 1;
+  }
+  
+  /* Convert filectrl text option to value. Allow any
+  ** unique prefix of the option name, or a numerical value. */
+  n2 = strlen30(zCmd);
+  for(i=0; i<ArraySize(aCtrl); i++){
+    if( strncmp(zCmd, aCtrl[i].zCtrlName, n2)==0 ){
+      if( filectrl<0 ){
+        filectrl = aCtrl[i].ctrlCode;
+        iCtrl = i;
+      }else{
+        utf8_printf(stderr, "Error: ambiguous file-control: \"%s\"\n"
+                    "Use \".filectrl --help\" for help\n", zCmd);
+        return 1;
       }
-      if( zLine[h]==delim ){
-        zLine[h++] = 0;
+    }
+  }
+  if( filectrl<0 ){
+    utf8_printf(stderr,"Error: unknown file-control: %s\n"
+                "Use \".filectrl --help\" for help\n", zCmd);
+  }else{
+   switch(filectrl){
+    case SQLITE_FCNTL_SIZE_LIMIT: {
+      if( nArg!=2 && nArg!=3 ) break;
+      iRes = nArg==3 ? integerValue(azArg[2]) : -1;
+      sqlite3_file_control(p->db, zSchema, SQLITE_FCNTL_SIZE_LIMIT, &iRes);
+      isOk = 1;
+      break;
+    }
+    case SQLITE_FCNTL_LOCK_TIMEOUT:
+    case SQLITE_FCNTL_CHUNK_SIZE: {
+      int x;
+      if( nArg!=3 ) break;
+      x = (int)integerValue(azArg[2]);
+      sqlite3_file_control(p->db, zSchema, filectrl, &x);
+      isOk = 2;
+      break;
+    }
+    case SQLITE_FCNTL_PERSIST_WAL:
+    case SQLITE_FCNTL_POWERSAFE_OVERWRITE: {
+      int x;
+      if( nArg!=2 && nArg!=3 ) break;
+      x = nArg==3 ? booleanValue(azArg[2]) : -1;
+      sqlite3_file_control(p->db, zSchema, filectrl, &x);
+      iRes = x;
+      isOk = 1;
+      break;
+    }
+    case SQLITE_FCNTL_DATA_VERSION:
+    case SQLITE_FCNTL_HAS_MOVED: {
+      int x;
+      if( nArg!=2 ) break;
+      sqlite3_file_control(p->db, zSchema, filectrl, &x);
+      iRes = x;
+      isOk = 1;
+      break;
+    }
+    case SQLITE_FCNTL_TEMPFILENAME: {
+      char *z = 0;
+      if( nArg!=2 ) break;
+      sqlite3_file_control(p->db, zSchema, filectrl, &z);
+      if( z ){
+        utf8_printf(p->out, "%s\n", z);
+        sqlite3_free(z);
       }
-      if( delim=='"' ) resolve_backslashes(azArg[nArg-1]);
-    }else{
-      azArg[nArg++] = &zLine[h];
-      while( zLine[h] && !IsSpace(zLine[h]) ){ h++; }
-      if( zLine[h] ) zLine[h++] = 0;
-      resolve_backslashes(azArg[nArg-1]);
+      isOk = 2;
+      break;
+    }
+    case SQLITE_FCNTL_RESERVE_BYTES: {
+      int x;
+      if( nArg>=3 ){
+        x = atoi(azArg[2]);
+        sqlite3_file_control(p->db, zSchema, filectrl, &x);
+      }
+      x = -1;
+      sqlite3_file_control(p->db, zSchema, filectrl, &x);
+      utf8_printf(p->out,"%d\n", x);
+      isOk = 2;
+      break;
     }
+   }
   }
-  azArg[nArg] = 0;
+  if( isOk==0 && iCtrl>=0 ){
+    utf8_printf(p->out, "Usage: .filectrl %s %s\n", zCmd,aCtrl[iCtrl].zUsage);
+    return 1;
+  }else if( isOk==1 ){
+    char zBuf[100];
+    sqlite3_snprintf(sizeof(zBuf), zBuf, "%lld", iRes);
+    raw_printf(p->out, "%s\n", zBuf);
+  }
+  return 0;
+}
 
-  /* Process the input line.
-  */
-  if( nArg==0 ) return 0; /* no tokens, no error */
-  n = strlen30(azArg[0]);
-  c = azArg[0][0];
-  clearTempFile(p);
+DISPATCHABLE_COMMAND( fullschema ? 1 2 ){
+  int rc;
+  ShellState data;
+  int doStats = 0;
+  memcpy(&data, p, sizeof(data));
+  data.showHeader = 0;
+  data.cMode = data.mode = MODE_Semi;
+  if( nArg==2 && optionMatch(azArg[1], "indent") ){
+    data.cMode = data.mode = MODE_Pretty;
+    nArg = 1;
+  }
+  if( nArg!=1 ){
+    raw_printf(stderr, "Usage: .fullschema ?--indent?\n");
+    return 1;
+  }
+  open_db(p, 0);
+  rc = sqlite3_exec(p->db,
+    "SELECT sql FROM"
+    "  (SELECT sql sql, type type, tbl_name tbl_name, name name, rowid x"
+    "     FROM sqlite_schema UNION ALL"
+    "   SELECT sql, type, tbl_name, name, rowid FROM sqlite_temp_schema) "
+    "WHERE type!='meta' AND sql NOTNULL AND name NOT LIKE 'sqlite_%' "
+    "ORDER BY x",
+    callback, &data, 0
+  );
+  if( rc==SQLITE_OK ){
+    sqlite3_stmt *pStmt;
+    rc = sqlite3_prepare_v2(p->db,
+                            "SELECT rowid FROM sqlite_schema"
+                            " WHERE name GLOB 'sqlite_stat[134]'",
+                            -1, &pStmt, 0);
+    doStats = sqlite3_step(pStmt)==SQLITE_ROW;
+    sqlite3_finalize(pStmt);
+  }
+  if( doStats==0 ){
+    raw_printf(p->out, "/* No STAT tables available */\n");
+  }else{
+    raw_printf(p->out, "ANALYZE sqlite_schema;\n");
+    data.cMode = data.mode = MODE_Insert;
+    data.zDestTable = "sqlite_stat1";
+    shell_exec(&data, "SELECT * FROM sqlite_stat1", 0);
+    data.zDestTable = "sqlite_stat4";
+    shell_exec(&data, "SELECT * FROM sqlite_stat4", 0);
+    raw_printf(p->out, "ANALYZE sqlite_schema;\n");
+  }
+  return rc > 0;
+}
 
-  /* Check for the special, non-dispatched meta-commands.
-  */
+/*****************
+ * The .headers command
+ */
+COLLECT_HELP_TEXT[
+  ".headers on|off          Turn display of headers on or off",
+];
+DISPATCHABLE_COMMAND( headers 6 1 2 ){
+  p->showHeader = booleanValue(azArg[1]);
+  p->shellFlgs |= SHFLG_HeaderSet;
+  return 0;
+}
 
+/*****************
+ * The .help command
+ */
+COLLECT_HELP_TEXT[
+  ".help ?-all? ?PATTERN?   Show help text, just for PATTERN if given",
+];
+DISPATCHABLE_COMMAND( help 3 0 0 ){
+  if( nArg>=2 ){
+    if( showHelp(p->out, azArg[1])==0 ){
+      utf8_printf(p->out, "Nothing matches '%s'\n", azArg[1]);
+    }
+  }else{
+    showHelp(p->out, 0);
+  }
+  /* Help pleas never fail! */
+  return 0;
+}
 
-  if( c=='e' && strncmp(azArg[0], "exit", n)==0 ){
-    if( nArg>1 && (rc = (int)integerValue(azArg[1]))!=0 ) exit(rc);
-    rc = 2;
-  }else
+/*****************
+ * The .import command
+ */
+COLLECT_HELP_TEXT[
+  ".import FILE TABLE       Import data from FILE into TABLE",
+  "   Options:",
+  "     --ascii               Use \\037 and \\036 as column and row separators",
+  "     --csv                 Use , and \\n as column and row separators",
+  "     --skip N              Skip the first N rows of input",
+  "     -v                    \"Verbose\" - increase auxiliary output",
+  "   Notes:",
+  "     *  If TABLE does not exist, it is created.  The first row of input",
+  "        determines the column names.",
+  "     *  If neither --csv or --ascii are used, the input mode is derived",
+  "        from the \".mode\" output mode",
+  "     *  If FILE begins with \"|\" then it is a command that generates the",
+  "        input text.",
+];
+DISPATCHABLE_COMMAND( import ? 3 7 ){
+  char *zTable = 0;           /* Insert data into this table */
+  char *zFile = 0;            /* Name of file to extra content from */
+  sqlite3_stmt *pStmt = NULL; /* A statement */
+  int nCol;                   /* Number of columns in the table */
+  int nByte;                  /* Number of bytes in an SQL string */
+  int i, j;                   /* Loop counters */
+  int needCommit;             /* True to COMMIT or ROLLBACK at end */
+  int nSep;                   /* Number of bytes in p->colSeparator[] */
+  char *zSql;                 /* An SQL statement */
+  ImportCtx sCtx;             /* Reader context */
+  char *(SQLITE_CDECL *xRead)(ImportCtx*); /* Func to read one value */
+  int eVerbose = 0;           /* Larger for more console output */
+  int nSkip = 0;              /* Initial lines to skip */
+  int useOutputMode = 1;      /* Use output mode to determine separators */
+  int rc = 0;
 
-  /* The ".explain" command is automatic now.  It is largely pointless.  It
-  ** retained purely for backwards compatibility */
-  if( c=='e' && strncmp(azArg[0], "explain", n)==0 ){
-    int val = 1;
-    if( nArg>=2 ){
-      if( strcmp(azArg[1],"auto")==0 ){
-        val = 99;
+  memset(&sCtx, 0, sizeof(sCtx));
+  if( p->mode==MODE_Ascii ){
+    xRead = ascii_read_one_field;
+  }else{
+    xRead = csv_read_one_field;
+  }
+  for(i=1; i<nArg; i++){
+    char *z = azArg[i];
+    if( z[0]=='-' && z[1]=='-' ) z++;
+    if( z[0]!='-' ){
+      if( zFile==0 ){
+        zFile = z;
+      }else if( zTable==0 ){
+        zTable = z;
       }else{
-        val =  booleanValue(azArg[1]);
+        utf8_printf(p->out, "ERROR: extra argument: \"%s\".  Usage:\n", z);
+        showHelp(p->out, "import");
+        return 1;
       }
+    }else if( strcmp(z,"-v")==0 ){
+      eVerbose++;
+    }else if( strcmp(z,"-skip")==0 && i<nArg-1 ){
+      nSkip = integerValue(azArg[++i]);
+    }else if( strcmp(z,"-ascii")==0 ){
+      sCtx.cColSep = SEP_Unit[0];
+      sCtx.cRowSep = SEP_Record[0];
+      xRead = ascii_read_one_field;
+      useOutputMode = 0;
+    }else if( strcmp(z,"-csv")==0 ){
+      sCtx.cColSep = ',';
+      sCtx.cRowSep = '\n';
+      xRead = csv_read_one_field;
+      useOutputMode = 0;
+    }else{
+      utf8_printf(p->out, "ERROR: unknown option: \"%s\".  Usage:\n", z);
+      showHelp(p->out, "import");
+      return 1;
+    }
+  }
+  if( zTable==0 ){
+    utf8_printf(p->out, "ERROR: missing %s argument. Usage:\n",
+                zFile==0 ? "FILE" : "TABLE");
+    showHelp(p->out, "import");
+    return 1;
+  }
+  seenInterrupt = 0;
+  open_db(p, 0);
+  if( useOutputMode ){
+    /* If neither the --csv or --ascii options are specified, then set
+    ** the column and row separator characters from the output mode. */
+    nSep = strlen30(p->colSeparator);
+    if( nSep==0 ){
+      raw_printf(stderr,
+                 "Error: non-null column separator required for import\n");
+      return 1;
     }
-    if( val==1 && p->mode!=MODE_Explain ){
-      p->normalMode = p->mode;
-      p->mode = MODE_Explain;
-      p->autoExplain = 0;
-    }else if( val==0 ){
-      if( p->mode==MODE_Explain ) p->mode = p->normalMode;
-      p->autoExplain = 0;
-    }else if( val==99 ){
-      if( p->mode==MODE_Explain ) p->mode = p->normalMode;
-      p->autoExplain = 1;
+    if( nSep>1 ){
+      raw_printf(stderr, 
+                 "Error: multi-character column separators not allowed"
+                 " for import\n");
+      return 1;
     }
-  }else
-
-#ifndef SQLITE_OMIT_VIRTUALTABLE
-  if( c=='e' && strncmp(azArg[0], "expert", n)==0 ){
-    open_db(p, 0);
-    expertDotCommand(p, azArg, nArg);
-  }else
+    nSep = strlen30(p->rowSeparator);
+    if( nSep==0 ){
+      raw_printf(stderr,
+                 "Error: non-null row separator required for import\n");
+      return 1;
+    }
+    if( nSep==2 && p->mode==MODE_Csv && strcmp(p->rowSeparator,SEP_CrLf)==0 ){
+      /* When importing CSV (only), if the row separator is set to the
+      ** default output row separator, change it to the default input
+      ** row separator.  This avoids having to maintain different input
+      ** and output row separators. */
+      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
+      nSep = strlen30(p->rowSeparator);
+    }
+    if( nSep>1 ){
+      raw_printf(stderr, "Error: multi-character row separators not allowed"
+                 " for import\n");
+      return 1;
+    }
+    sCtx.cColSep = p->colSeparator[0];
+    sCtx.cRowSep = p->rowSeparator[0];
+  }
+  sCtx.zFile = zFile;
+  sCtx.nLine = 1;
+  if( sCtx.zFile[0]=='|' ){
+#ifdef SQLITE_OMIT_POPEN
+    raw_printf(stderr, "Error: pipes are not supported in this OS\n");
+    return 1;
+#else
+    sCtx.in = popen(sCtx.zFile+1, "r");
+    sCtx.zFile = "<pipe>";
+    sCtx.xCloser = pclose;
 #endif
-
-  if( c=='f' && strncmp(azArg[0], "filectrl", n)==0 ){
-    static const struct {
-       const char *zCtrlName;   /* Name of a test-control option */
-       int ctrlCode;            /* Integer code for that option */
-       const char *zUsage;      /* Usage notes */
-    } aCtrl[] = {
-      { "chunk_size",     SQLITE_FCNTL_CHUNK_SIZE,      "SIZE"           },
-      { "data_version",   SQLITE_FCNTL_DATA_VERSION,    ""               },
-      { "has_moved",      SQLITE_FCNTL_HAS_MOVED,       ""               },  
-      { "lock_timeout",   SQLITE_FCNTL_LOCK_TIMEOUT,    "MILLISEC"       },
-      { "persist_wal",    SQLITE_FCNTL_PERSIST_WAL,     "[BOOLEAN]"      },
-   /* { "pragma",         SQLITE_FCNTL_PRAGMA,          "NAME ARG"       },*/
-      { "psow",       SQLITE_FCNTL_POWERSAFE_OVERWRITE, "[BOOLEAN]"      },
-      { "reserve_bytes",  SQLITE_FCNTL_RESERVE_BYTES,   "[N]"            },
-      { "size_limit",     SQLITE_FCNTL_SIZE_LIMIT,      "[LIMIT]"        },
-      { "tempfilename",   SQLITE_FCNTL_TEMPFILENAME,    ""               },
-   /* { "win32_av_retry", SQLITE_FCNTL_WIN32_AV_RETRY,  "COUNT DELAY"    },*/
-    };
-    int filectrl = -1;
-    int iCtrl = -1;
-    sqlite3_int64 iRes = 0;  /* Integer result to display if rc2==1 */
-    int isOk = 0;            /* 0: usage  1: %lld  2: no-result */
-    int n2, i;
-    const char *zCmd = 0;
-    const char *zSchema = 0;
-
-    open_db(p, 0);
-    zCmd = nArg>=2 ? azArg[1] : "help";
-
-    if( zCmd[0]=='-' 
-     && (strcmp(zCmd,"--schema")==0 || strcmp(zCmd,"-schema")==0)
-     && nArg>=4
-    ){
-      zSchema = azArg[2];
-      for(i=3; i<nArg; i++) azArg[i-2] = azArg[i];
-      nArg -= 2;
-      zCmd = azArg[1];
+  }else{
+    sCtx.in = fopen(sCtx.zFile, "rb");
+    sCtx.xCloser = fclose;
+  }
+  if( sCtx.in==0 ){
+    utf8_printf(stderr, "Error: cannot open \"%s\"\n", zFile);
+    return 1;
+  }
+  if( eVerbose>=2 || (eVerbose>=1 && useOutputMode) ){
+    char zSep[2];
+    zSep[1] = 0;
+    zSep[0] = sCtx.cColSep;
+    utf8_printf(p->out, "Column separator ");
+    output_c_string(p->out, zSep);
+    utf8_printf(p->out, ", row separator ");
+    zSep[0] = sCtx.cRowSep;
+    output_c_string(p->out, zSep);
+    utf8_printf(p->out, "\n");
+  }
+  while( (nSkip--)>0 ){
+    while( xRead(&sCtx) && sCtx.cTerm==sCtx.cColSep ){}
+  }
+  zSql = sqlite3_mprintf("SELECT * FROM \"%w\"", zTable);
+  if( zSql==0 ){
+    import_cleanup(&sCtx);
+    shell_out_of_memory();
+  }
+  nByte = strlen30(zSql);
+  rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+  import_append_char(&sCtx, 0);    /* To ensure sCtx.z is allocated */
+  if( rc && sqlite3_strglob("no such table: *", sqlite3_errmsg(p->db))==0 ){
+    char *zCreate = sqlite3_mprintf("CREATE TABLE \"%w\"", zTable);
+    char cSep = '(';
+    while( xRead(&sCtx) ){
+      zCreate = sqlite3_mprintf("%z%c\n  \"%w\" TEXT", zCreate, cSep, sCtx.z);
+      cSep = ',';
+      if( sCtx.cTerm!=sCtx.cColSep ) break;
+    }
+    if( cSep=='(' ){
+      sqlite3_free(zCreate);
+      import_cleanup(&sCtx);
+      utf8_printf(stderr,"%s: empty file\n", sCtx.zFile);
+      return 1;
     }
-
-    /* The argument can optionally begin with "-" or "--" */
-    if( zCmd[0]=='-' && zCmd[1] ){
-      zCmd++;
-      if( zCmd[0]=='-' && zCmd[1] ) zCmd++;
+    zCreate = sqlite3_mprintf("%z\n)", zCreate);
+    if( eVerbose>=1 ){
+      utf8_printf(p->out, "%s\n", zCreate);
     }
-
-    /* --help lists all file-controls */
-    if( strcmp(zCmd,"help")==0 ){
-      utf8_printf(p->out, "Available file-controls:\n");
-      for(i=0; i<ArraySize(aCtrl); i++){
-        utf8_printf(p->out, "  .filectrl %s %s\n",
-                    aCtrl[i].zCtrlName, aCtrl[i].zUsage);
-      }
-      rc = 1;
-      goto meta_command_exit;
+    rc = sqlite3_exec(p->db, zCreate, 0, 0, 0);
+    sqlite3_free(zCreate);
+    if( rc ){
+      utf8_printf(stderr, "CREATE TABLE \"%s\"(...) failed: %s\n", zTable,
+                  sqlite3_errmsg(p->db));
+      import_cleanup(&sCtx);
+      return 1;
     }
-
-    /* convert filectrl text option to value. allow any unique prefix
-    ** of the option name, or a numerical value. */
-    n2 = strlen30(zCmd);
-    for(i=0; i<ArraySize(aCtrl); i++){
-      if( strncmp(zCmd, aCtrl[i].zCtrlName, n2)==0 ){
-        if( filectrl<0 ){
-          filectrl = aCtrl[i].ctrlCode;
-          iCtrl = i;
-        }else{
-          utf8_printf(stderr, "Error: ambiguous file-control: \"%s\"\n"
-                              "Use \".filectrl --help\" for help\n", zCmd);
-          rc = 1;
-          goto meta_command_exit;
-        }
+    rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+  }
+  sqlite3_free(zSql);
+  if( rc ){
+    if (pStmt) sqlite3_finalize(pStmt);
+    utf8_printf(stderr,"Error: %s\n", sqlite3_errmsg(p->db));
+    import_cleanup(&sCtx);
+    return 1;
+  }
+  nCol = sqlite3_column_count(pStmt);
+  sqlite3_finalize(pStmt);
+  pStmt = 0;
+  if( nCol==0 ) return 0; /* no columns, no error */
+  zSql = sqlite3_malloc64( nByte*2 + 20 + nCol*2 );
+  if( zSql==0 ){
+    import_cleanup(&sCtx);
+    shell_out_of_memory();
+  }
+  sqlite3_snprintf(nByte+20, zSql, "INSERT INTO \"%w\" VALUES(?", zTable);
+  j = strlen30(zSql);
+  for(i=1; i<nCol; i++){
+    zSql[j++] = ',';
+    zSql[j++] = '?';
+  }
+  zSql[j++] = ')';
+  zSql[j] = 0;
+  if( eVerbose>=2 ){
+    utf8_printf(p->out, "Insert using: %s\n", zSql);
+  }
+  rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+  sqlite3_free(zSql);
+  if( rc ){
+    utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
+    if (pStmt) sqlite3_finalize(pStmt);
+    import_cleanup(&sCtx);
+    return 1;
+  }
+  needCommit = sqlite3_get_autocommit(p->db);
+  if( needCommit ) sqlite3_exec(p->db, "BEGIN", 0, 0, 0);
+  do{
+    int startLine = sCtx.nLine;
+    for(i=0; i<nCol; i++){
+      char *z = xRead(&sCtx);
+      /*
+      ** Did we reach end-of-file before finding any columns?
+      ** If so, stop instead of NULL filling the remaining columns.
+      */
+      if( z==0 && i==0 ) break;
+      /*
+      ** Did we reach end-of-file OR end-of-line before finding any
+      ** columns in ASCII mode?  If so, stop instead of NULL filling
+      ** the remaining columns.
+      */
+      if( p->mode==MODE_Ascii && (z==0 || z[0]==0) && i==0 ) break;
+      sqlite3_bind_text(pStmt, i+1, z, -1, SQLITE_TRANSIENT);
+      if( i<nCol-1 && sCtx.cTerm!=sCtx.cColSep ){
+        utf8_printf(stderr, "%s:%d: expected %d columns but found %d - "
+                    "filling the rest with NULL\n",
+                    sCtx.zFile, startLine, nCol, i+1);
+        i += 2;
+        while( i<=nCol ){ sqlite3_bind_null(pStmt, i); i++; }
       }
     }
-    if( filectrl<0 ){
-      utf8_printf(stderr,"Error: unknown file-control: %s\n"
-                         "Use \".filectrl --help\" for help\n", zCmd);
-    }else{
-      switch(filectrl){
-        case SQLITE_FCNTL_SIZE_LIMIT: {
-          if( nArg!=2 && nArg!=3 ) break;
-          iRes = nArg==3 ? integerValue(azArg[2]) : -1;
-          sqlite3_file_control(p->db, zSchema, SQLITE_FCNTL_SIZE_LIMIT, &iRes);
-          isOk = 1;
-          break;
-        }
-        case SQLITE_FCNTL_LOCK_TIMEOUT:
-        case SQLITE_FCNTL_CHUNK_SIZE: {
-          int x;
-          if( nArg!=3 ) break;
-          x = (int)integerValue(azArg[2]);
-          sqlite3_file_control(p->db, zSchema, filectrl, &x);
-          isOk = 2;
-          break;
-        }
-        case SQLITE_FCNTL_PERSIST_WAL:
-        case SQLITE_FCNTL_POWERSAFE_OVERWRITE: {
-          int x;
-          if( nArg!=2 && nArg!=3 ) break;
-          x = nArg==3 ? booleanValue(azArg[2]) : -1;
-          sqlite3_file_control(p->db, zSchema, filectrl, &x);
-          iRes = x;
-          isOk = 1;
-          break;
-        }
-        case SQLITE_FCNTL_DATA_VERSION:
-        case SQLITE_FCNTL_HAS_MOVED: {
-          int x;
-          if( nArg!=2 ) break;
-          sqlite3_file_control(p->db, zSchema, filectrl, &x);
-          iRes = x;
-          isOk = 1;
-          break;
-        }
-        case SQLITE_FCNTL_TEMPFILENAME: {
-          char *z = 0;
-          if( nArg!=2 ) break;
-          sqlite3_file_control(p->db, zSchema, filectrl, &z);
-          if( z ){
-            utf8_printf(p->out, "%s\n", z);
-            sqlite3_free(z);
-          }
-          isOk = 2;
-          break;
-        }
-        case SQLITE_FCNTL_RESERVE_BYTES: {
-          int x;
-          if( nArg>=3 ){
-            x = atoi(azArg[2]);
-            sqlite3_file_control(p->db, zSchema, filectrl, &x);
-          }
-          x = -1;
-          sqlite3_file_control(p->db, zSchema, filectrl, &x);
-          utf8_printf(p->out,"%d\n", x);
-          isOk = 2;
-          break;
-        }
-      }
+    if( sCtx.cTerm==sCtx.cColSep ){
+      do{
+        xRead(&sCtx);
+        i++;
+      }while( sCtx.cTerm==sCtx.cColSep );
+      utf8_printf(stderr, "%s:%d: expected %d columns but found %d - "
+                  "extras ignored\n",
+                  sCtx.zFile, startLine, nCol, i);
     }
-    if( isOk==0 && iCtrl>=0 ){
-      utf8_printf(p->out, "Usage: .filectrl %s %s\n", zCmd,aCtrl[iCtrl].zUsage);
-      rc = 1;
-    }else if( isOk==1 ){
-      char zBuf[100];
-      sqlite3_snprintf(sizeof(zBuf), zBuf, "%lld", iRes);
-      raw_printf(p->out, "%s\n", zBuf);
+    if( i>=nCol ){
+      sqlite3_step(pStmt);
+      rc = sqlite3_reset(pStmt);
+      if( rc!=SQLITE_OK ){
+        utf8_printf(stderr, "%s:%d: INSERT failed: %s\n", sCtx.zFile,
+                    startLine, sqlite3_errmsg(p->db));
+        sCtx.nErr++;
+      }else{
+        sCtx.nRow++;
+      }
     }
-  }else
+  }while( sCtx.cTerm!=EOF );
 
-  if( c=='f' && strncmp(azArg[0], "fullschema", n)==0 ){
-    ShellState data;
-    int doStats = 0;
-    memcpy(&data, p, sizeof(data));
-    data.showHeader = 0;
-    data.cMode = data.mode = MODE_Semi;
-    if( nArg==2 && optionMatch(azArg[1], "indent") ){
-      data.cMode = data.mode = MODE_Pretty;
-      nArg = 1;
-    }
-    if( nArg!=1 ){
-      raw_printf(stderr, "Usage: .fullschema ?--indent?\n");
-      rc = 1;
-      goto meta_command_exit;
+  import_cleanup(&sCtx);
+  sqlite3_finalize(pStmt);
+  if( needCommit ) sqlite3_exec(p->db, "COMMIT", 0, 0, 0);
+  if( eVerbose>0 ){
+    utf8_printf(p->out,
+      "Added %d rows with %d errors using %d lines of input\n",
+      sCtx.nRow, sCtx.nErr, sCtx.nLine-1);
+  }
+  return 0;
+}
+
+
+/*****************
+ * The .imposter, .iotrace, limit, lint, .load and .log commands
+ */
+CONDITION_COMMAND( imposter !defined(SQLITE_OMIT_TEST_CONTROL) );
+CONDITION_COMMAND( iotrace defined(SQLITE_ENABLE_IOTRACE) );
+CONDITION_COMMAND( load !defined(SQLITE_OMIT_LOAD_EXTENSION) );
+COLLECT_HELP_TEXT[
+  ".imposter INDEX TABLE    Create imposter table TABLE on index INDEX",
+  ".iotrace FILE            Enable I/O diagnostic logging to FILE",
+  ".limit ?LIMIT? ?VAL?     Display or change the value of an SQLITE_LIMIT",
+  ".lint OPTIONS            Report potential schema issues.",
+  "     Options:",
+  "        fkey-indexes     Find missing foreign key indexes",
+  ".load FILE ?ENTRY?       Load an extension library",
+  ".log FILE|off            Turn logging on or off.  FILE can be stderr/stdout",
+];
+DISPATCHABLE_COMMAND( imposter ? 3 3 ){
+  int rc = 0;
+  char *zSql;
+  char *zCollist = 0;
+  sqlite3_stmt *pStmt;
+  int tnum = 0;
+  int isWO = 0;  /* True if making an imposter of a WITHOUT ROWID table */
+  int lenPK = 0; /* Length of the PRIMARY KEY string for isWO tables */
+  int i;
+  if( !(nArg==3 || (nArg==2 && sqlite3_stricmp(azArg[1],"off")==0)) ){
+    utf8_printf(stderr, "Usage: .imposter INDEX IMPOSTER\n"
+                "       .imposter off\n");
+    /* Also allowed, but not documented:
+    **
+    **    .imposter TABLE IMPOSTER
+    **
+    ** where TABLE is a WITHOUT ROWID table.  In that case, the
+    ** imposter is another WITHOUT ROWID table with the columns in
+    ** storage order. */
+    return 1;
+  }
+  open_db(p, 0);
+  if( nArg==2 ){
+    sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 0, 1);
+    return 0;
+  }
+  zSql = sqlite3_mprintf(
+                         "SELECT rootpage, 0 FROM sqlite_schema"
+                         " WHERE name='%q' AND type='index'"
+                         "UNION ALL "
+                         "SELECT rootpage, 1 FROM sqlite_schema"
+                         " WHERE name='%q' AND type='table'"
+                         "   AND sql LIKE '%%without%%rowid%%'",
+                         azArg[1], azArg[1]
+                         );
+  sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+  sqlite3_free(zSql);
+  if( sqlite3_step(pStmt)==SQLITE_ROW ){
+    tnum = sqlite3_column_int(pStmt, 0);
+    isWO = sqlite3_column_int(pStmt, 1);
+  }
+  sqlite3_finalize(pStmt);
+  zSql = sqlite3_mprintf("PRAGMA index_xinfo='%q'", azArg[1]);
+  rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+  sqlite3_free(zSql);
+  i = 0;
+  while( sqlite3_step(pStmt)==SQLITE_ROW ){
+    char zLabel[20];
+    const char *zCol = (const char*)sqlite3_column_text(pStmt,2);
+    i++;
+    if( zCol==0 ){
+      if( sqlite3_column_int(pStmt,1)==-1 ){
+        zCol = "_ROWID_";
+      }else{
+        sqlite3_snprintf(sizeof(zLabel),zLabel,"expr%d",i);
+        zCol = zLabel;
+      }
     }
-    open_db(p, 0);
-    rc = sqlite3_exec(p->db,
-       "SELECT sql FROM"
-       "  (SELECT sql sql, type type, tbl_name tbl_name, name name, rowid x"
-       "     FROM sqlite_schema UNION ALL"
-       "   SELECT sql, type, tbl_name, name, rowid FROM sqlite_temp_schema) "
-       "WHERE type!='meta' AND sql NOTNULL AND name NOT LIKE 'sqlite_%' "
-       "ORDER BY x",
-       callback, &data, 0
-    );
-    if( rc==SQLITE_OK ){
-      sqlite3_stmt *pStmt;
-      rc = sqlite3_prepare_v2(p->db,
-               "SELECT rowid FROM sqlite_schema"
-               " WHERE name GLOB 'sqlite_stat[134]'",
-               -1, &pStmt, 0);
-      doStats = sqlite3_step(pStmt)==SQLITE_ROW;
-      sqlite3_finalize(pStmt);
+    if( isWO && lenPK==0 && sqlite3_column_int(pStmt,5)==0 && zCollist ){
+      lenPK = (int)strlen(zCollist);
     }
-    if( doStats==0 ){
-      raw_printf(p->out, "/* No STAT tables available */\n");
+    if( zCollist==0 ){
+      zCollist = sqlite3_mprintf("\"%w\"", zCol);
     }else{
-      raw_printf(p->out, "ANALYZE sqlite_schema;\n");
-      data.cMode = data.mode = MODE_Insert;
-      data.zDestTable = "sqlite_stat1";
-      shell_exec(&data, "SELECT * FROM sqlite_stat1", 0);
-      data.zDestTable = "sqlite_stat4";
-      shell_exec(&data, "SELECT * FROM sqlite_stat4", 0);
-      raw_printf(p->out, "ANALYZE sqlite_schema;\n");
+      zCollist = sqlite3_mprintf("%z,\"%w\"", zCollist, zCol);
     }
-  }else
-
-  if( c=='h' && strncmp(azArg[0], "headers", n)==0 ){
-    if( nArg==2 ){
-      p->showHeader = booleanValue(azArg[1]);
-      p->shellFlgs |= SHFLG_HeaderSet;
+  }
+  sqlite3_finalize(pStmt);
+  if( i==0 || tnum==0 ){
+    utf8_printf(stderr, "no such index: \"%s\"\n", azArg[1]);
+    sqlite3_free(zCollist);
+    return 1;
+  }
+  if( lenPK==0 ) lenPK = 100000;
+  zSql = sqlite3_mprintf(
+                         "CREATE TABLE \"%w\"(%s,PRIMARY KEY(%.*s))WITHOUT ROWID",
+                         azArg[2], zCollist, lenPK, zCollist);
+  sqlite3_free(zCollist);
+  rc = sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 1, tnum);
+  if( rc==SQLITE_OK ){
+    rc = sqlite3_exec(p->db, zSql, 0, 0, 0);
+    sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 0, 0);
+    if( rc ){
+      utf8_printf(stderr, "Error in [%s]: %s\n", zSql, sqlite3_errmsg(p->db));
     }else{
-      raw_printf(stderr, "Usage: .headers on|off\n");
-      rc = 1;
+      utf8_printf(stdout, "%s;\n", zSql);
+      raw_printf(stdout,
+                 "WARNING: writing to an imposter table will corrupt the \"%s\" %s!\n",
+                 azArg[1], isWO ? "table" : "index"
+                 );
     }
-  }else
-
-  if( c=='i' && strncmp(azArg[0], "import", n)==0 ){
-    char *zTable = 0;           /* Insert data into this table */
-    char *zFile = 0;            /* Name of file to extra content from */
-    sqlite3_stmt *pStmt = NULL; /* A statement */
-    int nCol;                   /* Number of columns in the table */
-    int nByte;                  /* Number of bytes in an SQL string */
-    int i, j;                   /* Loop counters */
-    int needCommit;             /* True to COMMIT or ROLLBACK at end */
-    int nSep;                   /* Number of bytes in p->colSeparator[] */
-    char *zSql;                 /* An SQL statement */
-    ImportCtx sCtx;             /* Reader context */
-    char *(SQLITE_CDECL *xRead)(ImportCtx*); /* Func to read one value */
-    int eVerbose = 0;           /* Larger for more console output */
-    int nSkip = 0;              /* Initial lines to skip */
-    int useOutputMode = 1;      /* Use output mode to determine separators */
-
-    memset(&sCtx, 0, sizeof(sCtx));
-    if( p->mode==MODE_Ascii ){
-      xRead = ascii_read_one_field;
+  }else{
+    raw_printf(stderr, "SQLITE_TESTCTRL_IMPOSTER returns %d\n", rc);
+  }
+  sqlite3_free(zSql);
+  return rc != 0;
+}
+DISPATCHABLE_COMMAND( iotrace ? 2 2 ){
+  SQLITE_API extern void (SQLITE_CDECL *sqlite3IoTrace)(const char*, ...);
+  if( iotrace && iotrace!=stdout ) fclose(iotrace);
+  iotrace = 0;
+  if( nArg<2 ){
+    sqlite3IoTrace = 0;
+  }else if( strcmp(azArg[1], "-")==0 ){
+    sqlite3IoTrace = iotracePrintf;
+    iotrace = stdout;
+  }else{
+    iotrace = fopen(azArg[1], "w");
+    if( iotrace==0 ){
+      utf8_printf(stderr, "Error: cannot open \"%s\"\n", azArg[1]);
+      sqlite3IoTrace = 0;
+      return 1;
     }else{
-      xRead = csv_read_one_field;
+      sqlite3IoTrace = iotracePrintf;
     }
-    for(i=1; i<nArg; i++){
-      char *z = azArg[i];
-      if( z[0]=='-' && z[1]=='-' ) z++;
-      if( z[0]!='-' ){
-        if( zFile==0 ){
-          zFile = z;
-        }else if( zTable==0 ){
-          zTable = z;
+  }
+  return 0;
+}
+DISPATCHABLE_COMMAND( limits 5 1 3 ){
+  static const struct {
+    const char *zLimitName;   /* Name of a limit */
+    int limitCode;            /* Integer code for that limit */
+  } aLimit[] = {
+    { "length",                SQLITE_LIMIT_LENGTH                    },
+    { "sql_length",            SQLITE_LIMIT_SQL_LENGTH                },
+    { "column",                SQLITE_LIMIT_COLUMN                    },
+    { "expr_depth",            SQLITE_LIMIT_EXPR_DEPTH                },
+    { "compound_select",       SQLITE_LIMIT_COMPOUND_SELECT           },
+    { "vdbe_op",               SQLITE_LIMIT_VDBE_OP                   },
+    { "function_arg",          SQLITE_LIMIT_FUNCTION_ARG              },
+    { "attached",              SQLITE_LIMIT_ATTACHED                  },
+    { "like_pattern_length",   SQLITE_LIMIT_LIKE_PATTERN_LENGTH       },
+    { "variable_number",       SQLITE_LIMIT_VARIABLE_NUMBER           },
+    { "trigger_depth",         SQLITE_LIMIT_TRIGGER_DEPTH             },
+    { "worker_threads",        SQLITE_LIMIT_WORKER_THREADS            },
+  };
+  int i, n2;
+  open_db(p, 0);
+  if( nArg==1 ){
+    for(i=0; i<ArraySize(aLimit); i++){
+      printf("%20s %d\n", aLimit[i].zLimitName,
+             sqlite3_limit(p->db, aLimit[i].limitCode, -1));
+    }
+  }else if( nArg>3 ){
+    raw_printf(stderr, "Usage: .limit NAME ?NEW-VALUE?\n");
+    return 1;
+  }else{
+    int iLimit = -1;
+    n2 = strlen30(azArg[1]);
+    for(i=0; i<ArraySize(aLimit); i++){
+      if( sqlite3_strnicmp(aLimit[i].zLimitName, azArg[1], n2)==0 ){
+        if( iLimit<0 ){
+          iLimit = i;
         }else{
-          utf8_printf(p->out, "ERROR: extra argument: \"%s\".  Usage:\n", z);
-          showHelp(p->out, "import");
-          rc = 1;
-          goto meta_command_exit;
+          utf8_printf(stderr, "ambiguous limit: \"%s\"\n", azArg[1]);
+          return 1;
         }
-      }else if( strcmp(z,"-v")==0 ){
-        eVerbose++;
-      }else if( strcmp(z,"-skip")==0 && i<nArg-1 ){
-        nSkip = integerValue(azArg[++i]);
-      }else if( strcmp(z,"-ascii")==0 ){
-        sCtx.cColSep = SEP_Unit[0];
-        sCtx.cRowSep = SEP_Record[0];
-        xRead = ascii_read_one_field;
-        useOutputMode = 0;
-      }else if( strcmp(z,"-csv")==0 ){
-        sCtx.cColSep = ',';
-        sCtx.cRowSep = '\n';
-        xRead = csv_read_one_field;
-        useOutputMode = 0;
-      }else{
-        utf8_printf(p->out, "ERROR: unknown option: \"%s\".  Usage:\n", z);
-        showHelp(p->out, "import");
-        rc = 1;
-        goto meta_command_exit;
       }
     }
-    if( zTable==0 ){
-      utf8_printf(p->out, "ERROR: missing %s argument. Usage:\n",
-                  zFile==0 ? "FILE" : "TABLE");
-      showHelp(p->out, "import");
-      rc = 1;
-      goto meta_command_exit;
+    if( iLimit<0 ){
+      utf8_printf(stderr, "unknown limit: \"%s\"\n"
+                  "enter \".limits\" with no arguments for a list.\n",
+                  azArg[1]);
+      return 1;
     }
-    seenInterrupt = 0;
-    open_db(p, 0);
-    if( useOutputMode ){
-      /* If neither the --csv or --ascii options are specified, then set
-      ** the column and row separator characters from the output mode. */
-      nSep = strlen30(p->colSeparator);
-      if( nSep==0 ){
-        raw_printf(stderr,
-                   "Error: non-null column separator required for import\n");
-        rc = 1;
-        goto meta_command_exit;
-      }
-      if( nSep>1 ){
-        raw_printf(stderr, 
-              "Error: multi-character column separators not allowed"
-              " for import\n");
-        rc = 1;
-        goto meta_command_exit;
-      }
-      nSep = strlen30(p->rowSeparator);
-      if( nSep==0 ){
-        raw_printf(stderr,
-            "Error: non-null row separator required for import\n");
-        rc = 1;
-        goto meta_command_exit;
-      }
-      if( nSep==2 && p->mode==MODE_Csv && strcmp(p->rowSeparator,SEP_CrLf)==0 ){
-        /* When importing CSV (only), if the row separator is set to the
-        ** default output row separator, change it to the default input
-        ** row separator.  This avoids having to maintain different input
-        ** and output row separators. */
-        sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
-        nSep = strlen30(p->rowSeparator);
-      }
-      if( nSep>1 ){
-        raw_printf(stderr, "Error: multi-character row separators not allowed"
-                           " for import\n");
-        rc = 1;
-        goto meta_command_exit;
+    if( nArg==3 ){
+      sqlite3_limit(p->db, aLimit[iLimit].limitCode,
+                    (int)integerValue(azArg[2]));
+    }
+    printf("%20s %d\n", aLimit[iLimit].zLimitName,
+           sqlite3_limit(p->db, aLimit[iLimit].limitCode, -1));
+  }
+  return 0;
+}
+
+DISPATCHABLE_COMMAND( lint 3 1 0 ){
+  open_db(p, 0);
+  int n = (nArg>=2 ? strlen30(azArg[1]) : 0);
+  if( n>0 && !sqlite3_strnicmp(azArg[1], "fkey-indexes", n) ){
+    return lintFkeyIndexes(p, azArg, nArg);
+  }
+  raw_printf(stderr,
+             "Usage %s sub-command ?switches...?\n"
+             "Where sub-commands are:\n"
+             "    fkey-indexes\n", azArg[0]);
+  return 1;
+}
+
+DISPATCHABLE_COMMAND( load ? 2 3 ){
+  const char *zFile, *zProc;
+  char *zErrMsg = 0;
+  zFile = azArg[1];
+  zProc = nArg>=3 ? azArg[2] : 0;
+  open_db(p, 0);
+  if( SQLITE_OK!=sqlite3_load_extension(p->db, zFile, zProc, &zErrMsg)){
+    utf8_printf(stderr, "Error: %s\n", zErrMsg);
+    sqlite3_free(zErrMsg);
+    return 1;
+  }
+  return 0;
+}
+
+DISPATCHABLE_COMMAND( log ? 2 2 ){
+  const char *zFile = azArg[1];
+  output_file_close(p->pLog);
+  p->pLog = output_file_open(zFile, 0);
+  return 0;
+}
+
+/*****************
+ * The .mode command
+ */
+COLLECT_HELP_TEXT[
+  ".mode MODE ?TABLE?       Set output mode",
+  "   MODE is one of:",
+  "     ascii     Columns/rows delimited by 0x1F and 0x1E",
+  "     box       Tables using unicode box-drawing characters",
+  "     csv       Comma-separated values",
+  "     column    Output in columns.  (See .width)",
+  "     html      HTML <table> code",
+  "     insert    SQL insert statements for TABLE",
+  "     json      Results in a JSON array",
+  "     line      One value per line",
+  "     list      Values delimited by \"|\"",
+  "     markdown  Markdown table format",
+  "     quote     Escape answers as for SQL",
+  "     table     ASCII-art table",
+  "     tabs      Tab-separated values",
+  "     tcl       TCL list elements",
+];
+DISPATCHABLE_COMMAND( mode ? 2 3 ){
+  const char *zMode = nArg>=2 ? azArg[1] : "";
+  int n2 = strlen30(zMode);
+  int c2 = zMode[0];
+  if( c2=='l' && n2>2 && strncmp(azArg[1],"lines",n2)==0 ){
+    p->mode = MODE_Line;
+    sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
+  }else if( c2=='c' && strncmp(azArg[1],"columns",n2)==0 ){
+    p->mode = MODE_Column;
+    if( (p->shellFlgs & SHFLG_HeaderSet)==0 ){
+      p->showHeader = 1;
+    }
+    sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
+  }else if( c2=='l' && n2>2 && strncmp(azArg[1],"list",n2)==0 ){
+    p->mode = MODE_List;
+    sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Column);
+    sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
+  }else if( c2=='h' && strncmp(azArg[1],"html",n2)==0 ){
+    p->mode = MODE_Html;
+  }else if( c2=='t' && strncmp(azArg[1],"tcl",n2)==0 ){
+    p->mode = MODE_Tcl;
+    sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Space);
+    sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
+  }else if( c2=='c' && strncmp(azArg[1],"csv",n2)==0 ){
+    p->mode = MODE_Csv;
+    sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma);
+    sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_CrLf);
+  }else if( c2=='t' && strncmp(azArg[1],"tabs",n2)==0 ){
+    p->mode = MODE_List;
+    sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Tab);
+  }else if( c2=='i' && strncmp(azArg[1],"insert",n2)==0 ){
+    p->mode = MODE_Insert;
+    set_table_name(p, nArg>=3 ? azArg[2] : "table");
+  }else if( c2=='q' && strncmp(azArg[1],"quote",n2)==0 ){
+    p->mode = MODE_Quote;
+    sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma);
+    sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
+  }else if( c2=='a' && strncmp(azArg[1],"ascii",n2)==0 ){
+    p->mode = MODE_Ascii;
+    sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Unit);
+    sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Record);
+  }else if( c2=='m' && strncmp(azArg[1],"markdown",n2)==0 ){
+    p->mode = MODE_Markdown;
+  }else if( c2=='t' && strncmp(azArg[1],"table",n2)==0 ){
+    p->mode = MODE_Table;
+  }else if( c2=='b' && strncmp(azArg[1],"box",n2)==0 ){
+    p->mode = MODE_Box;
+  }else if( c2=='j' && strncmp(azArg[1],"json",n2)==0 ){
+    p->mode = MODE_Json;
+  }else if( nArg==1 ){
+    raw_printf(p->out, "current output mode: %s\n", modeDescr[p->mode]);
+  }else{
+    raw_printf(stderr, "Error: mode should be one of: "
+               "ascii box column csv html insert json line\n"
+               " list markdown quote table tabs tcl\n");
+    return 1;
+  }
+  p->cMode = p->mode;
+  return 0;
+}
+
+/*****************
+ * The .oom command
+ */
+CONDITION_COMMAND( oom defined(SQLITE_DEBUG) );
+COLLECT_HELP_TEXT[
+  ".oom ?--repeat M? ?N?    Simulate an OOM error on the N-th allocation",
+];
+DISPATCHABLE_COMMAND( oom ? 1 4 ){
+  int i;
+  for(i=1; i<nArg; i++){
+    const char *z = azArg[i];
+    if( z[0]=='-' && z[1]=='-' ) z++;
+    if( strcmp(z,"-repeat")==0 ){
+      if( i==nArg-1 ){
+        raw_printf(p->out, "missing argument on \"%s\"\n", azArg[i]);
+        return 1;
+      }else{
+        oomRepeat = (int)integerValue(azArg[++i]);
       }
-      sCtx.cColSep = p->colSeparator[0];
-      sCtx.cRowSep = p->rowSeparator[0];
+    }else if( IsDigit(z[0]) ){
+      oomCounter = (int)integerValue(azArg[i]);
+    }else{
+      raw_printf(p->out, "unknown argument: \"%s\"\n", azArg[i]);
+      raw_printf(p->out, "Usage: .oom [--repeat N] [M]\n");
+      return 1;
     }
-    sCtx.zFile = zFile;
-    sCtx.nLine = 1;
-    if( sCtx.zFile[0]=='|' ){
-#ifdef SQLITE_OMIT_POPEN
-      raw_printf(stderr, "Error: pipes are not supported in this OS\n");
-      rc = 1;
-      goto meta_command_exit;
-#else
-      sCtx.in = popen(sCtx.zFile+1, "r");
-      sCtx.zFile = "<pipe>";
-      sCtx.xCloser = pclose;
+  }
+  raw_printf(p->out, "oomCounter = %d\n", oomCounter);
+  raw_printf(p->out, "oomRepeat  = %d\n", oomRepeat);
+  return 0;
+}
+
+/*****************
+ * The .open and .nullvalue commands
+ */
+COLLECT_HELP_TEXT[
+  ".open ?OPTIONS? ?FILE?   Close existing database and reopen FILE",
+  "     Options:",
+  "        --append        Use appendvfs to append database to the end of FILE",
+#ifndef SQLITE_OMIT_DESERIALIZE
+  "        --deserialize   Load into memory using sqlite3_deserialize()",
+  "        --hexdb         Load the output of \"dbtotxt\" as an in-memory db",
+  "        --maxsize N     Maximum size for --hexdb or --deserialized database",
 #endif
+  "        --new           Initialize FILE to an empty database",
+  "        --nofollow      Do not follow symbolic links",
+  "        --readonly      Open FILE readonly",
+  "        --zip           FILE is a ZIP archive",
+  ".nullvalue STRING        Use STRING in place of NULL values",
+];
+DISPATCHABLE_COMMAND( open 3 1 0 ){
+  char *zNewFilename = 0;  /* Name of the database file to open */
+  int iName = 1;           /* Index in azArg[] of the filename */
+  int newFlag = 0;         /* True to delete file before opening */
+  /* Close the existing database */
+  session_close_all(p);
+  close_db(p->db);
+  p->db = 0;
+  p->zDbFilename = 0;
+  sqlite3_free(p->zFreeOnClose);
+  p->zFreeOnClose = 0;
+  p->openMode = SHELL_OPEN_UNSPEC;
+  p->openFlags = 0;
+  p->szMax = 0;
+  /* Check for command-line arguments */
+  for(iName=1; iName<nArg; iName++){
+    const char *z = azArg[iName];
+    if( optionMatch(z,"new") ){
+      newFlag = 1;
+#ifdef SQLITE_HAVE_ZLIB
+    }else if( optionMatch(z, "zip") ){
+      p->openMode = SHELL_OPEN_ZIPFILE;
+#endif
+    }else if( optionMatch(z, "append") ){
+      p->openMode = SHELL_OPEN_APPENDVFS;
+    }else if( optionMatch(z, "readonly") ){
+      p->openMode = SHELL_OPEN_READONLY;
+    }else if( optionMatch(z, "nofollow") ){
+      p->openFlags |= SQLITE_OPEN_NOFOLLOW;
+    }else if( optionMatch(z, "excl") ){
+      p->openFlags |= SQLITE_OPEN_EXCLUSIVE;
+#ifndef SQLITE_OMIT_DESERIALIZE
+    }else if( optionMatch(z, "deserialize") ){
+      p->openMode = SHELL_OPEN_DESERIALIZE;
+    }else if( optionMatch(z, "hexdb") ){
+      p->openMode = SHELL_OPEN_HEXDB;
+    }else if( optionMatch(z, "maxsize") && iName+1<nArg ){
+      p->szMax = integerValue(azArg[++iName]);
+#endif /* SQLITE_OMIT_DESERIALIZE */
+    }else if( z[0]=='-' ){
+      utf8_printf(stderr, "unknown option: %s\n", z);
+      return 1;
+    }else if( zNewFilename ){
+      utf8_printf(stderr, "extra argument: \"%s\"\n", z);
+      return 1;
     }else{
-      sCtx.in = fopen(sCtx.zFile, "rb");
-      sCtx.xCloser = fclose;
-    }
-    if( sCtx.in==0 ){
-      utf8_printf(stderr, "Error: cannot open \"%s\"\n", zFile);
-      rc = 1;
-      goto meta_command_exit;
-    }
-    if( eVerbose>=2 || (eVerbose>=1 && useOutputMode) ){
-      char zSep[2];
-      zSep[1] = 0;
-      zSep[0] = sCtx.cColSep;
-      utf8_printf(p->out, "Column separator ");
-      output_c_string(p->out, zSep);
-      utf8_printf(p->out, ", row separator ");
-      zSep[0] = sCtx.cRowSep;
-      output_c_string(p->out, zSep);
-      utf8_printf(p->out, "\n");
-    }
-    while( (nSkip--)>0 ){
-      while( xRead(&sCtx) && sCtx.cTerm==sCtx.cColSep ){}
-    }
-    zSql = sqlite3_mprintf("SELECT * FROM \"%w\"", zTable);
-    if( zSql==0 ){
-      import_cleanup(&sCtx);
-      shell_out_of_memory();
+      zNewFilename = sqlite3_mprintf("%s", z);
     }
-    nByte = strlen30(zSql);
-    rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
-    import_append_char(&sCtx, 0);    /* To ensure sCtx.z is allocated */
-    if( rc && sqlite3_strglob("no such table: *", sqlite3_errmsg(p->db))==0 ){
-      char *zCreate = sqlite3_mprintf("CREATE TABLE \"%w\"", zTable);
-      char cSep = '(';
-      while( xRead(&sCtx) ){
-        zCreate = sqlite3_mprintf("%z%c\n  \"%w\" TEXT", zCreate, cSep, sCtx.z);
-        cSep = ',';
-        if( sCtx.cTerm!=sCtx.cColSep ) break;
-      }
-      if( cSep=='(' ){
-        sqlite3_free(zCreate);
-        import_cleanup(&sCtx);
-        utf8_printf(stderr,"%s: empty file\n", sCtx.zFile);
-        rc = 1;
-        goto meta_command_exit;
-      }
-      zCreate = sqlite3_mprintf("%z\n)", zCreate);
-      if( eVerbose>=1 ){
-        utf8_printf(p->out, "%s\n", zCreate);
-      }
-      rc = sqlite3_exec(p->db, zCreate, 0, 0, 0);
-      sqlite3_free(zCreate);
-      if( rc ){
-        utf8_printf(stderr, "CREATE TABLE \"%s\"(...) failed: %s\n", zTable,
-                sqlite3_errmsg(p->db));
-        import_cleanup(&sCtx);
-        rc = 1;
-        goto meta_command_exit;
-      }
-      rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+  }
+  /* If a filename is specified, try to open it first */
+  if( zNewFilename || p->openMode==SHELL_OPEN_HEXDB ){
+    if( newFlag ) shellDeleteFile(zNewFilename);
+    p->zDbFilename = zNewFilename;
+    open_db(p, OPEN_DB_KEEPALIVE);
+    if( p->db==0 ){
+      utf8_printf(stderr, "Error: cannot open '%s'\n", zNewFilename);
+      sqlite3_free(zNewFilename);
+    }else{
+      p->zFreeOnClose = zNewFilename;
     }
-    sqlite3_free(zSql);
-    if( rc ){
-      if (pStmt) sqlite3_finalize(pStmt);
-      utf8_printf(stderr,"Error: %s\n", sqlite3_errmsg(p->db));
-      import_cleanup(&sCtx);
-      rc = 1;
-      goto meta_command_exit;
+  }
+  if( p->db==0 ){
+    /* As a fall-back open a TEMP database */
+    p->zDbFilename = 0;
+    open_db(p, 0);
+  }
+  return 0;
+}
+
+DISPATCHABLE_COMMAND( nullvalue ? 2 2 ){
+  sqlite3_snprintf(sizeof(p->nullValue), p->nullValue,
+                   "%.*s", (int)ArraySize(p->nullValue)-1, azArg[1]);
+  return 0;
+}
+
+/*****************
+ * The .parameter command
+ */
+COLLECT_HELP_TEXT[
+  ".parameter CMD ...       Manage SQL parameter bindings",
+  "   clear                   Erase all bindings",
+  "   init                    Initialize the TEMP table that holds bindings",
+  "   list                    List the current parameter bindings",
+  "   set PARAMETER VALUE     Given SQL parameter PARAMETER a value of VALUE",
+  "                           PARAMETER should start with one of: $ : @ ?",
+  "   unset PARAMETER         Remove PARAMETER from the binding table",
+];
+DISPATCHABLE_COMMAND( parameter 4 2 4 ){
+  int rc = 0;
+  open_db(p,0);
+
+  /* .parameter clear
+  ** Clear all bind parameters by dropping the TEMP table that holds them.
+  */
+  if( nArg==2 && strcmp(azArg[1],"clear")==0 ){
+    sqlite3_exec(p->db, "DROP TABLE IF EXISTS temp.sqlite_parameters;",
+                 0, 0, 0);
+  }else
+
+  /* .parameter list
+  ** List all bind parameters.
+  */
+  if( nArg==2 && strcmp(azArg[1],"list")==0 ){
+    sqlite3_stmt *pStmt = 0;
+    int rx;
+    int len = 0;
+    rx = sqlite3_prepare_v2(p->db,
+                            "SELECT max(length(key)) "
+                            "FROM temp.sqlite_parameters;", -1, &pStmt, 0);
+    if( rx==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
+      len = sqlite3_column_int(pStmt, 0);
+      if( len>40 ) len = 40;
     }
-    nCol = sqlite3_column_count(pStmt);
     sqlite3_finalize(pStmt);
     pStmt = 0;
-    if( nCol==0 ) return 0; /* no columns, no error */
-    zSql = sqlite3_malloc64( nByte*2 + 20 + nCol*2 );
-    if( zSql==0 ){
-      import_cleanup(&sCtx);
-      shell_out_of_memory();
-    }
-    sqlite3_snprintf(nByte+20, zSql, "INSERT INTO \"%w\" VALUES(?", zTable);
-    j = strlen30(zSql);
-    for(i=1; i<nCol; i++){
-      zSql[j++] = ',';
-      zSql[j++] = '?';
-    }
-    zSql[j++] = ')';
-    zSql[j] = 0;
-    if( eVerbose>=2 ){
-      utf8_printf(p->out, "Insert using: %s\n", zSql);
-    }
-    rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
-    sqlite3_free(zSql);
-    if( rc ){
-      utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
-      if (pStmt) sqlite3_finalize(pStmt);
-      import_cleanup(&sCtx);
-      rc = 1;
-      goto meta_command_exit;
-    }
-    needCommit = sqlite3_get_autocommit(p->db);
-    if( needCommit ) sqlite3_exec(p->db, "BEGIN", 0, 0, 0);
-    do{
-      int startLine = sCtx.nLine;
-      for(i=0; i<nCol; i++){
-        char *z = xRead(&sCtx);
-        /*
-        ** Did we reach end-of-file before finding any columns?
-        ** If so, stop instead of NULL filling the remaining columns.
-        */
-        if( z==0 && i==0 ) break;
-        /*
-        ** Did we reach end-of-file OR end-of-line before finding any
-        ** columns in ASCII mode?  If so, stop instead of NULL filling
-        ** the remaining columns.
-        */
-        if( p->mode==MODE_Ascii && (z==0 || z[0]==0) && i==0 ) break;
-        sqlite3_bind_text(pStmt, i+1, z, -1, SQLITE_TRANSIENT);
-        if( i<nCol-1 && sCtx.cTerm!=sCtx.cColSep ){
-          utf8_printf(stderr, "%s:%d: expected %d columns but found %d - "
-                          "filling the rest with NULL\n",
-                          sCtx.zFile, startLine, nCol, i+1);
-          i += 2;
-          while( i<=nCol ){ sqlite3_bind_null(pStmt, i); i++; }
-        }
-      }
-      if( sCtx.cTerm==sCtx.cColSep ){
-        do{
-          xRead(&sCtx);
-          i++;
-        }while( sCtx.cTerm==sCtx.cColSep );
-        utf8_printf(stderr, "%s:%d: expected %d columns but found %d - "
-                        "extras ignored\n",
-                        sCtx.zFile, startLine, nCol, i);
-      }
-      if( i>=nCol ){
-        sqlite3_step(pStmt);
-        rc = sqlite3_reset(pStmt);
-        if( rc!=SQLITE_OK ){
-          utf8_printf(stderr, "%s:%d: INSERT failed: %s\n", sCtx.zFile,
-                      startLine, sqlite3_errmsg(p->db));
-          sCtx.nErr++;
-        }else{
-          sCtx.nRow++;
-        }
+    if( len ){
+      rx = sqlite3_prepare_v2(p->db,
+                              "SELECT key, quote(value) "
+                              "FROM temp.sqlite_parameters;", -1, &pStmt, 0);
+      while( sqlite3_step(pStmt)==SQLITE_ROW ){
+        utf8_printf(p->out, "%-*s %s\n", len, sqlite3_column_text(pStmt,0),
+                    sqlite3_column_text(pStmt,1));
       }
-    }while( sCtx.cTerm!=EOF );
-
-    import_cleanup(&sCtx);
-    sqlite3_finalize(pStmt);
-    if( needCommit ) sqlite3_exec(p->db, "COMMIT", 0, 0, 0);
-    if( eVerbose>0 ){
-      utf8_printf(p->out,
-          "Added %d rows with %d errors using %d lines of input\n",
-          sCtx.nRow, sCtx.nErr, sCtx.nLine-1);
+      sqlite3_finalize(pStmt);
     }
   }else
 
-#ifndef SQLITE_UNTESTABLE
-  if( c=='i' && strncmp(azArg[0], "imposter", n)==0 ){
+  /* .parameter init
+  ** Make sure the TEMP table used to hold bind parameters exists.
+  ** Create it if necessary.
+  */
+  if( nArg==2 && strcmp(azArg[1],"init")==0 ){
+    bind_table_init(p);
+  }else
+
+  /* .parameter set NAME VALUE
+  ** Set or reset a bind parameter.  NAME should be the full parameter
+  ** name exactly as it appears in the query.  (ex: $abc, @def).  The
+  ** VALUE can be in either SQL literal notation, or if not it will be
+  ** understood to be a text string.
+  */
+  if( nArg==4 && strcmp(azArg[1],"set")==0 ){
+    int rx;
     char *zSql;
-    char *zCollist = 0;
     sqlite3_stmt *pStmt;
-    int tnum = 0;
-    int isWO = 0;  /* True if making an imposter of a WITHOUT ROWID table */
-    int lenPK = 0; /* Length of the PRIMARY KEY string for isWO tables */
-    int i;
-    if( !(nArg==3 || (nArg==2 && sqlite3_stricmp(azArg[1],"off")==0)) ){
-      utf8_printf(stderr, "Usage: .imposter INDEX IMPOSTER\n"
-                          "       .imposter off\n");
-      /* Also allowed, but not documented:
-      **
-      **    .imposter TABLE IMPOSTER
-      **
-      ** where TABLE is a WITHOUT ROWID table.  In that case, the
-      ** imposter is another WITHOUT ROWID table with the columns in
-      ** storage order. */
-      rc = 1;
-      goto meta_command_exit;
-    }
-    open_db(p, 0);
-    if( nArg==2 ){
-      sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 0, 1);
-      goto meta_command_exit;
-    }
+    const char *zKey = azArg[2];
+    const char *zValue = azArg[3];
+    bind_table_init(p);
     zSql = sqlite3_mprintf(
-      "SELECT rootpage, 0 FROM sqlite_schema"
-      " WHERE name='%q' AND type='index'"
-      "UNION ALL "
-      "SELECT rootpage, 1 FROM sqlite_schema"
-      " WHERE name='%q' AND type='table'"
-      "   AND sql LIKE '%%without%%rowid%%'",
-      azArg[1], azArg[1]
-    );
-    sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+                "REPLACE INTO temp.sqlite_parameters(key,value)"
+                "VALUES(%Q,%s);", zKey, zValue);
+    if( zSql==0 ) shell_out_of_memory();
+    pStmt = 0;
+    rx = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
     sqlite3_free(zSql);
-    if( sqlite3_step(pStmt)==SQLITE_ROW ){
-      tnum = sqlite3_column_int(pStmt, 0);
-      isWO = sqlite3_column_int(pStmt, 1);
+    if( rx!=SQLITE_OK ){
+      sqlite3_finalize(pStmt);
+      pStmt = 0;
+      zSql = sqlite3_mprintf(
+                 "REPLACE INTO temp.sqlite_parameters(key,value)"
+                 "VALUES(%Q,%Q);", zKey, zValue);
+      if( zSql==0 ) shell_out_of_memory();
+      rx = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+      sqlite3_free(zSql);
+      if( rx!=SQLITE_OK ){
+        utf8_printf(p->out, "Error: %s\n", sqlite3_errmsg(p->db));
+        sqlite3_finalize(pStmt);
+        pStmt = 0;
+        rc = 1;
+      }
     }
+    sqlite3_step(pStmt);
     sqlite3_finalize(pStmt);
-    zSql = sqlite3_mprintf("PRAGMA index_xinfo='%q'", azArg[1]);
-    rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+  }else
+
+  /* .parameter unset NAME
+  ** Remove the NAME binding from the parameter binding table, if it
+  ** exists.
+  */
+  if( nArg==3 && strcmp(azArg[1],"unset")==0 ){
+    char *zSql = sqlite3_mprintf(
+        "DELETE FROM temp.sqlite_parameters WHERE key=%Q", azArg[2]);
+    if( zSql==0 ) shell_out_of_memory();
+    sqlite3_exec(p->db, zSql, 0, 0, 0);
     sqlite3_free(zSql);
-    i = 0;
-    while( sqlite3_step(pStmt)==SQLITE_ROW ){
-      char zLabel[20];
-      const char *zCol = (const char*)sqlite3_column_text(pStmt,2);
-      i++;
-      if( zCol==0 ){
-        if( sqlite3_column_int(pStmt,1)==-1 ){
-          zCol = "_ROWID_";
-        }else{
-          sqlite3_snprintf(sizeof(zLabel),zLabel,"expr%d",i);
-          zCol = zLabel;
-        }
+  }else
+
+  {  /* If no command name and arg count matches, show a syntax error */
+    showHelp(p->out, "parameter");
+    return 1;
+  }
+
+  return rc;
+}
+
+/*****************
+ * The .print, .progress and .prompt commands
+ */
+CONDITION_COMMAND( progress !defined(SQLITE_OMIT_PROGRESS_CALLBACK) );
+COLLECT_HELP_TEXT[
+  ".print STRING...         Print literal STRING",
+  ".progress N              Invoke progress handler after every N opcodes",
+  "   --limit N                 Interrupt after N progress callbacks",
+  "   --once                    Do no more than one progress interrupt",
+  "   --quiet|-q                No output except at interrupts",
+  "   --reset                   Reset the count for each input and interrupt",
+  ".prompt MAIN CONTINUE    Replace the standard prompts",
+];
+DISPATCHABLE_COMMAND( print 3 1 0 ){
+  int i;
+  for(i=1; i<nArg; i++){
+    if( i>1 ) raw_printf(p->out, " ");
+    utf8_printf(p->out, "%s", azArg[i]);
+  }
+  raw_printf(p->out, "\n");
+  return 0;
+}
+DISPATCHABLE_COMMAND( progress 3 2 0 ){
+  int i;
+  int nn = 0;
+  p->flgProgress = 0;
+  p->mxProgress = 0;
+  p->nProgress = 0;
+  for(i=1; i<nArg; i++){
+    const char *z = azArg[i];
+    if( z[0]=='-' ){
+      z++;
+      if( z[0]=='-' ) z++;
+      if( strcmp(z,"quiet")==0 || strcmp(z,"q")==0 ){
+        p->flgProgress |= SHELL_PROGRESS_QUIET;
+        continue;
       }
-      if( isWO && lenPK==0 && sqlite3_column_int(pStmt,5)==0 && zCollist ){
-        lenPK = (int)strlen(zCollist);
+      if( strcmp(z,"reset")==0 ){
+        p->flgProgress |= SHELL_PROGRESS_RESET;
+        continue;
       }
-      if( zCollist==0 ){
-        zCollist = sqlite3_mprintf("\"%w\"", zCol);
-      }else{
-        zCollist = sqlite3_mprintf("%z,\"%w\"", zCollist, zCol);
+      if( strcmp(z,"once")==0 ){
+        p->flgProgress |= SHELL_PROGRESS_ONCE;
+        continue;
+      }
+      if( strcmp(z,"limit")==0 ){
+        if( i+1>=nArg ){
+          utf8_printf(stderr, "Error: missing argument on --limit\n");
+          return 1;
+        }else{
+          p->mxProgress = (int)integerValue(azArg[++i]);
+        }
+        continue;
       }
+      utf8_printf(stderr, "Error: unknown option: \"%s\"\n", azArg[i]);
+      return 1;
+    }else{
+      nn = (int)integerValue(z);
     }
-    sqlite3_finalize(pStmt);
-    if( i==0 || tnum==0 ){
-      utf8_printf(stderr, "no such index: \"%s\"\n", azArg[1]);
+  }
+  open_db(p, 0);
+  sqlite3_progress_handler(p->db, nn, progress_handler, p);
+  return 0;
+}
+DISPATCHABLE_COMMAND( prompt ? 2 3 ){
+  if( nArg >= 2) {
+    strncpy(mainPrompt,azArg[1],(int)ArraySize(mainPrompt)-1);
+  }
+  if( nArg >= 3) {
+    strncpy(continuePrompt,azArg[2],(int)ArraySize(continuePrompt)-1);
+  }
+  return 0;
+}
+
+/*****************
+ * The .read, .recover and .restore commands
+ */
+CONDITION_COMMAND( recover !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) );
+COLLECT_HELP_TEXT[
+  ".read FILE               Read input from FILE",
+  ".recover                 Recover as much data as possible from corrupt db.",
+  "   --freelist-corrupt       Assume the freelist is corrupt",
+  "   --recovery-db NAME       Store recovery metadata in database file NAME",
+  "   --lost-and-found TABLE   Alternative name for the lost-and-found table",
+  "   --no-rowids              Do not attempt to recover rowid values",
+  "                            that are not also INTEGER PRIMARY KEYs",
+  ".restore ?DB? FILE       Restore content of DB (default \"main\") from FILE",
+];
+DISPATCHABLE_COMMAND( read 3 2 2 ){
+  int rc = 0;
+  FILE *inSaved = p->in;
+  int savedLineno = p->lineno;
+  if( azArg[1][0]=='|' ){
+#ifdef SQLITE_OMIT_POPEN
+    raw_printf(stderr, "Error: pipes are not supported in this OS\n");
+    rc = 1;
+    p->out = stdout;
+#else
+    p->in = popen(azArg[1]+1, "r");
+    if( p->in==0 ){
+      utf8_printf(stderr, "Error: cannot open \"%s\"\n", azArg[1]);
       rc = 1;
-      sqlite3_free(zCollist);
-      goto meta_command_exit;
+    }else{
+      rc = process_input(p);
+      pclose(p->in);
+    }
+#endif
+  }else if( notNormalFile(azArg[1]) || (p->in = fopen(azArg[1], "rb"))==0 ){
+    utf8_printf(stderr,"Error: cannot open \"%s\"\n", azArg[1]);
+    rc = 1;
+  }else{
+    rc = process_input(p);
+    fclose(p->in);
+  }
+  p->in = inSaved;
+  p->lineno = savedLineno;
+  return rc;
+}
+
+/*
+** This command is invoked to recover data from the database. A script
+** to construct a new database containing all recovered data is output
+** on stream pState->out.
+*/
+DISPATCHABLE_COMMAND( recover ? 1 7 ){
+  open_db(p, 0);
+  int rc = SQLITE_OK;
+  sqlite3_stmt *pLoop = 0;        /* Loop through all root pages */
+  sqlite3_stmt *pPages = 0;       /* Loop through all pages in a group */
+  sqlite3_stmt *pCells = 0;       /* Loop through all cells in a page */
+  const char *zRecoveryDb = "";   /* Name of "recovery" database */
+  const char *zLostAndFound = "lost_and_found";
+  int i;
+  int nOrphan = -1;
+  RecoverTable *pOrphan = 0;
+
+  int bFreelist = 1;              /* 0 if --freelist-corrupt is specified */
+  int bRowids = 1;                /* 0 if --no-rowids */
+  for(i=1; i<nArg; i++){
+    char *z = azArg[i];
+    int n;
+    if( z[0]=='-' && z[1]=='-' ) z++;
+    n = strlen30(z);
+    if( n<=17 && memcmp("-freelist-corrupt", z, n)==0 ){
+      bFreelist = 0;
+    }else
+    if( n<=12 && memcmp("-recovery-db", z, n)==0 && i<(nArg-1) ){
+      i++;
+      zRecoveryDb = azArg[i];
+    }else
+    if( n<=15 && memcmp("-lost-and-found", z, n)==0 && i<(nArg-1) ){
+      i++;
+      zLostAndFound = azArg[i];
+    }else
+    if( n<=10 && memcmp("-no-rowids", z, n)==0 ){
+      bRowids = 0;
     }
-    if( lenPK==0 ) lenPK = 100000;
-    zSql = sqlite3_mprintf(
-          "CREATE TABLE \"%w\"(%s,PRIMARY KEY(%.*s))WITHOUT ROWID",
-          azArg[2], zCollist, lenPK, zCollist);
-    sqlite3_free(zCollist);
-    rc = sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 1, tnum);
-    if( rc==SQLITE_OK ){
-      rc = sqlite3_exec(p->db, zSql, 0, 0, 0);
-      sqlite3_test_control(SQLITE_TESTCTRL_IMPOSTER, p->db, "main", 0, 0);
-      if( rc ){
-        utf8_printf(stderr, "Error in [%s]: %s\n", zSql, sqlite3_errmsg(p->db));
-      }else{
-        utf8_printf(stdout, "%s;\n", zSql);
-        raw_printf(stdout,
-          "WARNING: writing to an imposter table will corrupt the \"%s\" %s!\n",
-          azArg[1], isWO ? "table" : "index"
-        );
-      }
-    }else{
-      raw_printf(stderr, "SQLITE_TESTCTRL_IMPOSTER returns %d\n", rc);
-      rc = 1;
+    else{
+      utf8_printf(stderr, "unexpected option: %s\n", azArg[i]); 
+      showHelp(p->out, azArg[0]);
+      return 1;
     }
-    sqlite3_free(zSql);
-  }else
-#endif /* !defined(SQLITE_OMIT_TEST_CONTROL) */
+  }
 
-#ifdef SQLITE_ENABLE_IOTRACE
-  if( c=='i' && strncmp(azArg[0], "iotrace", n)==0 ){
-    SQLITE_API extern void (SQLITE_CDECL *sqlite3IoTrace)(const char*, ...);
-    if( iotrace && iotrace!=stdout ) fclose(iotrace);
-    iotrace = 0;
-    if( nArg<2 ){
-      sqlite3IoTrace = 0;
-    }else if( strcmp(azArg[1], "-")==0 ){
-      sqlite3IoTrace = iotracePrintf;
-      iotrace = stdout;
-    }else{
-      iotrace = fopen(azArg[1], "w");
-      if( iotrace==0 ){
-        utf8_printf(stderr, "Error: cannot open \"%s\"\n", azArg[1]);
-        sqlite3IoTrace = 0;
-        rc = 1;
-      }else{
-        sqlite3IoTrace = iotracePrintf;
-      }
-    }
-  }else
-#endif
+  shellExecPrintf(p->db, &rc,
+    /* Attach an in-memory database named 'recovery'. Create an indexed 
+    ** cache of the sqlite_dbptr virtual table. */
+    "PRAGMA writable_schema = on;"
+    "ATTACH %Q AS recovery;"
+    "DROP TABLE IF EXISTS recovery.dbptr;"
+    "DROP TABLE IF EXISTS recovery.freelist;"
+    "DROP TABLE IF EXISTS recovery.map;"
+    "DROP TABLE IF EXISTS recovery.schema;"
+    "CREATE TABLE recovery.freelist(pgno INTEGER PRIMARY KEY);", zRecoveryDb
+  );
 
-  if( c=='l' && n>=5 && strncmp(azArg[0], "limits", n)==0 ){
-    static const struct {
-       const char *zLimitName;   /* Name of a limit */
-       int limitCode;            /* Integer code for that limit */
-    } aLimit[] = {
-      { "length",                SQLITE_LIMIT_LENGTH                    },
-      { "sql_length",            SQLITE_LIMIT_SQL_LENGTH                },
-      { "column",                SQLITE_LIMIT_COLUMN                    },
-      { "expr_depth",            SQLITE_LIMIT_EXPR_DEPTH                },
-      { "compound_select",       SQLITE_LIMIT_COMPOUND_SELECT           },
-      { "vdbe_op",               SQLITE_LIMIT_VDBE_OP                   },
-      { "function_arg",          SQLITE_LIMIT_FUNCTION_ARG              },
-      { "attached",              SQLITE_LIMIT_ATTACHED                  },
-      { "like_pattern_length",   SQLITE_LIMIT_LIKE_PATTERN_LENGTH       },
-      { "variable_number",       SQLITE_LIMIT_VARIABLE_NUMBER           },
-      { "trigger_depth",         SQLITE_LIMIT_TRIGGER_DEPTH             },
-      { "worker_threads",        SQLITE_LIMIT_WORKER_THREADS            },
-    };
-    int i, n2;
-    open_db(p, 0);
-    if( nArg==1 ){
-      for(i=0; i<ArraySize(aLimit); i++){
-        printf("%20s %d\n", aLimit[i].zLimitName,
-               sqlite3_limit(p->db, aLimit[i].limitCode, -1));
-      }
-    }else if( nArg>3 ){
-      raw_printf(stderr, "Usage: .limit NAME ?NEW-VALUE?\n");
-      rc = 1;
-      goto meta_command_exit;
-    }else{
-      int iLimit = -1;
-      n2 = strlen30(azArg[1]);
-      for(i=0; i<ArraySize(aLimit); i++){
-        if( sqlite3_strnicmp(aLimit[i].zLimitName, azArg[1], n2)==0 ){
-          if( iLimit<0 ){
-            iLimit = i;
-          }else{
-            utf8_printf(stderr, "ambiguous limit: \"%s\"\n", azArg[1]);
-            rc = 1;
-            goto meta_command_exit;
-          }
-        }
-      }
-      if( iLimit<0 ){
-        utf8_printf(stderr, "unknown limit: \"%s\"\n"
-                        "enter \".limits\" with no arguments for a list.\n",
-                         azArg[1]);
-        rc = 1;
-        goto meta_command_exit;
-      }
-      if( nArg==3 ){
-        sqlite3_limit(p->db, aLimit[iLimit].limitCode,
-                      (int)integerValue(azArg[2]));
-      }
-      printf("%20s %d\n", aLimit[iLimit].zLimitName,
-             sqlite3_limit(p->db, aLimit[iLimit].limitCode, -1));
-    }
-  }else
+  if( bFreelist ){
+    shellExec(p->db, &rc,
+      "WITH trunk(pgno) AS ("
+      "  SELECT shell_int32("
+      "      (SELECT data FROM sqlite_dbpage WHERE pgno=1), 8) AS x "
+      "      WHERE x>0"
+      "    UNION"
+      "  SELECT shell_int32("
+      "      (SELECT data FROM sqlite_dbpage WHERE pgno=trunk.pgno), 0) AS x "
+      "      FROM trunk WHERE x>0"
+      "),"
+      "freelist(data, n, freepgno) AS ("
+      "  SELECT data, min(16384, shell_int32(data, 1)-1), t.pgno "
+      "      FROM trunk t, sqlite_dbpage s WHERE s.pgno=t.pgno"
+      "    UNION ALL"
+      "  SELECT data, n-1, shell_int32(data, 2+n) "
+      "      FROM freelist WHERE n>=0"
+      ")"
+      "REPLACE INTO recovery.freelist SELECT freepgno FROM freelist;"
+    );
+  }
 
-  if( c=='l' && n>2 && strncmp(azArg[0], "lint", n)==0 ){
-    open_db(p, 0);
-    lintDotCommand(p, azArg, nArg);
-  }else
+  /* If this is an auto-vacuum database, add all pointer-map pages to
+  ** the freelist table. Do this regardless of whether or not 
+  ** --freelist-corrupt was specified.  */
+  shellExec(p->db, &rc, 
+    "WITH ptrmap(pgno) AS ("
+    "  SELECT 2 WHERE shell_int32("
+    "    (SELECT data FROM sqlite_dbpage WHERE pgno=1), 13"
+    "  )"
+    "    UNION ALL "
+    "  SELECT pgno+1+(SELECT page_size FROM pragma_page_size)/5 AS pp "
+    "  FROM ptrmap WHERE pp<=(SELECT page_count FROM pragma_page_count)"
+    ")"
+    "REPLACE INTO recovery.freelist SELECT pgno FROM ptrmap"
+  );
 
-#ifndef SQLITE_OMIT_LOAD_EXTENSION
-  if( c=='l' && strncmp(azArg[0], "load", n)==0 ){
-    const char *zFile, *zProc;
-    char *zErrMsg = 0;
-    if( nArg<2 ){
-      raw_printf(stderr, "Usage: .load FILE ?ENTRYPOINT?\n");
-      rc = 1;
-      goto meta_command_exit;
-    }
-    zFile = azArg[1];
-    zProc = nArg>=3 ? azArg[2] : 0;
-    open_db(p, 0);
-    rc = sqlite3_load_extension(p->db, zFile, zProc, &zErrMsg);
-    if( rc!=SQLITE_OK ){
-      utf8_printf(stderr, "Error: %s\n", zErrMsg);
-      sqlite3_free(zErrMsg);
-      rc = 1;
-    }
-  }else
-#endif
+  shellExec(p->db, &rc, 
+    "CREATE TABLE recovery.dbptr("
+    "      pgno, child, PRIMARY KEY(child, pgno)"
+    ") WITHOUT ROWID;"
+    "INSERT OR IGNORE INTO recovery.dbptr(pgno, child) "
+    "    SELECT * FROM sqlite_dbptr"
+    "      WHERE pgno NOT IN freelist AND child NOT IN freelist;"
 
-  if( c=='l' && strncmp(azArg[0], "log", n)==0 ){
-    if( nArg!=2 ){
-      raw_printf(stderr, "Usage: .log FILENAME\n");
-      rc = 1;
-    }else{
-      const char *zFile = azArg[1];
-      output_file_close(p->pLog);
-      p->pLog = output_file_open(zFile, 0);
-    }
-  }else
+    /* Delete any pointer to page 1. This ensures that page 1 is considered
+    ** a root page, regardless of how corrupt the db is. */
+    "DELETE FROM recovery.dbptr WHERE child = 1;"
 
-  if( c=='m' && strncmp(azArg[0], "mode", n)==0 ){
-    const char *zMode = nArg>=2 ? azArg[1] : "";
-    int n2 = strlen30(zMode);
-    int c2 = zMode[0];
-    if( c2=='l' && n2>2 && strncmp(azArg[1],"lines",n2)==0 ){
-      p->mode = MODE_Line;
-      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
-    }else if( c2=='c' && strncmp(azArg[1],"columns",n2)==0 ){
-      p->mode = MODE_Column;
-      if( (p->shellFlgs & SHFLG_HeaderSet)==0 ){
-        p->showHeader = 1;
-      }
-      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
-    }else if( c2=='l' && n2>2 && strncmp(azArg[1],"list",n2)==0 ){
-      p->mode = MODE_List;
-      sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Column);
-      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
-    }else if( c2=='h' && strncmp(azArg[1],"html",n2)==0 ){
-      p->mode = MODE_Html;
-    }else if( c2=='t' && strncmp(azArg[1],"tcl",n2)==0 ){
-      p->mode = MODE_Tcl;
-      sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Space);
-      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
-    }else if( c2=='c' && strncmp(azArg[1],"csv",n2)==0 ){
-      p->mode = MODE_Csv;
-      sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma);
-      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_CrLf);
-    }else if( c2=='t' && strncmp(azArg[1],"tabs",n2)==0 ){
-      p->mode = MODE_List;
-      sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Tab);
-    }else if( c2=='i' && strncmp(azArg[1],"insert",n2)==0 ){
-      p->mode = MODE_Insert;
-      set_table_name(p, nArg>=3 ? azArg[2] : "table");
-    }else if( c2=='q' && strncmp(azArg[1],"quote",n2)==0 ){
-      p->mode = MODE_Quote;
-      sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma);
-      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row);
-    }else if( c2=='a' && strncmp(azArg[1],"ascii",n2)==0 ){
-      p->mode = MODE_Ascii;
-      sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Unit);
-      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Record);
-    }else if( c2=='m' && strncmp(azArg[1],"markdown",n2)==0 ){
-      p->mode = MODE_Markdown;
-    }else if( c2=='t' && strncmp(azArg[1],"table",n2)==0 ){
-      p->mode = MODE_Table;
-    }else if( c2=='b' && strncmp(azArg[1],"box",n2)==0 ){
-      p->mode = MODE_Box;
-    }else if( c2=='j' && strncmp(azArg[1],"json",n2)==0 ){
-      p->mode = MODE_Json;
-    }else if( nArg==1 ){
-      raw_printf(p->out, "current output mode: %s\n", modeDescr[p->mode]);
-    }else{
-      raw_printf(stderr, "Error: mode should be one of: "
-         "ascii box column csv html insert json line list markdown "
-         "quote table tabs tcl\n");
-      rc = 1;
-    }
-    p->cMode = p->mode;
-  }else
+    /* Delete all pointers to any pages that have more than one pointer
+    ** to them. Such pages will be treated as root pages when recovering
+    ** data.  */
+    "DELETE FROM recovery.dbptr WHERE child IN ("
+    "  SELECT child FROM recovery.dbptr GROUP BY child HAVING count(*)>1"
+    ");"
 
-  if( c=='n' && strncmp(azArg[0], "nullvalue", n)==0 ){
-    if( nArg==2 ){
-      sqlite3_snprintf(sizeof(p->nullValue), p->nullValue,
-                       "%.*s", (int)ArraySize(p->nullValue)-1, azArg[1]);
-    }else{
-      raw_printf(stderr, "Usage: .nullvalue STRING\n");
-      rc = 1;
-    }
-  }else
+    /* Create the "map" table that will (eventually) contain instructions
+    ** for dealing with each page in the db that contains one or more 
+    ** records. */
+    "CREATE TABLE recovery.map("
+      "pgno INTEGER PRIMARY KEY, maxlen INT, intkey, root INT"
+    ");"
 
-#ifdef SQLITE_DEBUG
-  if( c=='o' && strcmp(azArg[0],"oom")==0 ){
-    int i;
-    for(i=1; i<nArg; i++){
-      const char *z = azArg[i];
-      if( z[0]=='-' && z[1]=='-' ) z++;
-      if( strcmp(z,"-repeat")==0 ){
-        if( i==nArg-1 ){
-          raw_printf(p->out, "missing argument on \"%s\"\n", azArg[i]);
-          rc = 1;
-        }else{
-          oomRepeat = (int)integerValue(azArg[++i]);
-        }
-      }else if( IsDigit(z[0]) ){
-        oomCounter = (int)integerValue(azArg[i]);
-      }else{
-        raw_printf(p->out, "unknown argument: \"%s\"\n", azArg[i]);
-        raw_printf(p->out, "Usage: .oom [--repeat N] [M]\n");
-        rc = 1;
-      }
-    }
-    if( rc==0 ){
-      raw_printf(p->out, "oomCounter = %d\n", oomCounter);
-      raw_printf(p->out, "oomRepeat  = %d\n", oomRepeat);
-    }
-  }else
-#endif /* SQLITE_DEBUG */
+    /* Populate table [map]. If there are circular loops of pages in the
+    ** database, the following adds all pages in such a loop to the map
+    ** as individual root pages. This could be handled better.  */
+    "WITH pages(i, maxlen) AS ("
+    "  SELECT page_count, ("
+    "    SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=page_count"
+    "  ) FROM pragma_page_count WHERE page_count>0"
+    "    UNION ALL"
+    "  SELECT i-1, ("
+    "    SELECT max(field+1) FROM sqlite_dbdata WHERE pgno=i-1"
+    "  ) FROM pages WHERE i>=2"
+    ")"
+    "INSERT INTO recovery.map(pgno, maxlen, intkey, root) "
+    "  SELECT i, maxlen, NULL, ("
+    "    WITH p(orig, pgno, parent) AS ("
+    "      SELECT 0, i, (SELECT pgno FROM recovery.dbptr WHERE child=i)"
+    "        UNION "
+    "      SELECT i, p.parent, "
+    "        (SELECT pgno FROM recovery.dbptr WHERE child=p.parent) FROM p"
+    "    )"
+    "    SELECT pgno FROM p WHERE (parent IS NULL OR pgno = orig)"
+    ") "
+    "FROM pages WHERE maxlen IS NOT NULL AND i NOT IN freelist;"
+    "UPDATE recovery.map AS o SET intkey = ("
+    "  SELECT substr(data, 1, 1)==X'0D' FROM sqlite_dbpage WHERE pgno=o.pgno"
+    ");"
 
-  if( c=='o' && strncmp(azArg[0], "open", n)==0 && n>=2 ){
-    char *zNewFilename = 0;  /* Name of the database file to open */
-    int iName = 1;           /* Index in azArg[] of the filename */
-    int newFlag = 0;         /* True to delete file before opening */
-    /* Close the existing database */
-    session_close_all(p);
-    close_db(p->db);
-    p->db = 0;
-    p->zDbFilename = 0;
-    sqlite3_free(p->zFreeOnClose);
-    p->zFreeOnClose = 0;
-    p->openMode = SHELL_OPEN_UNSPEC;
-    p->openFlags = 0;
-    p->szMax = 0;
-    /* Check for command-line arguments */
-    for(iName=1; iName<nArg; iName++){
-      const char *z = azArg[iName];
-      if( optionMatch(z,"new") ){
-        newFlag = 1;
-#ifdef SQLITE_HAVE_ZLIB
-      }else if( optionMatch(z, "zip") ){
-        p->openMode = SHELL_OPEN_ZIPFILE;
-#endif
-      }else if( optionMatch(z, "append") ){
-        p->openMode = SHELL_OPEN_APPENDVFS;
-      }else if( optionMatch(z, "readonly") ){
-        p->openMode = SHELL_OPEN_READONLY;
-      }else if( optionMatch(z, "nofollow") ){
-        p->openFlags |= SQLITE_OPEN_NOFOLLOW;
-      }else if( optionMatch(z, "excl") ){
-        p->openFlags |= SQLITE_OPEN_EXCLUSIVE;
-#ifndef SQLITE_OMIT_DESERIALIZE
-      }else if( optionMatch(z, "deserialize") ){
-        p->openMode = SHELL_OPEN_DESERIALIZE;
-      }else if( optionMatch(z, "hexdb") ){
-        p->openMode = SHELL_OPEN_HEXDB;
-      }else if( optionMatch(z, "maxsize") && iName+1<nArg ){
-        p->szMax = integerValue(azArg[++iName]);
-#endif /* SQLITE_OMIT_DESERIALIZE */
-      }else if( z[0]=='-' ){
-        utf8_printf(stderr, "unknown option: %s\n", z);
-        rc = 1;
-        goto meta_command_exit;
-      }else if( zNewFilename ){
-        utf8_printf(stderr, "extra argument: \"%s\"\n", z);
-        rc = 1;
-        goto meta_command_exit;
-      }else{
-        zNewFilename = sqlite3_mprintf("%s", z);
-      }
-    }
-    /* If a filename is specified, try to open it first */
-    if( zNewFilename || p->openMode==SHELL_OPEN_HEXDB ){
-      if( newFlag ) shellDeleteFile(zNewFilename);
-      p->zDbFilename = zNewFilename;
-      open_db(p, OPEN_DB_KEEPALIVE);
-      if( p->db==0 ){
-        utf8_printf(stderr, "Error: cannot open '%s'\n", zNewFilename);
-        sqlite3_free(zNewFilename);
-      }else{
-        p->zFreeOnClose = zNewFilename;
-      }
-    }
-    if( p->db==0 ){
-      /* As a fall-back open a TEMP database */
-      p->zDbFilename = 0;
-      open_db(p, 0);
+    /* Extract data from page 1 and any linked pages into table
+    ** recovery.schema. With the same schema as an sqlite_schema table.  */
+    "CREATE TABLE recovery.schema(type, name, tbl_name, rootpage, sql);"
+    "INSERT INTO recovery.schema SELECT "
+    "  max(CASE WHEN field=0 THEN value ELSE NULL END),"
+    "  max(CASE WHEN field=1 THEN value ELSE NULL END),"
+    "  max(CASE WHEN field=2 THEN value ELSE NULL END),"
+    "  max(CASE WHEN field=3 THEN value ELSE NULL END),"
+    "  max(CASE WHEN field=4 THEN value ELSE NULL END)"
+    "FROM sqlite_dbdata WHERE pgno IN ("
+    "  SELECT pgno FROM recovery.map WHERE root=1"
+    ")"
+    "GROUP BY pgno, cell;"
+    "CREATE INDEX recovery.schema_rootpage ON schema(rootpage);"
+  );
+
+  /* Open a transaction, then print out all non-virtual, non-"sqlite_%" 
+  ** CREATE TABLE statements that extracted from the existing schema.  */
+  if( rc==SQLITE_OK ){
+    sqlite3_stmt *pStmt = 0;
+    /* ".recover" might output content in an order which causes immediate
+    ** foreign key constraints to be violated. So disable foreign-key
+    ** constraint enforcement to prevent problems when running the output
+    ** script. */
+    raw_printf(p->out, "PRAGMA foreign_keys=OFF;\n");
+    raw_printf(p->out, "BEGIN;\n");
+    raw_printf(p->out, "PRAGMA writable_schema = on;\n");
+    shellPrepare(p->db, &rc,
+        "SELECT sql FROM recovery.schema "
+        "WHERE type='table' AND sql LIKE 'create table%'", &pStmt
+    );
+    while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
+      const char *zCreateTable = (const char*)sqlite3_column_text(pStmt, 0);
+      raw_printf(p->out, "CREATE TABLE IF NOT EXISTS %s;\n", 
+          &zCreateTable[12]
+      );
     }
-  }else
+    shellFinalize(&rc, pStmt);
+  }
+
+  /* Figure out if an orphan table will be required. And if so, how many
+  ** user columns it should contain */
+  shellPrepare(p->db, &rc, 
+      "SELECT coalesce(max(maxlen), -2) FROM recovery.map WHERE root>1"
+      , &pLoop
+  );
+  if( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){
+    nOrphan = sqlite3_column_int(pLoop, 0);
+  }
+  shellFinalize(&rc, pLoop);
+  pLoop = 0;
+
+  shellPrepare(p->db, &rc,
+      "SELECT pgno FROM recovery.map WHERE root=?", &pPages
+  );
+
+  shellPrepare(p->db, &rc,
+      "SELECT max(field), group_concat(shell_escape_crnl(quote"
+      "(case when (? AND field<0) then NULL else value end)"
+      "), ', ')"
+      ", min(field) "
+      "FROM sqlite_dbdata WHERE pgno = ? AND field != ?"
+      "GROUP BY cell", &pCells
+  );
 
-  if( (c=='o'
-        && (strncmp(azArg[0], "output", n)==0||strncmp(azArg[0], "once", n)==0))
-   || (c=='e' && n==5 && strcmp(azArg[0],"excel")==0)
-  ){
-    char *zFile = 0;
-    int bTxtMode = 0;
-    int i;
-    int eMode = 0;
-    int bBOM = 0;
-    int bOnce = 0;  /* 0: .output, 1: .once, 2: .excel */
+  /* Loop through each root page. */
+  shellPrepare(p->db, &rc, 
+      "SELECT root, intkey, max(maxlen) FROM recovery.map" 
+      " WHERE root>1 GROUP BY root, intkey ORDER BY root=("
+      "  SELECT rootpage FROM recovery.schema WHERE name='sqlite_sequence'"
+      ")", &pLoop
+  );
+  while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pLoop) ){
+    int iRoot = sqlite3_column_int(pLoop, 0);
+    int bIntkey = sqlite3_column_int(pLoop, 1);
+    int nCol = sqlite3_column_int(pLoop, 2);
+    int bNoop = 0;
+    RecoverTable *pTab;
 
-    if( c=='e' ){
-      eMode = 'x';
-      bOnce = 2;
-    }else if( strncmp(azArg[0],"once",n)==0 ){
-      bOnce = 1;
-    }
-    for(i=1; i<nArg; i++){
-      char *z = azArg[i];
-      if( z[0]=='-' ){
-        if( z[1]=='-' ) z++;
-        if( strcmp(z,"-bom")==0 ){
-          bBOM = 1;
-        }else if( c!='e' && strcmp(z,"-x")==0 ){
-          eMode = 'x';  /* spreadsheet */
-        }else if( c!='e' && strcmp(z,"-e")==0 ){
-          eMode = 'e';  /* text editor */
-        }else{
-          utf8_printf(p->out, "ERROR: unknown option: \"%s\".  Usage:\n",
-                      azArg[i]);
-          showHelp(p->out, azArg[0]);
-          rc = 1;
-          goto meta_command_exit;
-        }
-      }else if( zFile==0 && eMode!='e' && eMode!='x' ){
-        zFile = sqlite3_mprintf("%s", z);
-        if( zFile[0]=='|' ){
-          while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
-          break;
-        }
-      }else{
-        utf8_printf(p->out,"ERROR: extra parameter: \"%s\".  Usage:\n",
-                    azArg[i]);
-        showHelp(p->out, azArg[0]);
-        rc = 1;
-        sqlite3_free(zFile);
-        goto meta_command_exit;
+    assert( bIntkey==0 || bIntkey==1 );
+    pTab = recoverFindTable(p, &rc, iRoot, bIntkey, nCol, &bNoop);
+    if( bNoop || rc ) continue;
+    if( pTab==0 ){
+      if( pOrphan==0 ){
+        pOrphan = recoverOrphanTable(p, &rc, zLostAndFound, nOrphan);
       }
+      pTab = pOrphan;
+      if( pTab==0 ) break;
     }
-    if( zFile==0 ) zFile = sqlite3_mprintf("stdout");
-    if( bOnce ){
-      p->outCount = 2;
-    }else{
-      p->outCount = 0;
-    }
-    output_reset(p);
-#ifndef SQLITE_NOHAVE_SYSTEM
-    if( eMode=='e' || eMode=='x' ){
-      p->doXdgOpen = 1;
-      outputModePush(p);
-      if( eMode=='x' ){
-        /* spreadsheet mode.  Output as CSV. */
-        newTempFile(p, "csv");
-        ShellClearFlag(p, SHFLG_Echo);
-        p->mode = MODE_Csv;
-        sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma);
-        sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_CrLf);
-      }else{
-        /* text editor mode */
-        newTempFile(p, "txt");
-        bTxtMode = 1;
-      }
-      sqlite3_free(zFile);
-      zFile = sqlite3_mprintf("%s", p->zTempFile);
+
+    if( 0==sqlite3_stricmp(pTab->zQuoted, "\"sqlite_sequence\"") ){
+      raw_printf(p->out, "DELETE FROM sqlite_sequence;\n");
     }
-#endif /* SQLITE_NOHAVE_SYSTEM */
-    if( zFile[0]=='|' ){
-#ifdef SQLITE_OMIT_POPEN
-      raw_printf(stderr, "Error: pipes are not supported in this OS\n");
-      rc = 1;
-      p->out = stdout;
-#else
-      p->out = popen(zFile + 1, "w");
-      if( p->out==0 ){
-        utf8_printf(stderr,"Error: cannot open pipe \"%s\"\n", zFile + 1);
-        p->out = stdout;
-        rc = 1;
-      }else{
-        if( bBOM ) fprintf(p->out,"\357\273\277");
-        sqlite3_snprintf(sizeof(p->outfile), p->outfile, "%s", zFile);
-      }
-#endif
+    sqlite3_bind_int(pPages, 1, iRoot);
+    if( bRowids==0 && pTab->iPk<0 ){
+      sqlite3_bind_int(pCells, 1, 1);
     }else{
-      p->out = output_file_open(zFile, bTxtMode);
-      if( p->out==0 ){
-        if( strcmp(zFile,"off")!=0 ){
-          utf8_printf(stderr,"Error: cannot write to \"%s\"\n", zFile);
-        }
-        p->out = stdout;
-        rc = 1;
-      } else {
-        if( bBOM ) fprintf(p->out,"\357\273\277");
-        sqlite3_snprintf(sizeof(p->outfile), p->outfile, "%s", zFile);
-      }
+      sqlite3_bind_int(pCells, 1, 0);
     }
-    sqlite3_free(zFile);
-  }else
-
-  if( c=='p' && n>=3 && strncmp(azArg[0], "parameter", n)==0 ){
-    open_db(p,0);
-    if( nArg<=1 ) goto parameter_syntax_error;
+    sqlite3_bind_int(pCells, 3, pTab->iPk);
 
-    /* .parameter clear
-    ** Clear all bind parameters by dropping the TEMP table that holds them.
-    */
-    if( nArg==2 && strcmp(azArg[1],"clear")==0 ){
-      sqlite3_exec(p->db, "DROP TABLE IF EXISTS temp.sqlite_parameters;",
-                   0, 0, 0);
-    }else
+    while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPages) ){
+      int iPgno = sqlite3_column_int(pPages, 0);
+      sqlite3_bind_int(pCells, 2, iPgno);
+      while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pCells) ){
+        int nField = sqlite3_column_int(pCells, 0);
+        int iMin = sqlite3_column_int(pCells, 2);
+        const char *zVal = (const char*)sqlite3_column_text(pCells, 1);
 
-    /* .parameter list
-    ** List all bind parameters.
-    */
-    if( nArg==2 && strcmp(azArg[1],"list")==0 ){
-      sqlite3_stmt *pStmt = 0;
-      int rx;
-      int len = 0;
-      rx = sqlite3_prepare_v2(p->db,
-             "SELECT max(length(key)) "
-             "FROM temp.sqlite_parameters;", -1, &pStmt, 0);
-      if( rx==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
-        len = sqlite3_column_int(pStmt, 0);
-        if( len>40 ) len = 40;
-      }
-      sqlite3_finalize(pStmt);
-      pStmt = 0;
-      if( len ){
-        rx = sqlite3_prepare_v2(p->db,
-             "SELECT key, quote(value) "
-             "FROM temp.sqlite_parameters;", -1, &pStmt, 0);
-        while( sqlite3_step(pStmt)==SQLITE_ROW ){
-          utf8_printf(p->out, "%-*s %s\n", len, sqlite3_column_text(pStmt,0),
-                      sqlite3_column_text(pStmt,1));
+        RecoverTable *pTab2 = pTab;
+        if( pTab!=pOrphan && (iMin<0)!=bIntkey ){
+          if( pOrphan==0 ){
+            pOrphan = recoverOrphanTable(p, &rc, zLostAndFound, nOrphan);
+          }
+          pTab2 = pOrphan;
+          if( pTab2==0 ) break;
         }
-        sqlite3_finalize(pStmt);
-      }
-    }else
-
-    /* .parameter init
-    ** Make sure the TEMP table used to hold bind parameters exists.
-    ** Create it if necessary.
-    */
-    if( nArg==2 && strcmp(azArg[1],"init")==0 ){
-      bind_table_init(p);
-    }else
 
-    /* .parameter set NAME VALUE
-    ** Set or reset a bind parameter.  NAME should be the full parameter
-    ** name exactly as it appears in the query.  (ex: $abc, @def).  The
-    ** VALUE can be in either SQL literal notation, or if not it will be
-    ** understood to be a text string.
-    */
-    if( nArg==4 && strcmp(azArg[1],"set")==0 ){
-      int rx;
-      char *zSql;
-      sqlite3_stmt *pStmt;
-      const char *zKey = azArg[2];
-      const char *zValue = azArg[3];
-      bind_table_init(p);
-      zSql = sqlite3_mprintf(
-                  "REPLACE INTO temp.sqlite_parameters(key,value)"
-                  "VALUES(%Q,%s);", zKey, zValue);
-      if( zSql==0 ) shell_out_of_memory();
-      pStmt = 0;
-      rx = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
-      sqlite3_free(zSql);
-      if( rx!=SQLITE_OK ){
-        sqlite3_finalize(pStmt);
-        pStmt = 0;
-        zSql = sqlite3_mprintf(
-                   "REPLACE INTO temp.sqlite_parameters(key,value)"
-                   "VALUES(%Q,%Q);", zKey, zValue);
-        if( zSql==0 ) shell_out_of_memory();
-        rx = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
-        sqlite3_free(zSql);
-        if( rx!=SQLITE_OK ){
-          utf8_printf(p->out, "Error: %s\n", sqlite3_errmsg(p->db));
-          sqlite3_finalize(pStmt);
-          pStmt = 0;
-          rc = 1;
+        nField = nField+1;
+        if( pTab2==pOrphan ){
+          raw_printf(p->out, 
+              "INSERT INTO %s VALUES(%d, %d, %d, %s%s%s);\n",
+              pTab2->zQuoted, iRoot, iPgno, nField,
+              iMin<0 ? "" : "NULL, ", zVal, pTab2->azlCol[nField]
+          );
+        }else{
+          raw_printf(p->out, "INSERT INTO %s(%s) VALUES( %s );\n", 
+              pTab2->zQuoted, pTab2->azlCol[nField], zVal
+          );
         }
       }
-      sqlite3_step(pStmt);
-      sqlite3_finalize(pStmt);
-    }else
-
-    /* .parameter unset NAME
-    ** Remove the NAME binding from the parameter binding table, if it
-    ** exists.
-    */
-    if( nArg==3 && strcmp(azArg[1],"unset")==0 ){
-      char *zSql = sqlite3_mprintf(
-          "DELETE FROM temp.sqlite_parameters WHERE key=%Q", azArg[2]);
-      if( zSql==0 ) shell_out_of_memory();
-      sqlite3_exec(p->db, zSql, 0, 0, 0);
-      sqlite3_free(zSql);
-    }else
-    /* If no command name matches, show a syntax error */
-    parameter_syntax_error:
-    showHelp(p->out, "parameter");
-  }else
-
-  if( c=='p' && n>=3 && strncmp(azArg[0], "print", n)==0 ){
-    int i;
-    for(i=1; i<nArg; i++){
-      if( i>1 ) raw_printf(p->out, " ");
-      utf8_printf(p->out, "%s", azArg[i]);
+      shellReset(&rc, pCells);
     }
-    raw_printf(p->out, "\n");
-  }else
+    shellReset(&rc, pPages);
+    if( pTab!=pOrphan ) recoverFreeTable(pTab);
+  }
+  shellFinalize(&rc, pLoop);
+  shellFinalize(&rc, pPages);
+  shellFinalize(&rc, pCells);
+  recoverFreeTable(pOrphan);
 
-#ifndef SQLITE_OMIT_PROGRESS_CALLBACK
-  if( c=='p' && n>=3 && strncmp(azArg[0], "progress", n)==0 ){
-    int i;
-    int nn = 0;
-    p->flgProgress = 0;
-    p->mxProgress = 0;
-    p->nProgress = 0;
-    for(i=1; i<nArg; i++){
-      const char *z = azArg[i];
-      if( z[0]=='-' ){
-        z++;
-        if( z[0]=='-' ) z++;
-        if( strcmp(z,"quiet")==0 || strcmp(z,"q")==0 ){
-          p->flgProgress |= SHELL_PROGRESS_QUIET;
-          continue;
-        }
-        if( strcmp(z,"reset")==0 ){
-          p->flgProgress |= SHELL_PROGRESS_RESET;
-          continue;
-        }
-        if( strcmp(z,"once")==0 ){
-          p->flgProgress |= SHELL_PROGRESS_ONCE;
-          continue;
-        }
-        if( strcmp(z,"limit")==0 ){
-          if( i+1>=nArg ){
-            utf8_printf(stderr, "Error: missing argument on --limit\n");
-            rc = 1;
-            goto meta_command_exit;
-          }else{
-            p->mxProgress = (int)integerValue(azArg[++i]);
-          }
-          continue;
-        }
-        utf8_printf(stderr, "Error: unknown option: \"%s\"\n", azArg[i]);
-        rc = 1;
-        goto meta_command_exit;
+  /* The rest of the schema */
+  if( rc==SQLITE_OK ){
+    sqlite3_stmt *pStmt = 0;
+    shellPrepare(p->db, &rc, 
+        "SELECT sql, name FROM recovery.schema "
+        "WHERE sql NOT LIKE 'create table%'", &pStmt
+    );
+    while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pStmt) ){
+      const char *zSql = (const char*)sqlite3_column_text(pStmt, 0);
+      if( sqlite3_strnicmp(zSql, "create virt", 11)==0 ){
+        const char *zName = (const char*)sqlite3_column_text(pStmt, 1);
+        char *zPrint = shellMPrintf(&rc, 
+          "INSERT INTO sqlite_schema VALUES('table', %Q, %Q, 0, %Q)",
+          zName, zName, zSql
+        );
+        raw_printf(p->out, "%s;\n", zPrint);
+        sqlite3_free(zPrint);
       }else{
-        nn = (int)integerValue(z);
+        raw_printf(p->out, "%s;\n", zSql);
       }
     }
-    open_db(p, 0);
-    sqlite3_progress_handler(p->db, nn, progress_handler, p);
-  }else
-#endif /* SQLITE_OMIT_PROGRESS_CALLBACK */
-
-  if( c=='p' && strncmp(azArg[0], "prompt", n)==0 ){
-    if( nArg >= 2) {
-      strncpy(mainPrompt,azArg[1],(int)ArraySize(mainPrompt)-1);
-    }
-    if( nArg >= 3) {
-      strncpy(continuePrompt,azArg[2],(int)ArraySize(continuePrompt)-1);
-    }
-  }else
-
-  if( c=='q' && strncmp(azArg[0], "quit", n)==0 ){
-    rc = 2;
-  }else
+    shellFinalize(&rc, pStmt);
+  }
 
-  if( c=='r' && n>=3 && strncmp(azArg[0], "read", n)==0 ){
-    FILE *inSaved = p->in;
-    int savedLineno = p->lineno;
-    if( nArg!=2 ){
-      raw_printf(stderr, "Usage: .read FILE\n");
-      rc = 1;
-      goto meta_command_exit;
-    }
-    if( azArg[1][0]=='|' ){
-#ifdef SQLITE_OMIT_POPEN
-      raw_printf(stderr, "Error: pipes are not supported in this OS\n");
-      rc = 1;
-      p->out = stdout;
-#else
-      p->in = popen(azArg[1]+1, "r");
-      if( p->in==0 ){
-        utf8_printf(stderr, "Error: cannot open \"%s\"\n", azArg[1]);
-        rc = 1;
-      }else{
-        rc = process_input(p);
-        pclose(p->in);
-      }
-#endif
-    }else if( notNormalFile(azArg[1]) || (p->in = fopen(azArg[1], "rb"))==0 ){
-      utf8_printf(stderr,"Error: cannot open \"%s\"\n", azArg[1]);
-      rc = 1;
-    }else{
-      rc = process_input(p);
-      fclose(p->in);
-    }
-    p->in = inSaved;
-    p->lineno = savedLineno;
-  }else
+  if( rc==SQLITE_OK ){
+    raw_printf(p->out, "PRAGMA writable_schema = off;\n");
+    raw_printf(p->out, "COMMIT;\n");
+  }
+  sqlite3_exec(p->db, "DETACH recovery", 0, 0, 0);
+  return rc;
+}
 
-#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB)
-  if( c=='r' && strncmp(azArg[0], "recover", n)==0 ){
-    open_db(p, 0);
-    rc = recoverDatabaseCmd(p, nArg, azArg);
-  }else
-#endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */
+DISPATCHABLE_COMMAND( restore ? 2 3 ){
+  int rc;
+  const char *zSrcFile;
+  const char *zDb;
+  sqlite3 *pSrc;
+  sqlite3_backup *pBackup;
+  int nTimeout = 0;
 
-  if( c=='r' && n>=3 && strncmp(azArg[0], "restore", n)==0 ){
-    const char *zSrcFile;
-    const char *zDb;
-    sqlite3 *pSrc;
-    sqlite3_backup *pBackup;
-    int nTimeout = 0;
-
-    if( nArg==2 ){
-      zSrcFile = azArg[1];
-      zDb = "main";
-    }else if( nArg==3 ){
-      zSrcFile = azArg[2];
-      zDb = azArg[1];
-    }else{
-      raw_printf(stderr, "Usage: .restore ?DB? FILE\n");
-      rc = 1;
-      goto meta_command_exit;
-    }
-    rc = sqlite3_open(zSrcFile, &pSrc);
-    if( rc!=SQLITE_OK ){
-      utf8_printf(stderr, "Error: cannot open \"%s\"\n", zSrcFile);
-      close_db(pSrc);
-      return 1;
-    }
-    open_db(p, 0);
-    pBackup = sqlite3_backup_init(p->db, zDb, pSrc, "main");
-    if( pBackup==0 ){
-      utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
-      close_db(pSrc);
-      return 1;
-    }
-    while( (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK
-          || rc==SQLITE_BUSY  ){
-      if( rc==SQLITE_BUSY ){
-        if( nTimeout++ >= 3 ) break;
-        sqlite3_sleep(100);
-      }
-    }
-    sqlite3_backup_finish(pBackup);
-    if( rc==SQLITE_DONE ){
-      rc = 0;
-    }else if( rc==SQLITE_BUSY || rc==SQLITE_LOCKED ){
-      raw_printf(stderr, "Error: source database is busy\n");
-      rc = 1;
-    }else{
-      utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
-      rc = 1;
-    }
+  if( nArg==2 ){
+    zSrcFile = azArg[1];
+    zDb = "main";
+  }else if( nArg==3 ){
+    zSrcFile = azArg[2];
+    zDb = azArg[1];
+  }else{
+    raw_printf(stderr, "Usage: .restore ?DB? FILE\n");
+    return 1;
+  }
+  rc = sqlite3_open(zSrcFile, &pSrc);
+  if( rc!=SQLITE_OK ){
+    utf8_printf(stderr, "Error: cannot open \"%s\"\n", zSrcFile);
     close_db(pSrc);
-  }else
+    return 1;
+  }
+  open_db(p, 0);
+  pBackup = sqlite3_backup_init(p->db, zDb, pSrc, "main");
+  if( pBackup==0 ){
+    utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
+    close_db(pSrc);
+    return 1;
+  }
+  while( (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK
+         || rc==SQLITE_BUSY  ){
+    if( rc==SQLITE_BUSY ){
+      if( nTimeout++ >= 3 ) break;
+      sqlite3_sleep(100);
+    }
+  }
+  sqlite3_backup_finish(pBackup);
+  if( rc==SQLITE_DONE ){
+    rc = 0;
+  }else if( rc==SQLITE_BUSY || rc==SQLITE_LOCKED ){
+    raw_printf(stderr, "Error: source database is busy\n");
+    rc = 1;
+  }else{
+    utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
+    rc = 1;
+  }
+  close_db(pSrc);
+  return rc;
+}
 
-  if( c=='s' && strncmp(azArg[0], "scanstats", n)==0 ){
-    if( nArg==2 ){
-      p->scanstatsOn = (u8)booleanValue(azArg[1]);
+/*****************
+ * The .scanstats and .schema commands
+ */
+COLLECT_HELP_TEXT[
+  ".scanstats on|off        Turn sqlite3_stmt_scanstatus() metrics on or off",
+  ".schema ?PATTERN?        Show the CREATE statements matching PATTERN",
+  "   Options:",
+  "      --indent             Try to pretty-print the schema",
+  "      --nosys              Omit objects whose names start with \"sqlite_\"",
+];
+DISPATCHABLE_COMMAND( scanstats ? 2 2 ){
+    p->scanstatsOn = (u8)booleanValue(azArg[1]);
 #ifndef SQLITE_ENABLE_STMT_SCANSTATUS
-      raw_printf(stderr, "Warning: .scanstats not available in this build.\n");
+    raw_printf(stderr, "Warning: .scanstats not available in this build.\n");
 #endif
+  return 0;
+}
+DISPATCHABLE_COMMAND( schema ? 1 2 ){
+  int rc;
+  ShellText sSelect;
+  ShellState data;
+  char *zErrMsg = 0;
+  const char *zDiv = "(";
+  const char *zName = 0;
+  int iSchema = 0;
+  int bDebug = 0;
+  int bNoSystemTabs = 0;
+  int ii;
+
+  open_db(p, 0);
+  memcpy(&data, p, sizeof(data));
+  data.showHeader = 0;
+  data.cMode = data.mode = MODE_Semi;
+  initText(&sSelect);
+  for(ii=1; ii<nArg; ii++){
+    if( optionMatch(azArg[ii],"indent") ){
+      data.cMode = data.mode = MODE_Pretty;
+    }else if( optionMatch(azArg[ii],"debug") ){
+      bDebug = 1;
+    }else if( optionMatch(azArg[ii],"nosys") ){
+      bNoSystemTabs = 1;
+    }else if( azArg[ii][0]=='-' ){
+      utf8_printf(stderr, "Unknown option: \"%s\"\n", azArg[ii]);
+      return 1;
+    }else if( zName==0 ){
+      zName = azArg[ii];
     }else{
-      raw_printf(stderr, "Usage: .scanstats on|off\n");
-      rc = 1;
+      raw_printf(stderr, "Usage: .schema ?--indent? ?--nosys? ?LIKE-PATTERN?\n");
+      return 1;
     }
-  }else
-
-  if( c=='s' && strncmp(azArg[0], "schema", n)==0 ){
-    ShellText sSelect;
-    ShellState data;
-    char *zErrMsg = 0;
-    const char *zDiv = "(";
-    const char *zName = 0;
-    int iSchema = 0;
-    int bDebug = 0;
-    int bNoSystemTabs = 0;
-    int ii;
-
-    open_db(p, 0);
-    memcpy(&data, p, sizeof(data));
-    data.showHeader = 0;
-    data.cMode = data.mode = MODE_Semi;
-    initText(&sSelect);
-    for(ii=1; ii<nArg; ii++){
-      if( optionMatch(azArg[ii],"indent") ){
-        data.cMode = data.mode = MODE_Pretty;
-      }else if( optionMatch(azArg[ii],"debug") ){
-        bDebug = 1;
-      }else if( optionMatch(azArg[ii],"nosys") ){
-        bNoSystemTabs = 1;
-      }else if( azArg[ii][0]=='-' ){
-        utf8_printf(stderr, "Unknown option: \"%s\"\n", azArg[ii]);
-        rc = 1;
-        goto meta_command_exit;
-      }else if( zName==0 ){
-        zName = azArg[ii];
-      }else{
-        raw_printf(stderr, "Usage: .schema ?--indent? ?--nosys? ?LIKE-PATTERN?\n");
-        rc = 1;
-        goto meta_command_exit;
-      }
-    }
-    if( zName!=0 ){
-      int isSchema = sqlite3_strlike(zName, "sqlite_master", '\\')==0
-                  || sqlite3_strlike(zName, "sqlite_schema", '\\')==0
-                  || sqlite3_strlike(zName,"sqlite_temp_master", '\\')==0
-                  || sqlite3_strlike(zName,"sqlite_temp_schema", '\\')==0;
-      if( isSchema ){
-        char *new_argv[2], *new_colv[2];
-        new_argv[0] = sqlite3_mprintf(
-                      "CREATE TABLE %s (\n"
-                      "  type text,\n"
-                      "  name text,\n"
-                      "  tbl_name text,\n"
-                      "  rootpage integer,\n"
-                      "  sql text\n"
-                      ")", zName);
-        new_argv[1] = 0;
-        new_colv[0] = "sql";
-        new_colv[1] = 0;
-        callback(&data, 1, new_argv, new_colv);
-        sqlite3_free(new_argv[0]);
-      }
-    }
-    if( zDiv ){
-      sqlite3_stmt *pStmt = 0;
-      rc = sqlite3_prepare_v2(p->db, "SELECT name FROM pragma_database_list",
-                              -1, &pStmt, 0);
-      if( rc ){
-        utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
-        sqlite3_finalize(pStmt);
-        rc = 1;
-        goto meta_command_exit;
-      }
-      appendText(&sSelect, "SELECT sql FROM", 0);
-      iSchema = 0;
-      while( sqlite3_step(pStmt)==SQLITE_ROW ){
-        const char *zDb = (const char*)sqlite3_column_text(pStmt, 0);
-        char zScNum[30];
-        sqlite3_snprintf(sizeof(zScNum), zScNum, "%d", ++iSchema);
-        appendText(&sSelect, zDiv, 0);
-        zDiv = " UNION ALL ";
-        appendText(&sSelect, "SELECT shell_add_schema(sql,", 0);
-        if( sqlite3_stricmp(zDb, "main")!=0 ){
-          appendText(&sSelect, zDb, '\'');
-        }else{
-          appendText(&sSelect, "NULL", 0);
-        }
-        appendText(&sSelect, ",name) AS sql, type, tbl_name, name, rowid,", 0);
-        appendText(&sSelect, zScNum, 0);
-        appendText(&sSelect, " AS snum, ", 0);
+  }
+  if( zName!=0 ){
+    int isSchema = sqlite3_strlike(zName, "sqlite_master", '\\')==0
+      || sqlite3_strlike(zName, "sqlite_schema", '\\')==0
+      || sqlite3_strlike(zName,"sqlite_temp_master", '\\')==0
+      || sqlite3_strlike(zName,"sqlite_temp_schema", '\\')==0;
+    if( isSchema ){
+      char *new_argv[2], *new_colv[2];
+      new_argv[0] = sqlite3_mprintf(
+                                    "CREATE TABLE %s (\n"
+                                    "  type text,\n"
+                                    "  name text,\n"
+                                    "  tbl_name text,\n"
+                                    "  rootpage integer,\n"
+                                    "  sql text\n"
+                                    ")", zName);
+      new_argv[1] = 0;
+      new_colv[0] = "sql";
+      new_colv[1] = 0;
+      callback(&data, 1, new_argv, new_colv);
+      sqlite3_free(new_argv[0]);
+    }
+  }
+  if( zDiv ){
+    sqlite3_stmt *pStmt = 0;
+    rc = sqlite3_prepare_v2(p->db, "SELECT name FROM pragma_database_list",
+                            -1, &pStmt, 0);
+    if( rc ){
+      utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
+      sqlite3_finalize(pStmt);
+      return 1;
+    }
+    appendText(&sSelect, "SELECT sql FROM", 0);
+    iSchema = 0;
+    while( sqlite3_step(pStmt)==SQLITE_ROW ){
+      const char *zDb = (const char*)sqlite3_column_text(pStmt, 0);
+      char zScNum[30];
+      sqlite3_snprintf(sizeof(zScNum), zScNum, "%d", ++iSchema);
+      appendText(&sSelect, zDiv, 0);
+      zDiv = " UNION ALL ";
+      appendText(&sSelect, "SELECT shell_add_schema(sql,", 0);
+      if( sqlite3_stricmp(zDb, "main")!=0 ){
         appendText(&sSelect, zDb, '\'');
-        appendText(&sSelect, " AS sname FROM ", 0);
-        appendText(&sSelect, zDb, quoteChar(zDb));
-        appendText(&sSelect, ".sqlite_schema", 0);
+      }else{
+        appendText(&sSelect, "NULL", 0);
       }
-      sqlite3_finalize(pStmt);
+      appendText(&sSelect, ",name) AS sql, type, tbl_name, name, rowid,", 0);
+      appendText(&sSelect, zScNum, 0);
+      appendText(&sSelect, " AS snum, ", 0);
+      appendText(&sSelect, zDb, '\'');
+      appendText(&sSelect, " AS sname FROM ", 0);
+      appendText(&sSelect, zDb, quoteChar(zDb));
+      appendText(&sSelect, ".sqlite_schema", 0);
+    }
+    sqlite3_finalize(pStmt);
 #ifndef SQLITE_OMIT_INTROSPECTION_PRAGMAS
-      if( zName ){
-        appendText(&sSelect,
-           " UNION ALL SELECT shell_module_schema(name),"
-           " 'table', name, name, name, 9e+99, 'main' FROM pragma_module_list",
-        0);
-      }
-#endif
-      appendText(&sSelect, ") WHERE ", 0);
-      if( zName ){
-        char *zQarg = sqlite3_mprintf("%Q", zName);
-        int bGlob = strchr(zName, '*') != 0 || strchr(zName, '?') != 0 ||
-                    strchr(zName, '[') != 0;
-        if( strchr(zName, '.') ){
-          appendText(&sSelect, "lower(printf('%s.%s',sname,tbl_name))", 0);
-        }else{
-          appendText(&sSelect, "lower(tbl_name)", 0);
-        }
-        appendText(&sSelect, bGlob ? " GLOB " : " LIKE ", 0);
-        appendText(&sSelect, zQarg, 0);
-        if( !bGlob ){
-          appendText(&sSelect, " ESCAPE '\\' ", 0);
-        }
-        appendText(&sSelect, " AND ", 0);
-        sqlite3_free(zQarg);
-      }
-      if( bNoSystemTabs ){
-        appendText(&sSelect, "name NOT LIKE 'sqlite_%%' AND ", 0);
-      }
-      appendText(&sSelect, "sql IS NOT NULL"
-                           " ORDER BY snum, rowid", 0);
-      if( bDebug ){
-        utf8_printf(p->out, "SQL: %s;\n", sSelect.z);
+    if( zName ){
+      appendText(&sSelect,
+                 " UNION ALL SELECT shell_module_schema(name),"
+                 " 'table', name, name, name, 9e+99, 'main' FROM pragma_module_list",
+                 0);
+    }
+#endif
+    appendText(&sSelect, ") WHERE ", 0);
+    if( zName ){
+      char *zQarg = sqlite3_mprintf("%Q", zName);
+      int bGlob = strchr(zName, '*') != 0 || strchr(zName, '?') != 0 ||
+        strchr(zName, '[') != 0;
+      if( strchr(zName, '.') ){
+        appendText(&sSelect, "lower(printf('%s.%s',sname,tbl_name))", 0);
       }else{
-        rc = sqlite3_exec(p->db, sSelect.z, callback, &data, &zErrMsg);
+        appendText(&sSelect, "lower(tbl_name)", 0);
       }
-      freeText(&sSelect);
+      appendText(&sSelect, bGlob ? " GLOB " : " LIKE ", 0);
+      appendText(&sSelect, zQarg, 0);
+      if( !bGlob ){
+        appendText(&sSelect, " ESCAPE '\\' ", 0);
+      }
+      appendText(&sSelect, " AND ", 0);
+      sqlite3_free(zQarg);
     }
-    if( zErrMsg ){
-      utf8_printf(stderr,"Error: %s\n", zErrMsg);
-      sqlite3_free(zErrMsg);
-      rc = 1;
-    }else if( rc != SQLITE_OK ){
-      raw_printf(stderr,"Error: querying schema information\n");
-      rc = 1;
+    if( bNoSystemTabs ){
+      appendText(&sSelect, "name NOT LIKE 'sqlite_%%' AND ", 0);
+    }
+    appendText(&sSelect, "sql IS NOT NULL"
+               " ORDER BY snum, rowid", 0);
+    if( bDebug ){
+      utf8_printf(p->out, "SQL: %s;\n", sSelect.z);
     }else{
-      rc = 0;
+      rc = sqlite3_exec(p->db, sSelect.z, callback, &data, &zErrMsg);
     }
-  }else
-
-  if( c=='s' && n==11 && strncmp(azArg[0], "selecttrace", n)==0 ){
-    unsigned int x = nArg>=2 ? (unsigned int)integerValue(azArg[1]) : 0xffffffff;
-    sqlite3_test_control(SQLITE_TESTCTRL_TRACEFLAGS, 1, &x);
-  }else
+    freeText(&sSelect);
+  }
+  if( zErrMsg ){
+    utf8_printf(stderr,"Error: %s\n", zErrMsg);
+    sqlite3_free(zErrMsg);
+    rc = 1;
+  }else if( rc != SQLITE_OK ){
+    raw_printf(stderr,"Error: querying schema information\n");
+    rc = 1;
+  }else{
+    rc = 0;
+  }
+  return rc;
+}
 
-#if defined(SQLITE_ENABLE_SESSION)
-  if( c=='s' && strncmp(azArg[0],"session",n)==0 && n>=3 ){
-    OpenSession *pSession = &p->aSession[0];
-    char **azCmd = &azArg[1];
-    int iSes = 0;
-    int nCmd = nArg - 1;
-    int i;
-    if( nArg<=1 ) goto session_syntax_error;
-    open_db(p, 0);
-    if( nArg>=3 ){
-      for(iSes=0; iSes<p->nSession; iSes++){
-        if( strcmp(p->aSession[iSes].zName, azArg[1])==0 ) break;
-      }
-      if( iSes<p->nSession ){
-        pSession = &p->aSession[iSes];
-        azCmd++;
-        nCmd--;
-      }else{
-        pSession = &p->aSession[0];
-        iSes = 0;
-      }
+/*****************
+ * The .selecttrace, .separator, .session and .sha3sum commands
+ */
+CONDITION_COMMAND( session defined(SQLITE_ENABLE_SESSION) );
+COLLECT_HELP_TEXT[
+  ".separator COL ?ROW?     Change the column and row separators",
+  ".session ?NAME? CMD ...  Create or control sessions",
+  "   Subcommands:",
+  "     attach TABLE             Attach TABLE",
+  "     changeset FILE           Write a changeset into FILE",
+  "     close                    Close one session",
+  "     enable ?BOOLEAN?         Set or query the enable bit",
+  "     filter GLOB...           Reject tables matching GLOBs",
+  "     indirect ?BOOLEAN?       Mark or query the indirect status",
+  "     isempty                  Query whether the session is empty",
+  "     list                     List currently open session names",
+  "     open DB NAME             Open a new session on DB",
+  "     patchset FILE            Write a patchset into FILE",
+  "   If ?NAME? is omitted, the first defined session is used.",
+  ".sha3sum ...             Compute a SHA3 hash of database content",
+  "    Options:",
+  "      --schema              Also hash the sqlite_schema table",
+  "      --sha3-224            Use the sha3-224 algorithm",
+  "      --sha3-256            Use the sha3-256 algorithm (default)",
+  "      --sha3-384            Use the sha3-384 algorithm",
+  "      --sha3-512            Use the sha3-512 algorithm",
+  "    Any other argument is a LIKE pattern for tables to hash",
+];
+DISPATCHABLE_COMMAND( selecttrace ? 1 0 ){
+  unsigned int x = nArg>=2 ? (unsigned int)integerValue(azArg[1]) : 0xffffffff;
+  sqlite3_test_control(SQLITE_TESTCTRL_TRACEFLAGS, 1, &x);
+  return 0;
+}
+DISPATCHABLE_COMMAND( separator ? 2 3 ){
+  if( nArg>=2 ){
+    sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator,
+                     "%.*s", (int)ArraySize(p->colSeparator)-1, azArg[1]);
+  }
+  if( nArg>=3 ){
+    sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator,
+                     "%.*s", (int)ArraySize(p->rowSeparator)-1, azArg[2]);
+  }
+  return 0;
+}
+DISPATCHABLE_COMMAND( session 3 2 0 ){
+  int rc = 0;
+  OpenSession *pSession = &p->aSession[0];
+  char **azCmd = &azArg[1];
+  int iSes = 0;
+  int nCmd = nArg - 1;
+  int i;
+  open_db(p, 0);
+  if( nArg>=3 ){
+    for(iSes=0; iSes<p->nSession; iSes++){
+      if( strcmp(p->aSession[iSes].zName, azArg[1])==0 ) break;
+    }
+    if( iSes<p->nSession ){
+      pSession = &p->aSession[iSes];
+      azCmd++;
+      nCmd--;
+    }else{
+      pSession = &p->aSession[0];
+      iSes = 0;
     }
+  }
 
-    /* .session attach TABLE
-    ** Invoke the sqlite3session_attach() interface to attach a particular
-    ** table so that it is never filtered.
-    */
-    if( strcmp(azCmd[0],"attach")==0 ){
-      if( nCmd!=2 ) goto session_syntax_error;
-      if( pSession->p==0 ){
-        session_not_open:
-        raw_printf(stderr, "ERROR: No sessions are open\n");
-      }else{
-        rc = sqlite3session_attach(pSession->p, azCmd[1]);
-        if( rc ){
-          raw_printf(stderr, "ERROR: sqlite3session_attach() returns %d\n", rc);
-          rc = 0;
-        }
+  /* .session attach TABLE
+  ** Invoke the sqlite3session_attach() interface to attach a particular
+  ** table so that it is never filtered.
+  */
+  if( strcmp(azCmd[0],"attach")==0 ){
+    if( nCmd!=2 ) goto session_syntax_error;
+    if( pSession->p==0 ){
+    session_not_open:
+      raw_printf(stderr, "ERROR: No sessions are open\n");
+    }else{
+      rc = sqlite3session_attach(pSession->p, azCmd[1]);
+      if( rc ){
+        raw_printf(stderr, "ERROR: sqlite3session_attach() returns %d\n", rc);
+        rc = 0;
       }
-    }else
+    }
+  }else
 
     /* .session changeset FILE
     ** .session patchset FILE
@@ -9290,232 +9328,340 @@ static int do_meta_command(char *zLine, ShellState *p){
           rc = 0;
         }
         if( pChng
-          && fwrite(pChng, szChng, 1, out)!=1 ){
+            && fwrite(pChng, szChng, 1, out)!=1 ){
           raw_printf(stderr, "ERROR: Failed to write entire %d-byte output\n",
-                  szChng);
+                     szChng);
         }
         sqlite3_free(pChng);
         fclose(out);
       }
     }else
 
-    /* .session close
-    ** Close the identified session
-    */
-    if( strcmp(azCmd[0], "close")==0 ){
-      if( nCmd!=1 ) goto session_syntax_error;
-      if( p->nSession ){
-        session_close(pSession);
-        p->aSession[iSes] = p->aSession[--p->nSession];
-      }
-    }else
-
-    /* .session enable ?BOOLEAN?
-    ** Query or set the enable flag
-    */
-    if( strcmp(azCmd[0], "enable")==0 ){
-      int ii;
-      if( nCmd>2 ) goto session_syntax_error;
-      ii = nCmd==1 ? -1 : booleanValue(azCmd[1]);
-      if( p->nSession ){
-        ii = sqlite3session_enable(pSession->p, ii);
-        utf8_printf(p->out, "session %s enable flag = %d\n",
-                    pSession->zName, ii);
-      }
-    }else
-
-    /* .session filter GLOB ....
-    ** Set a list of GLOB patterns of table names to be excluded.
-    */
-    if( strcmp(azCmd[0], "filter")==0 ){
-      int ii, nByte;
-      if( nCmd<2 ) goto session_syntax_error;
-      if( p->nSession ){
-        for(ii=0; ii<pSession->nFilter; ii++){
-          sqlite3_free(pSession->azFilter[ii]);
-        }
-        sqlite3_free(pSession->azFilter);
-        nByte = sizeof(pSession->azFilter[0])*(nCmd-1);
-        pSession->azFilter = sqlite3_malloc( nByte );
-        if( pSession->azFilter==0 ){
-          raw_printf(stderr, "Error: out or memory\n");
-          exit(1);
-        }
-        for(ii=1; ii<nCmd; ii++){
-          pSession->azFilter[ii-1] = sqlite3_mprintf("%s", azCmd[ii]);
+      /* .session close
+      ** Close the identified session
+      */
+      if( strcmp(azCmd[0], "close")==0 ){
+        if( nCmd!=1 ) goto session_syntax_error;
+        if( p->nSession ){
+          session_close(pSession);
+          p->aSession[iSes] = p->aSession[--p->nSession];
         }
-        pSession->nFilter = ii-1;
-      }
-    }else
-
-    /* .session indirect ?BOOLEAN?
-    ** Query or set the indirect flag
-    */
-    if( strcmp(azCmd[0], "indirect")==0 ){
-      int ii;
-      if( nCmd>2 ) goto session_syntax_error;
-      ii = nCmd==1 ? -1 : booleanValue(azCmd[1]);
-      if( p->nSession ){
-        ii = sqlite3session_indirect(pSession->p, ii);
-        utf8_printf(p->out, "session %s indirect flag = %d\n",
-                    pSession->zName, ii);
-      }
-    }else
-
-    /* .session isempty
-    ** Determine if the session is empty
-    */
-    if( strcmp(azCmd[0], "isempty")==0 ){
-      int ii;
-      if( nCmd!=1 ) goto session_syntax_error;
-      if( p->nSession ){
-        ii = sqlite3session_isempty(pSession->p);
-        utf8_printf(p->out, "session %s isempty flag = %d\n",
-                    pSession->zName, ii);
-      }
-    }else
-
-    /* .session list
-    ** List all currently open sessions
-    */
-    if( strcmp(azCmd[0],"list")==0 ){
-      for(i=0; i<p->nSession; i++){
-        utf8_printf(p->out, "%d %s\n", i, p->aSession[i].zName);
-      }
-    }else
+      }else
 
-    /* .session open DB NAME
-    ** Open a new session called NAME on the attached database DB.
-    ** DB is normally "main".
-    */
-    if( strcmp(azCmd[0],"open")==0 ){
-      char *zName;
-      if( nCmd!=3 ) goto session_syntax_error;
-      zName = azCmd[2];
-      if( zName[0]==0 ) goto session_syntax_error;
-      for(i=0; i<p->nSession; i++){
-        if( strcmp(p->aSession[i].zName,zName)==0 ){
-          utf8_printf(stderr, "Session \"%s\" already exists\n", zName);
-          goto meta_command_exit;
-        }
-      }
-      if( p->nSession>=ArraySize(p->aSession) ){
-        raw_printf(stderr, "Maximum of %d sessions\n", ArraySize(p->aSession));
-        goto meta_command_exit;
-      }
-      pSession = &p->aSession[p->nSession];
-      rc = sqlite3session_create(p->db, azCmd[1], &pSession->p);
-      if( rc ){
-        raw_printf(stderr, "Cannot open session: error code=%d\n", rc);
-        rc = 0;
-        goto meta_command_exit;
-      }
-      pSession->nFilter = 0;
-      sqlite3session_table_filter(pSession->p, session_filter, pSession);
-      p->nSession++;
-      pSession->zName = sqlite3_mprintf("%s", zName);
-    }else
-    /* If no command name matches, show a syntax error */
-    session_syntax_error:
-    showHelp(p->out, "session");
-  }else
-#endif
+        /* .session enable ?BOOLEAN?
+        ** Query or set the enable flag
+        */
+        if( strcmp(azCmd[0], "enable")==0 ){
+          int ii;
+          if( nCmd>2 ) goto session_syntax_error;
+          ii = nCmd==1 ? -1 : booleanValue(azCmd[1]);
+          if( p->nSession ){
+            ii = sqlite3session_enable(pSession->p, ii);
+            utf8_printf(p->out, "session %s enable flag = %d\n",
+                        pSession->zName, ii);
+          }
+        }else
 
-#ifdef SQLITE_DEBUG
-  /* Undocumented commands for internal testing.  Subject to change
-  ** without notice. */
-  if( c=='s' && n>=10 && strncmp(azArg[0], "selftest-", 9)==0 ){
-    if( strncmp(azArg[0]+9, "boolean", n-9)==0 ){
-      int i, v;
-      for(i=1; i<nArg; i++){
-        v = booleanValue(azArg[i]);
-        utf8_printf(p->out, "%s: %d 0x%x\n", azArg[i], v, v);
-      }
-    }
-    if( strncmp(azArg[0]+9, "integer", n-9)==0 ){
-      int i; sqlite3_int64 v;
-      for(i=1; i<nArg; i++){
-        char zBuf[200];
-        v = integerValue(azArg[i]);
-        sqlite3_snprintf(sizeof(zBuf),zBuf,"%s: %lld 0x%llx\n", azArg[i],v,v);
-        utf8_printf(p->out, "%s", zBuf);
-      }
+          /* .session filter GLOB ....
+          ** Set a list of GLOB patterns of table names to be excluded.
+          */
+          if( strcmp(azCmd[0], "filter")==0 ){
+            int ii, nByte;
+            if( nCmd<2 ) goto session_syntax_error;
+            if( p->nSession ){
+              for(ii=0; ii<pSession->nFilter; ii++){
+                sqlite3_free(pSession->azFilter[ii]);
+              }
+              sqlite3_free(pSession->azFilter);
+              nByte = sizeof(pSession->azFilter[0])*(nCmd-1);
+              pSession->azFilter = sqlite3_malloc( nByte );
+              if( pSession->azFilter==0 ){
+                raw_printf(stderr, "Error: out or memory\n");
+                exit(1);
+              }
+              for(ii=1; ii<nCmd; ii++){
+                pSession->azFilter[ii-1] = sqlite3_mprintf("%s", azCmd[ii]);
+              }
+              pSession->nFilter = ii-1;
+            }
+          }else
+
+            /* .session indirect ?BOOLEAN?
+            ** Query or set the indirect flag
+            */
+            if( strcmp(azCmd[0], "indirect")==0 ){
+              int ii;
+              if( nCmd>2 ) goto session_syntax_error;
+              ii = nCmd==1 ? -1 : booleanValue(azCmd[1]);
+              if( p->nSession ){
+                ii = sqlite3session_indirect(pSession->p, ii);
+                utf8_printf(p->out, "session %s indirect flag = %d\n",
+                            pSession->zName, ii);
+              }
+            }else
+
+              /* .session isempty
+              ** Determine if the session is empty
+              */
+              if( strcmp(azCmd[0], "isempty")==0 ){
+                int ii;
+                if( nCmd!=1 ) goto session_syntax_error;
+                if( p->nSession ){
+                  ii = sqlite3session_isempty(pSession->p);
+                  utf8_printf(p->out, "session %s isempty flag = %d\n",
+                              pSession->zName, ii);
+                }
+              }else
+
+                /* .session list
+                ** List all currently open sessions
+                */
+                if( strcmp(azCmd[0],"list")==0 ){
+                  for(i=0; i<p->nSession; i++){
+                    utf8_printf(p->out, "%d %s\n", i, p->aSession[i].zName);
+                  }
+                }else
+
+                  /* .session open DB NAME
+                  ** Open a new session called NAME on the attached database DB.
+                  ** DB is normally "main".
+                  */
+                  if( strcmp(azCmd[0],"open")==0 ){
+                    char *zName;
+                    if( nCmd!=3 ) goto session_syntax_error;
+                    zName = azCmd[2];
+                    if( zName[0]==0 ) goto session_syntax_error;
+                    for(i=0; i<p->nSession; i++){
+                      if( strcmp(p->aSession[i].zName,zName)==0 ){
+                        utf8_printf(stderr, "Session \"%s\" already exists\n", zName);
+                        return rc;
+                      }
+                    }
+                    if( p->nSession>=ArraySize(p->aSession) ){
+                      raw_printf(stderr, "Maximum of %d sessions\n", ArraySize(p->aSession));
+                      return rc;
+                    }
+                    pSession = &p->aSession[p->nSession];
+                    rc = sqlite3session_create(p->db, azCmd[1], &pSession->p);
+                    if( rc ){
+                      raw_printf(stderr, "Cannot open session: error code=%d\n", rc);
+                      return rc;
+                    }
+                    pSession->nFilter = 0;
+                    sqlite3session_table_filter(pSession->p, session_filter, pSession);
+                    p->nSession++;
+                    pSession->zName = sqlite3_mprintf("%s", zName);
+                  }else{
+                    /* If no command name matches, show a syntax error */
+                  session_syntax_error:
+                    showHelp(p->out, "session");
+                    return 1;
+                  }
+  return rc;
+}
+DISPATCHABLE_COMMAND( sha3sum 4 1 1 ){
+  const char *zLike = 0;   /* Which table to checksum. 0 means everything */
+  int i;                   /* Loop counter */
+  int bSchema = 0;         /* Also hash the schema */
+  int bSeparate = 0;       /* Hash each table separately */
+  int iSize = 224;         /* Hash algorithm to use */
+  int bDebug = 0;          /* Only show the query that would have run */
+  sqlite3_stmt *pStmt;     /* For querying tables names */
+  char *zSql;              /* SQL to be run */
+  char *zSep;              /* Separator */
+  ShellText sSql;          /* Complete SQL for the query to run the hash */
+  ShellText sQuery;        /* Set of queries used to read all content */
+  open_db(p, 0);
+  for(i=1; i<nArg; i++){
+    const char *z = azArg[i];
+    if( z[0]=='-' ){
+      z++;
+      if( z[0]=='-' ) z++;
+      if( strcmp(z,"schema")==0 ){
+        bSchema = 1;
+      }else
+        if( strcmp(z,"sha3-224")==0 || strcmp(z,"sha3-256")==0
+            || strcmp(z,"sha3-384")==0 || strcmp(z,"sha3-512")==0
+            ){
+          iSize = atoi(&z[5]);
+        }else
+          if( strcmp(z,"debug")==0 ){
+            bDebug = 1;
+          }else
+            {
+              utf8_printf(stderr, "Unknown option \"%s\" on \"%s\"\n",
+                          azArg[i], azArg[0]);
+              showHelp(p->out, azArg[0]);
+              return 1;
+            }
+    }else if( zLike ){
+      raw_printf(stderr, "Usage: .sha3sum ?OPTIONS? ?LIKE-PATTERN?\n");
+      return 1;
+    }else{
+      zLike = z;
+      bSeparate = 1;
+      if( sqlite3_strlike("sqlite\\_%", zLike, '\\')==0 ) bSchema = 1;
     }
-  }else
-#endif
+  }
+  if( bSchema ){
+    zSql = "SELECT lower(name) FROM sqlite_schema"
+      " WHERE type='table' AND coalesce(rootpage,0)>1"
+      " UNION ALL SELECT 'sqlite_schema'"
+      " ORDER BY 1 collate nocase";
+  }else{
+    zSql = "SELECT lower(name) FROM sqlite_schema"
+      " WHERE type='table' AND coalesce(rootpage,0)>1"
+      " AND name NOT LIKE 'sqlite_%'"
+      " ORDER BY 1 collate nocase";
+  }
+  sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+  initText(&sQuery);
+  initText(&sSql);
+  appendText(&sSql, "WITH [sha3sum$query](a,b) AS(",0);
+  zSep = "VALUES(";
+  while( SQLITE_ROW==sqlite3_step(pStmt) ){
+    const char *zTab = (const char*)sqlite3_column_text(pStmt,0);
+    if( zLike && sqlite3_strlike(zLike, zTab, 0)!=0 ) continue;
+    if( strncmp(zTab, "sqlite_",7)!=0 ){
+      appendText(&sQuery,"SELECT * FROM ", 0);
+      appendText(&sQuery,zTab,'"');
+      appendText(&sQuery," NOT INDEXED;", 0);
+    }else if( strcmp(zTab, "sqlite_schema")==0 ){
+      appendText(&sQuery,"SELECT type,name,tbl_name,sql FROM sqlite_schema"
+                 " ORDER BY name;", 0);
+    }else if( strcmp(zTab, "sqlite_sequence")==0 ){
+      appendText(&sQuery,"SELECT name,seq FROM sqlite_sequence"
+                 " ORDER BY name;", 0);
+    }else if( strcmp(zTab, "sqlite_stat1")==0 ){
+      appendText(&sQuery,"SELECT tbl,idx,stat FROM sqlite_stat1"
+                 " ORDER BY tbl,idx;", 0);
+    }else if( strcmp(zTab, "sqlite_stat4")==0 ){
+      appendText(&sQuery, "SELECT * FROM ", 0);
+      appendText(&sQuery, zTab, 0);
+      appendText(&sQuery, " ORDER BY tbl, idx, rowid;\n", 0);
+    }
+    appendText(&sSql, zSep, 0);
+    appendText(&sSql, sQuery.z, '\'');
+    sQuery.n = 0;
+    appendText(&sSql, ",", 0);
+    appendText(&sSql, zTab, '\'');
+    zSep = "),(";
+  }
+  sqlite3_finalize(pStmt);
+  if( bSeparate ){
+    zSql = sqlite3_mprintf(
+           "%s))"
+           " SELECT lower(hex(sha3_query(a,%d))) AS hash, b AS label"
+           "   FROM [sha3sum$query]",
+           sSql.z, iSize);
+  }else{
+    zSql = sqlite3_mprintf(
+           "%s))"
+           " SELECT lower(hex(sha3_query(group_concat(a,''),%d))) AS hash"
+           "   FROM [sha3sum$query]",
+           sSql.z, iSize);
+  }
+  freeText(&sQuery);
+  freeText(&sSql);
+  if( bDebug ){
+    utf8_printf(p->out, "%s\n", zSql);
+  }else{
+    shell_exec(p, zSql, 0);
+  }
+  sqlite3_free(zSql);
+  return 0;
+}
 
-  if( c=='s' && n>=4 && strncmp(azArg[0],"selftest",n)==0 ){
-    int bIsInit = 0;         /* True to initialize the SELFTEST table */
-    int bVerbose = 0;        /* Verbose output */
-    int bSelftestExists;     /* True if SELFTEST already exists */
-    int i, k;                /* Loop counters */
-    int nTest = 0;           /* Number of tests runs */
-    int nErr = 0;            /* Number of errors seen */
-    ShellText str;           /* Answer for a query */
-    sqlite3_stmt *pStmt = 0; /* Query against the SELFTEST table */
-
-    open_db(p,0);
-    for(i=1; i<nArg; i++){
-      const char *z = azArg[i];
-      if( z[0]=='-' && z[1]=='-' ) z++;
-      if( strcmp(z,"-init")==0 ){
-        bIsInit = 1;
-      }else
+/*****************
+ * The .selftest, .shell, .show, .stats and .system commands
+ */
+CONDITION_COMMAND( shell !defined(SQLITE_NOHAVE_SYSTEM) );
+CONDITION_COMMAND( system !defined(SQLITE_NOHAVE_SYSTEM) );
+COLLECT_HELP_TEXT[
+  ".selftest ?OPTIONS?      Run tests defined in the SELFTEST table",
+  "    Options:",
+  "       --init               Create a new SELFTEST table",
+  "       -v                   Verbose output",
+  ".shell CMD ARGS...       Run CMD ARGS... in a system shell",
+  ".show                    Show the current values for various settings",
+  ".stats ?ARG?             Show stats or turn stats on or off",
+  "   off                      Turn off automatic stat display",
+  "   on                       Turn on automatic stat display",
+  "   stmt                     Show statement stats",
+  "   vmstep                   Show the virtual machine step count only",
+  ".system CMD ARGS...      Run CMD ARGS... in a system shell",
+];
+DISPATCHABLE_COMMAND( selftest 4 0 0 ){
+  int rc;
+  int bIsInit = 0;         /* True to initialize the SELFTEST table */
+  int bVerbose = 0;        /* Verbose output */
+  int bSelftestExists;     /* True if SELFTEST already exists */
+  int i, k;                /* Loop counters */
+  int nTest = 0;           /* Number of tests runs */
+  int nErr = 0;            /* Number of errors seen */
+  ShellText str;           /* Answer for a query */
+  sqlite3_stmt *pStmt = 0; /* Query against the SELFTEST table */
+
+  open_db(p,0);
+  for(i=1; i<nArg; i++){
+    const char *z = azArg[i];
+    if( z[0]=='-' && z[1]=='-' ) z++;
+    if( strcmp(z,"-init")==0 ){
+      bIsInit = 1;
+    }else
       if( strcmp(z,"-v")==0 ){
         bVerbose++;
       }else
-      {
-        utf8_printf(stderr, "Unknown option \"%s\" on \"%s\"\n",
-                    azArg[i], azArg[0]);
-        raw_printf(stderr, "Should be one of: --init -v\n");
-        rc = 1;
-        goto meta_command_exit;
-      }
-    }
-    if( sqlite3_table_column_metadata(p->db,"main","selftest",0,0,0,0,0,0)
-           != SQLITE_OK ){
-      bSelftestExists = 0;
+        {
+          utf8_printf(stderr, "Unknown option \"%s\" on \"%s\"\n",
+                      azArg[i], azArg[0]);
+          raw_printf(stderr, "Should be one of: --init -v\n");
+          return 1;
+        }
+  }
+  if( sqlite3_table_column_metadata(p->db,"main","selftest",0,0,0,0,0,0)
+      != SQLITE_OK ){
+    bSelftestExists = 0;
+  }else{
+    bSelftestExists = 1;
+  }
+  if( bIsInit ){
+    createSelftestTable(p);
+    bSelftestExists = 1;
+  }
+  initText(&str);
+  appendText(&str, "x", 0);
+  for(k=bSelftestExists; k>=0; k--){
+    if( k==1 ){
+      rc = sqlite3_prepare_v2(p->db,
+              "SELECT tno,op,cmd,ans FROM selftest ORDER BY tno",
+              -1, &pStmt, 0);
     }else{
-      bSelftestExists = 1;
-    }
-    if( bIsInit ){
-      createSelftestTable(p);
-      bSelftestExists = 1;
-    }
-    initText(&str);
-    appendText(&str, "x", 0);
-    for(k=bSelftestExists; k>=0; k--){
-      if( k==1 ){
-        rc = sqlite3_prepare_v2(p->db,
-            "SELECT tno,op,cmd,ans FROM selftest ORDER BY tno",
-            -1, &pStmt, 0);
-      }else{
-        rc = sqlite3_prepare_v2(p->db,
-          "VALUES(0,'memo','Missing SELFTEST table - default checks only',''),"
-          "      (1,'run','PRAGMA integrity_check','ok')",
-          -1, &pStmt, 0);
+      rc = sqlite3_prepare_v2(p->db,
+              "VALUES(0,'memo','Missing SELFTEST table - default checks only',''),"
+              "      (1,'run','PRAGMA integrity_check','ok')",
+              -1, &pStmt, 0);
+    }
+    if( rc ){
+      raw_printf(stderr, "Error querying the selftest table\n");
+      sqlite3_finalize(pStmt);
+      return 1;
+    }
+    for(i=1; sqlite3_step(pStmt)==SQLITE_ROW; i++){
+      int tno = sqlite3_column_int(pStmt, 0);
+      const char *zOp = (const char*)sqlite3_column_text(pStmt, 1);
+      const char *zSql = (const char*)sqlite3_column_text(pStmt, 2);
+      const char *zAns = (const char*)sqlite3_column_text(pStmt, 3);
+
+      k = 0;
+      if( bVerbose>0 ){
+        char *zQuote = sqlite3_mprintf("%q", zSql);
+        printf("%d: %s %s\n", tno, zOp, zSql);
+        sqlite3_free(zQuote);
       }
-      if( rc ){
-        raw_printf(stderr, "Error querying the selftest table\n");
-        rc = 1;
-        sqlite3_finalize(pStmt);
-        goto meta_command_exit;
-      }
-      for(i=1; sqlite3_step(pStmt)==SQLITE_ROW; i++){
-        int tno = sqlite3_column_int(pStmt, 0);
-        const char *zOp = (const char*)sqlite3_column_text(pStmt, 1);
-        const char *zSql = (const char*)sqlite3_column_text(pStmt, 2);
-        const char *zAns = (const char*)sqlite3_column_text(pStmt, 3);
-
-        k = 0;
-        if( bVerbose>0 ){
-          char *zQuote = sqlite3_mprintf("%q", zSql);
-          printf("%d: %s %s\n", tno, zOp, zSql);
-          sqlite3_free(zQuote);
-        }
-        if( strcmp(zOp,"memo")==0 ){
-          utf8_printf(p->out, "%s\n", zSql);
-        }else
+      if( strcmp(zOp,"memo")==0 ){
+        utf8_printf(p->out, "%s\n", zSql);
+      }else
         if( strcmp(zOp,"run")==0 ){
           char *zErrMsg = 0;
           str.n = 0;
@@ -9537,804 +9683,873 @@ static int do_meta_command(char *zLine, ShellState *p){
             utf8_printf(p->out, "%d:      Got: [%s]\n", tno, str.z);
           }
         }else
-        {
-          utf8_printf(stderr,
-            "Unknown operation \"%s\" on selftest line %d\n", zOp, tno);
-          rc = 1;
-          break;
-        }
-      } /* End loop over rows of content from SELFTEST */
-      sqlite3_finalize(pStmt);
-    } /* End loop over k */
-    freeText(&str);
-    utf8_printf(p->out, "%d errors out of %d tests\n", nErr, nTest);
-  }else
-
-  if( c=='s' && strncmp(azArg[0], "separator", n)==0 ){
-    if( nArg<2 || nArg>3 ){
-      raw_printf(stderr, "Usage: .separator COL ?ROW?\n");
-      rc = 1;
-    }
-    if( nArg>=2 ){
-      sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator,
-                       "%.*s", (int)ArraySize(p->colSeparator)-1, azArg[1]);
-    }
-    if( nArg>=3 ){
-      sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator,
-                       "%.*s", (int)ArraySize(p->rowSeparator)-1, azArg[2]);
-    }
-  }else
-
-  if( c=='s' && n>=4 && strncmp(azArg[0],"sha3sum",n)==0 ){
-    const char *zLike = 0;   /* Which table to checksum. 0 means everything */
-    int i;                   /* Loop counter */
-    int bSchema = 0;         /* Also hash the schema */
-    int bSeparate = 0;       /* Hash each table separately */
-    int iSize = 224;         /* Hash algorithm to use */
-    int bDebug = 0;          /* Only show the query that would have run */
-    sqlite3_stmt *pStmt;     /* For querying tables names */
-    char *zSql;              /* SQL to be run */
-    char *zSep;              /* Separator */
-    ShellText sSql;          /* Complete SQL for the query to run the hash */
-    ShellText sQuery;        /* Set of queries used to read all content */
-    open_db(p, 0);
-    for(i=1; i<nArg; i++){
-      const char *z = azArg[i];
-      if( z[0]=='-' ){
-        z++;
-        if( z[0]=='-' ) z++;
-        if( strcmp(z,"schema")==0 ){
-          bSchema = 1;
-        }else
-        if( strcmp(z,"sha3-224")==0 || strcmp(z,"sha3-256")==0
-         || strcmp(z,"sha3-384")==0 || strcmp(z,"sha3-512")==0
-        ){
-          iSize = atoi(&z[5]);
-        }else
-        if( strcmp(z,"debug")==0 ){
-          bDebug = 1;
-        }else
-        {
-          utf8_printf(stderr, "Unknown option \"%s\" on \"%s\"\n",
-                      azArg[i], azArg[0]);
-          showHelp(p->out, azArg[0]);
-          rc = 1;
-          goto meta_command_exit;
-        }
-      }else if( zLike ){
-        raw_printf(stderr, "Usage: .sha3sum ?OPTIONS? ?LIKE-PATTERN?\n");
-        rc = 1;
-        goto meta_command_exit;
-      }else{
-        zLike = z;
-        bSeparate = 1;
-        if( sqlite3_strlike("sqlite\\_%", zLike, '\\')==0 ) bSchema = 1;
-      }
-    }
-    if( bSchema ){
-      zSql = "SELECT lower(name) FROM sqlite_schema"
-             " WHERE type='table' AND coalesce(rootpage,0)>1"
-             " UNION ALL SELECT 'sqlite_schema'"
-             " ORDER BY 1 collate nocase";
-    }else{
-      zSql = "SELECT lower(name) FROM sqlite_schema"
-             " WHERE type='table' AND coalesce(rootpage,0)>1"
-             " AND name NOT LIKE 'sqlite_%'"
-             " ORDER BY 1 collate nocase";
-    }
-    sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
-    initText(&sQuery);
-    initText(&sSql);
-    appendText(&sSql, "WITH [sha3sum$query](a,b) AS(",0);
-    zSep = "VALUES(";
-    while( SQLITE_ROW==sqlite3_step(pStmt) ){
-      const char *zTab = (const char*)sqlite3_column_text(pStmt,0);
-      if( zLike && sqlite3_strlike(zLike, zTab, 0)!=0 ) continue;
-      if( strncmp(zTab, "sqlite_",7)!=0 ){
-        appendText(&sQuery,"SELECT * FROM ", 0);
-        appendText(&sQuery,zTab,'"');
-        appendText(&sQuery," NOT INDEXED;", 0);
-      }else if( strcmp(zTab, "sqlite_schema")==0 ){
-        appendText(&sQuery,"SELECT type,name,tbl_name,sql FROM sqlite_schema"
-                           " ORDER BY name;", 0);
-      }else if( strcmp(zTab, "sqlite_sequence")==0 ){
-        appendText(&sQuery,"SELECT name,seq FROM sqlite_sequence"
-                           " ORDER BY name;", 0);
-      }else if( strcmp(zTab, "sqlite_stat1")==0 ){
-        appendText(&sQuery,"SELECT tbl,idx,stat FROM sqlite_stat1"
-                           " ORDER BY tbl,idx;", 0);
-      }else if( strcmp(zTab, "sqlite_stat4")==0 ){
-        appendText(&sQuery, "SELECT * FROM ", 0);
-        appendText(&sQuery, zTab, 0);
-        appendText(&sQuery, " ORDER BY tbl, idx, rowid;\n", 0);
-      }
-      appendText(&sSql, zSep, 0);
-      appendText(&sSql, sQuery.z, '\'');
-      sQuery.n = 0;
-      appendText(&sSql, ",", 0);
-      appendText(&sSql, zTab, '\'');
-      zSep = "),(";
-    }
+          {
+            utf8_printf(stderr,
+                        "Unknown operation \"%s\" on selftest line %d\n", zOp, tno);
+            rc = 1;
+            break;
+          }
+    } /* End loop over rows of content from SELFTEST */
     sqlite3_finalize(pStmt);
-    if( bSeparate ){
-      zSql = sqlite3_mprintf(
-          "%s))"
-          " SELECT lower(hex(sha3_query(a,%d))) AS hash, b AS label"
-          "   FROM [sha3sum$query]",
-          sSql.z, iSize);
-    }else{
-      zSql = sqlite3_mprintf(
-          "%s))"
-          " SELECT lower(hex(sha3_query(group_concat(a,''),%d))) AS hash"
-          "   FROM [sha3sum$query]",
-          sSql.z, iSize);
-    }
-    freeText(&sQuery);
-    freeText(&sSql);
-    if( bDebug ){
-      utf8_printf(p->out, "%s\n", zSql);
-    }else{
-      shell_exec(p, zSql, 0);
-    }
-    sqlite3_free(zSql);
-  }else
-
+  } /* End loop over k */
+  freeText(&str);
+  utf8_printf(p->out, "%d errors out of %d tests\n", nErr, nTest);
+  return rc > 0;
+}
 #ifndef SQLITE_NOHAVE_SYSTEM
-  if( c=='s'
-   && (strncmp(azArg[0], "shell", n)==0 || strncmp(azArg[0],"system",n)==0)
-  ){
-    char *zCmd;
-    int i, x;
-    if( nArg<2 ){
-      raw_printf(stderr, "Usage: .system COMMAND\n");
-      rc = 1;
-      goto meta_command_exit;
-    }
-    zCmd = sqlite3_mprintf(strchr(azArg[1],' ')==0?"%s":"\"%s\"", azArg[1]);
-    for(i=2; i<nArg; i++){
-      zCmd = sqlite3_mprintf(strchr(azArg[i],' ')==0?"%z %s":"%z \"%s\"",
-                             zCmd, azArg[i]);
-    }
-    x = system(zCmd);
-    sqlite3_free(zCmd);
-    if( x ) raw_printf(stderr, "System command returns %d\n", x);
-  }else
-#endif /* !defined(SQLITE_NOHAVE_SYSTEM) */
-
-  if( c=='s' && strncmp(azArg[0], "show", n)==0 ){
-    static const char *azBool[] = { "off", "on", "trigger", "full"};
-    const char *zOut;
-    int i;
-    if( nArg!=1 ){
-      raw_printf(stderr, "Usage: .show\n");
-      rc = 1;
-      goto meta_command_exit;
-    }
-    utf8_printf(p->out, "%12.12s: %s\n","echo",
-                                  azBool[ShellHasFlag(p, SHFLG_Echo)]);
-    utf8_printf(p->out, "%12.12s: %s\n","eqp", azBool[p->autoEQP&3]);
-    utf8_printf(p->out, "%12.12s: %s\n","explain",
-         p->mode==MODE_Explain ? "on" : p->autoExplain ? "auto" : "off");
-    utf8_printf(p->out,"%12.12s: %s\n","headers", azBool[p->showHeader!=0]);
-    utf8_printf(p->out, "%12.12s: %s\n","mode", modeDescr[p->mode]);
-    utf8_printf(p->out, "%12.12s: ", "nullvalue");
-      output_c_string(p->out, p->nullValue);
-      raw_printf(p->out, "\n");
-    utf8_printf(p->out,"%12.12s: %s\n","output",
-            strlen30(p->outfile) ? p->outfile : "stdout");
-    utf8_printf(p->out,"%12.12s: ", "colseparator");
-      output_c_string(p->out, p->colSeparator);
-      raw_printf(p->out, "\n");
-    utf8_printf(p->out,"%12.12s: ", "rowseparator");
-      output_c_string(p->out, p->rowSeparator);
-      raw_printf(p->out, "\n");
-    switch( p->statsOn ){
-      case 0:  zOut = "off";     break;
-      default: zOut = "on";      break;
-      case 2:  zOut = "stmt";    break;
-      case 3:  zOut = "vmstep";  break;
-    }
-    utf8_printf(p->out, "%12.12s: %s\n","stats", zOut);
-    utf8_printf(p->out, "%12.12s: ", "width");
-    for (i=0;i<p->nWidth;i++) {
-      raw_printf(p->out, "%d ", p->colWidth[i]);
-    }
-    raw_printf(p->out, "\n");
-    utf8_printf(p->out, "%12.12s: %s\n", "filename",
-                p->zDbFilename ? p->zDbFilename : "");
-  }else
-
-  if( c=='s' && strncmp(azArg[0], "stats", n)==0 ){
-    if( nArg==2 ){
-      if( strcmp(azArg[1],"stmt")==0 ){
-        p->statsOn = 2;
-      }else if( strcmp(azArg[1],"vmstep")==0 ){
-        p->statsOn = 3;
-      }else{
-        p->statsOn = (u8)booleanValue(azArg[1]);
-      }
-    }else if( nArg==1 ){
-      display_stats(p->db, p, 0);
+static int shellOut(char *azArg[], int nArg, ShellState *p){
+  char *zCmd;
+  int i, x;
+  zCmd = sqlite3_mprintf(strchr(azArg[1],' ')==0?"%s":"\"%s\"", azArg[1]);
+  for(i=2; i<nArg; i++){
+    zCmd = sqlite3_mprintf(strchr(azArg[i],' ')==0?"%z %s":"%z \"%s\"",
+                           zCmd, azArg[i]);
+  }
+  x = system(zCmd);
+  sqlite3_free(zCmd);
+  if( x ) raw_printf(stderr, "%s command returns %d\n", azArg[0], x);
+  return 0;
+}
+#endif
+DISPATCHABLE_COMMAND( shell ? 2 0 ){
+  return shellOut(azArg, nArg, p);  
+}
+DISPATCHABLE_COMMAND( system ? 2 0 ){
+  return shellOut(azArg, nArg, p);  
+}
+DISPATCHABLE_COMMAND( show ? 1 1 ){
+  static const char *azBool[] = { "off", "on", "trigger", "full"};
+  const char *zOut;
+  int i;
+  utf8_printf(p->out, "%12.12s: %s\n","echo",
+              azBool[ShellHasFlag(p, SHFLG_Echo)]);
+  utf8_printf(p->out, "%12.12s: %s\n","eqp", azBool[p->autoEQP&3]);
+  utf8_printf(p->out, "%12.12s: %s\n","explain",
+              p->mode==MODE_Explain ? "on" : p->autoExplain ? "auto" : "off");
+  utf8_printf(p->out,"%12.12s: %s\n","headers", azBool[p->showHeader!=0]);
+  utf8_printf(p->out, "%12.12s: %s\n","mode", modeDescr[p->mode]);
+  utf8_printf(p->out, "%12.12s: ", "nullvalue");
+  output_c_string(p->out, p->nullValue);
+  raw_printf(p->out, "\n");
+  utf8_printf(p->out,"%12.12s: %s\n","output",
+              strlen30(p->outfile) ? p->outfile : "stdout");
+  utf8_printf(p->out,"%12.12s: ", "colseparator");
+  output_c_string(p->out, p->colSeparator);
+  raw_printf(p->out, "\n");
+  utf8_printf(p->out,"%12.12s: ", "rowseparator");
+  output_c_string(p->out, p->rowSeparator);
+  raw_printf(p->out, "\n");
+  switch( p->statsOn ){
+  case 0:  zOut = "off";     break;
+  default: zOut = "on";      break;
+  case 2:  zOut = "stmt";    break;
+  case 3:  zOut = "vmstep";  break;
+  }
+  utf8_printf(p->out, "%12.12s: %s\n","stats", zOut);
+  utf8_printf(p->out, "%12.12s: ", "width");
+  for (i=0;i<p->nWidth;i++) {
+    raw_printf(p->out, "%d ", p->colWidth[i]);
+  }
+  raw_printf(p->out, "\n");
+  utf8_printf(p->out, "%12.12s: %s\n", "filename",
+              p->zDbFilename ? p->zDbFilename : "");
+  return 0;
+}
+DISPATCHABLE_COMMAND( stats ? 0 0 ){
+  if( nArg==2 ){
+    if( strcmp(azArg[1],"stmt")==0 ){
+      p->statsOn = 2;
+    }else if( strcmp(azArg[1],"vmstep")==0 ){
+      p->statsOn = 3;
     }else{
-      raw_printf(stderr, "Usage: .stats ?on|off|stmt|vmstep?\n");
-      rc = 1;
+      p->statsOn = (u8)booleanValue(azArg[1]);
     }
-  }else
+  }else if( nArg==1 ){
+    display_stats(p->db, p, 0);
+  }else{
+    raw_printf(stderr, "Usage: .stats ?on|off|stmt|vmstep?\n");
+    return 1;
+  }
+  return 0;
+}
 
-  if( (c=='t' && n>1 && strncmp(azArg[0], "tables", n)==0)
-   || (c=='i' && (strncmp(azArg[0], "indices", n)==0
-                 || strncmp(azArg[0], "indexes", n)==0) )
-  ){
-    sqlite3_stmt *pStmt;
-    char **azResult;
-    int nRow, nAlloc;
-    int ii;
-    ShellText s;
-    initText(&s);
-    open_db(p, 0);
-    rc = sqlite3_prepare_v2(p->db, "PRAGMA database_list", -1, &pStmt, 0);
-    if( rc ){
-      sqlite3_finalize(pStmt);
-      return shellDatabaseError(p->db);
-    }
+/*****************
+ * The .tables, .views, .indices and .indexes command
+ * These are kept together because they share implementation or are aliases.
+ */
+COLLECT_HELP_TEXT[
+  ".indexes ?TABLE?         Show names of indexes",
+  "                           If TABLE is specified, only show indexes for",
+  "                           tables matching TABLE using the LIKE operator.",
+  ".tables ?TABLE?          List names of tables/views matching LIKE pattern TABLE",
+  ".views  ?VIEW            List names of only views matching LIKE pattern TABLE",
+];
+static int showTableLike(char *azArg[], int nArg, ShellState *p, char ot){
+  int rc;
+  sqlite3_stmt *pStmt;
+  char **azResult;
+  int nRow, nAlloc;
+  int ii;
+  ShellText s;
+  initText(&s);
+  open_db(p, 0);
+  rc = sqlite3_prepare_v2(p->db, "PRAGMA database_list", -1, &pStmt, 0);
+  if( rc ){
+    sqlite3_finalize(pStmt);
+    return shellDatabaseError(p->db);
+  }
 
-    if( nArg>2 && c=='i' ){
-      /* It is an historical accident that the .indexes command shows an error
-      ** when called with the wrong number of arguments whereas the .tables
-      ** command does not. */
-      raw_printf(stderr, "Usage: .indexes ?LIKE-PATTERN?\n");
-      rc = 1;
-      sqlite3_finalize(pStmt);
-      goto meta_command_exit;
-    }
-    for(ii=0; sqlite3_step(pStmt)==SQLITE_ROW; ii++){
-      const char *zDbName = (const char*)sqlite3_column_text(pStmt, 1);
-      if( zDbName==0 ) continue;
-      if( s.z && s.z[0] ) appendText(&s, " UNION ALL ", 0);
-      if( sqlite3_stricmp(zDbName, "main")==0 ){
-        appendText(&s, "SELECT name FROM ", 0);
-      }else{
-        appendText(&s, "SELECT ", 0);
-        appendText(&s, zDbName, '\'');
-        appendText(&s, "||'.'||name FROM ", 0);
-      }
-      appendText(&s, zDbName, '"');
-      appendText(&s, ".sqlite_schema ", 0);
-      if( c=='t' ){
-        appendText(&s," WHERE type IN ('table','view')"
-                      "   AND name NOT LIKE 'sqlite_%'"
-                      "   AND name LIKE ?1", 0);
-      }else{
-        appendText(&s," WHERE type='index'"
-                      "   AND tbl_name LIKE ?1", 0);
-      }
-    }
-    rc = sqlite3_finalize(pStmt);
-    appendText(&s, " ORDER BY 1", 0);
-    rc = sqlite3_prepare_v2(p->db, s.z, -1, &pStmt, 0);
-    freeText(&s);
-    if( rc ) return shellDatabaseError(p->db);
-
-    /* Run the SQL statement prepared by the above block. Store the results
-    ** as an array of nul-terminated strings in azResult[].  */
-    nRow = nAlloc = 0;
-    azResult = 0;
-    if( nArg>1 ){
-      sqlite3_bind_text(pStmt, 1, azArg[1], -1, SQLITE_TRANSIENT);
+  if( nArg>2 && ot=='i' ){
+    /* It is an historical accident that the .indexes command shows an error
+    ** when called with the wrong number of arguments whereas the .tables
+    ** command does not. */
+    raw_printf(stderr, "Usage: .indexes ?LIKE-PATTERN?\n");
+    sqlite3_finalize(pStmt);
+    return 1;
+  }
+  for(ii=0; sqlite3_step(pStmt)==SQLITE_ROW; ii++){
+    const char *zDbName = (const char*)sqlite3_column_text(pStmt, 1);
+    if( zDbName==0 ) continue;
+    if( s.z && s.z[0] ) appendText(&s, " UNION ALL ", 0);
+    if( sqlite3_stricmp(zDbName, "main")==0 ){
+      appendText(&s, "SELECT name FROM ", 0);
     }else{
-      sqlite3_bind_text(pStmt, 1, "%", -1, SQLITE_STATIC);
-    }
-    while( sqlite3_step(pStmt)==SQLITE_ROW ){
-      if( nRow>=nAlloc ){
-        char **azNew;
-        int n2 = nAlloc*2 + 10;
-        azNew = sqlite3_realloc64(azResult, sizeof(azResult[0])*n2);
-        if( azNew==0 ) shell_out_of_memory();
-        nAlloc = n2;
-        azResult = azNew;
-      }
-      azResult[nRow] = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 0));
-      if( 0==azResult[nRow] ) shell_out_of_memory();
-      nRow++;
-    }
-    if( sqlite3_finalize(pStmt)!=SQLITE_OK ){
-      rc = shellDatabaseError(p->db);
-    }
-
-    /* Pretty-print the contents of array azResult[] to the output */
-    if( rc==0 && nRow>0 ){
-      int len, maxlen = 0;
-      int i, j;
-      int nPrintCol, nPrintRow;
-      for(i=0; i<nRow; i++){
-        len = strlen30(azResult[i]);
-        if( len>maxlen ) maxlen = len;
-      }
-      nPrintCol = 80/(maxlen+2);
-      if( nPrintCol<1 ) nPrintCol = 1;
-      nPrintRow = (nRow + nPrintCol - 1)/nPrintCol;
-      for(i=0; i<nPrintRow; i++){
-        for(j=i; j<nRow; j+=nPrintRow){
-          char *zSp = j<nPrintRow ? "" : "  ";
-          utf8_printf(p->out, "%s%-*s", zSp, maxlen,
-                      azResult[j] ? azResult[j]:"");
-        }
-        raw_printf(p->out, "\n");
+      appendText(&s, "SELECT ", 0);
+      appendText(&s, zDbName, '\'');
+      appendText(&s, "||'.'||name FROM ", 0);
+    }
+    appendText(&s, zDbName, '"');
+    appendText(&s, ".sqlite_schema ", 0);
+    if( ot=='i' ){
+      appendText(&s," WHERE type='index'"
+                 "   AND tbl_name LIKE ?1", 0);
+    }else{
+      appendText(&s," WHERE type IN ('view'", 0);
+      if( ot=='t' ){
+        appendText(&s, ",'table'", 0);
+      }
+      appendText(&s, ") AND name NOT LIKE 'sqlite_%'"
+                 "   AND name LIKE ?1"
+                 "   AND name NOT LIKE 'sqlite_%'"
+                 "   AND name LIKE ?1", 0);
+    }
+  }
+  rc = sqlite3_finalize(pStmt);
+  appendText(&s, " ORDER BY 1", 0);
+  rc = sqlite3_prepare_v2(p->db, s.z, -1, &pStmt, 0);
+  freeText(&s);
+  if( rc ) return shellDatabaseError(p->db);
+
+  /* Run the SQL statement prepared by the above block. Store the results
+  ** as an array of nul-terminated strings in azResult[].  */
+  nRow = nAlloc = 0;
+  azResult = 0;
+  if( nArg>1 ){
+    sqlite3_bind_text(pStmt, 1, azArg[1], -1, SQLITE_TRANSIENT);
+  }else{
+    sqlite3_bind_text(pStmt, 1, "%", -1, SQLITE_STATIC);
+  }
+  while( sqlite3_step(pStmt)==SQLITE_ROW ){
+    if( nRow>=nAlloc ){
+      char **azNew;
+      int n2 = nAlloc*2 + 10;
+      azNew = sqlite3_realloc64(azResult, sizeof(azResult[0])*n2);
+      if( azNew==0 ) shell_out_of_memory();
+      nAlloc = n2;
+      azResult = azNew;
+    }
+    azResult[nRow] = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 0));
+    if( 0==azResult[nRow] ) shell_out_of_memory();
+    nRow++;
+  }
+  if( sqlite3_finalize(pStmt)!=SQLITE_OK ){
+    rc = shellDatabaseError(p->db);
+  }
+
+  /* Pretty-print the contents of array azResult[] to the output */
+  if( rc==0 && nRow>0 ){
+    int len, maxlen = 0;
+    int i, j;
+    int nPrintCol, nPrintRow;
+    for(i=0; i<nRow; i++){
+      len = strlen30(azResult[i]);
+      if( len>maxlen ) maxlen = len;
+    }
+    nPrintCol = 80/(maxlen+2);
+    if( nPrintCol<1 ) nPrintCol = 1;
+    nPrintRow = (nRow + nPrintCol - 1)/nPrintCol;
+    for(i=0; i<nPrintRow; i++){
+      for(j=i; j<nRow; j+=nPrintRow){
+        char *zSp = j<nPrintRow ? "" : "  ";
+        utf8_printf(p->out, "%s%-*s", zSp, maxlen,
+                    azResult[j] ? azResult[j]:"");
       }
+      raw_printf(p->out, "\n");
     }
+  }
 
-    for(ii=0; ii<nRow; ii++) sqlite3_free(azResult[ii]);
-    sqlite3_free(azResult);
-  }else
+  for(ii=0; ii<nRow; ii++) sqlite3_free(azResult[ii]);
+  sqlite3_free(azResult);
+  return 0;
+}
+DISPATCHABLE_COMMAND( tables 2 1 2 ){
+  return showTableLike(azArg, nArg, p, 't');
+}
+DISPATCHABLE_COMMAND( views 2 1 2 ){
+  return showTableLike(azArg, nArg, p, 'v');
+}
+DISPATCHABLE_COMMAND( indexes 3 1 2 ){
+  return showTableLike(azArg, nArg, p, 'i');
+}
+DISPATCHABLE_COMMAND( indices 3 1 2 ){
+  return showTableLike(azArg, nArg, p, 'i');
+}
 
-  /* Begin redirecting output to the file "testcase-out.txt" */
-  if( c=='t' && strcmp(azArg[0],"testcase")==0 ){
-    output_reset(p);
-    p->out = output_file_open("testcase-out.txt", 0);
-    if( p->out==0 ){
-      raw_printf(stderr, "Error: cannot open 'testcase-out.txt'\n");
-    }
-    if( nArg>=2 ){
-      sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "%s", azArg[1]);
-    }else{
-      sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "?");
+/*****************
+ * The .unmodule command
+ */
+CONDITION_COMMAND( unmodule defined(SQLITE_DEBUG) && !defined(SQLITE_OMIT_VIRTUALTABLE) );
+COLLECT_HELP_TEXT[
+  ".unmodule NAME ...       Unregister virtual table modules",
+  "    --allexcept             Unregister everything except those named",
+];
+DISPATCHABLE_COMMAND( unmodule ? 2 0 ){
+  int ii;
+  int lenOpt;
+  char *zOpt;
+  open_db(p, 0);
+  zOpt = azArg[1];
+  if( zOpt[0]=='-' && zOpt[1]=='-' && zOpt[2]!=0 ) zOpt++;
+  lenOpt = (int)strlen(zOpt);
+  if( lenOpt>=3 && strncmp(zOpt, "-allexcept",lenOpt)==0 ){
+    assert( azArg[nArg]==0 );
+    sqlite3_drop_modules(p->db, nArg>2 ? (const char**)(azArg+2) : 0);
+  }else{
+    for(ii=1; ii<nArg; ii++){
+      sqlite3_create_module(p->db, azArg[ii], 0, 0);
     }
-  }else
+  }
+  return 0;
+}
 
-#ifndef SQLITE_UNTESTABLE
-  if( c=='t' && n>=8 && strncmp(azArg[0], "testctrl", n)==0 ){
-    static const struct {
-       const char *zCtrlName;   /* Name of a test-control option */
-       int ctrlCode;            /* Integer code for that option */
-       const char *zUsage;      /* Usage notes */
-    } aCtrl[] = {
-      { "always",             SQLITE_TESTCTRL_ALWAYS,        "BOOLEAN"        },
-      { "assert",             SQLITE_TESTCTRL_ASSERT,        "BOOLEAN"        },
+/*****************
+ * The .testcase, .testctrl, .timeout, .timer and .trace commands
+ */
+CONDITION_COMMAND( testctrl !defined(SQLITE_UNTESTABLE) );
+CONDITION_COMMAND( trace !defined(SQLITE_OMIT_TRACE) );
+COLLECT_HELP_TEXT[
+  ".testcase NAME           Begin redirecting output to 'testcase-out.txt'",
+  ".testctrl CMD ...        Run various sqlite3_test_control() operations",
+  "                           Run \".testctrl\" with no arguments for details",
+  ".timeout MS              Try opening locked tables for MS milliseconds",
+  ".timer on|off            Turn SQL timer on or off",
+  ".trace ?OPTIONS?         Output each SQL statement as it is run",
+  "    FILE                    Send output to FILE",
+  "    stdout                  Send output to stdout",
+  "    stderr                  Send output to stderr",
+  "    off                     Disable tracing",
+  "    --expanded              Expand query parameters",
+#ifdef SQLITE_ENABLE_NORMALIZE
+  "    --normalized            Normal the SQL statements",
+#endif
+  "    --plain                 Show SQL as it is input",
+  "    --stmt                  Trace statement execution (SQLITE_TRACE_STMT)",
+  "    --profile               Profile statements (SQLITE_TRACE_PROFILE)",
+  "    --row                   Trace each row (SQLITE_TRACE_ROW)",
+  "    --close                 Trace connection close (SQLITE_TRACE_CLOSE)",
+];
+
+/* Begin redirecting output to the file "testcase-out.txt" */
+DISPATCHABLE_COMMAND( testcase ? 0 0 ){
+  output_reset(p);
+  p->out = output_file_open("testcase-out.txt", 0);
+  if( p->out==0 ){
+    raw_printf(stderr, "Error: cannot open 'testcase-out.txt'\n");
+  }
+  if( nArg>=2 ){
+    sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "%s", azArg[1]);
+  }else{
+    sqlite3_snprintf(sizeof(p->zTestcase), p->zTestcase, "?");
+  }
+}
+DISPATCHABLE_COMMAND( testctrl ? 0 0 ){
+  static const struct {
+    const char *zCtrlName;   /* Name of a test-control option */
+    int ctrlCode;            /* Integer code for that option */
+    const char *zUsage;      /* Usage notes */
+  } aCtrl[] = {
+    { "always",             SQLITE_TESTCTRL_ALWAYS,        "BOOLEAN"        },
+    { "assert",             SQLITE_TESTCTRL_ASSERT,        "BOOLEAN"        },
     /*{ "benign_malloc_hooks",SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS, ""       },*/
     /*{ "bitvec_test",        SQLITE_TESTCTRL_BITVEC_TEST,   ""             },*/
-      { "byteorder",          SQLITE_TESTCTRL_BYTEORDER,     ""               },
-      { "extra_schema_checks",SQLITE_TESTCTRL_EXTRA_SCHEMA_CHECKS,"BOOLEAN"   },
+    { "byteorder",          SQLITE_TESTCTRL_BYTEORDER,     ""               },
+    { "extra_schema_checks",SQLITE_TESTCTRL_EXTRA_SCHEMA_CHECKS,"BOOLEAN"   },
     /*{ "fault_install",      SQLITE_TESTCTRL_FAULT_INSTALL, ""             },*/
-      { "imposter",         SQLITE_TESTCTRL_IMPOSTER, "SCHEMA ON/OFF ROOTPAGE"},
-      { "internal_functions", SQLITE_TESTCTRL_INTERNAL_FUNCTIONS, "" },
-      { "localtime_fault",    SQLITE_TESTCTRL_LOCALTIME_FAULT,"BOOLEAN"       },
-      { "never_corrupt",      SQLITE_TESTCTRL_NEVER_CORRUPT, "BOOLEAN"        },
-      { "optimizations",      SQLITE_TESTCTRL_OPTIMIZATIONS, "DISABLE-MASK"   },
+    { "imposter",         SQLITE_TESTCTRL_IMPOSTER, "SCHEMA ON/OFF ROOTPAGE"},
+    { "internal_functions", SQLITE_TESTCTRL_INTERNAL_FUNCTIONS, "" },
+    { "localtime_fault",    SQLITE_TESTCTRL_LOCALTIME_FAULT,"BOOLEAN"       },
+    { "never_corrupt",      SQLITE_TESTCTRL_NEVER_CORRUPT, "BOOLEAN"        },
+    { "optimizations",      SQLITE_TESTCTRL_OPTIMIZATIONS, "DISABLE-MASK"   },
 #ifdef YYCOVERAGE
-      { "parser_coverage",    SQLITE_TESTCTRL_PARSER_COVERAGE, ""             },
-#endif
-      { "pending_byte",       SQLITE_TESTCTRL_PENDING_BYTE,  "OFFSET  "       },
-      { "prng_restore",       SQLITE_TESTCTRL_PRNG_RESTORE,  ""               },
-      { "prng_save",          SQLITE_TESTCTRL_PRNG_SAVE,     ""               },
-      { "prng_seed",          SQLITE_TESTCTRL_PRNG_SEED,     "SEED ?db?"      },
-      { "seek_count",         SQLITE_TESTCTRL_SEEK_COUNT,    ""               },
-      { "tune",               SQLITE_TESTCTRL_TUNE,          "ID VALUE"       },
-    };
-    int testctrl = -1;
-    int iCtrl = -1;
-    int rc2 = 0;    /* 0: usage.  1: %d  2: %x  3: no-output */
-    int isOk = 0;
-    int i, n2;
-    const char *zCmd = 0;
+    { "parser_coverage",    SQLITE_TESTCTRL_PARSER_COVERAGE, ""             },
+#endif
+    { "pending_byte",       SQLITE_TESTCTRL_PENDING_BYTE,  "OFFSET  "       },
+    { "prng_restore",       SQLITE_TESTCTRL_PRNG_RESTORE,  ""               },
+    { "prng_save",          SQLITE_TESTCTRL_PRNG_SAVE,     ""               },
+    { "prng_seed",          SQLITE_TESTCTRL_PRNG_SEED,     "SEED ?db?"      },
+    { "seek_count",         SQLITE_TESTCTRL_SEEK_COUNT,    ""               },
+    { "tune",               SQLITE_TESTCTRL_TUNE,          "ID VALUE"       },
+  };
+  int testctrl = -1;
+  int iCtrl = -1;
+  int rc2 = 0;    /* 0: usage.  1: %d  2: %x  3: no-output */
+  int isOk = 0;
+  int i, n2;
+  const char *zCmd = 0;
 
-    open_db(p, 0);
-    zCmd = nArg>=2 ? azArg[1] : "help";
+  open_db(p, 0);
+  zCmd = nArg>=2 ? azArg[1] : "help";
+
+  /* The argument can optionally begin with "-" or "--" */
+  if( zCmd[0]=='-' && zCmd[1] ){
+    zCmd++;
+    if( zCmd[0]=='-' && zCmd[1] ) zCmd++;
+  }
 
-    /* The argument can optionally begin with "-" or "--" */
-    if( zCmd[0]=='-' && zCmd[1] ){
-      zCmd++;
-      if( zCmd[0]=='-' && zCmd[1] ) zCmd++;
+  /* --help lists all test-controls */
+  if( strcmp(zCmd,"help")==0 ){
+    utf8_printf(p->out, "Available test-controls:\n");
+    for(i=0; i<ArraySize(aCtrl); i++){
+      utf8_printf(p->out, "  .testctrl %s %s\n",
+                  aCtrl[i].zCtrlName, aCtrl[i].zUsage);
     }
+    return 1;
+  }
 
-    /* --help lists all test-controls */
-    if( strcmp(zCmd,"help")==0 ){
-      utf8_printf(p->out, "Available test-controls:\n");
-      for(i=0; i<ArraySize(aCtrl); i++){
-        utf8_printf(p->out, "  .testctrl %s %s\n",
-                    aCtrl[i].zCtrlName, aCtrl[i].zUsage);
+  /* convert testctrl text option to value. allow any unique prefix
+  ** of the option name, or a numerical value. */
+  n2 = strlen30(zCmd);
+  for(i=0; i<ArraySize(aCtrl); i++){
+    if( strncmp(zCmd, aCtrl[i].zCtrlName, n2)==0 ){
+      if( testctrl<0 ){
+        testctrl = aCtrl[i].ctrlCode;
+        iCtrl = i;
+      }else{
+        utf8_printf(stderr, "Error: ambiguous test-control: \"%s\"\n"
+                    "Use \".testctrl --help\" for help\n", zCmd);
+        return 1;
       }
-      rc = 1;
-      goto meta_command_exit;
     }
+  }
+  if( testctrl<0 ){
+    utf8_printf(stderr,"Error: unknown test-control: %s\n"
+                "Use \".testctrl --help\" for help\n", zCmd);
+  }else{
+    switch(testctrl){
 
-    /* convert testctrl text option to value. allow any unique prefix
-    ** of the option name, or a numerical value. */
-    n2 = strlen30(zCmd);
-    for(i=0; i<ArraySize(aCtrl); i++){
-      if( strncmp(zCmd, aCtrl[i].zCtrlName, n2)==0 ){
-        if( testctrl<0 ){
-          testctrl = aCtrl[i].ctrlCode;
-          iCtrl = i;
-        }else{
-          utf8_printf(stderr, "Error: ambiguous test-control: \"%s\"\n"
-                              "Use \".testctrl --help\" for help\n", zCmd);
-          rc = 1;
-          goto meta_command_exit;
-        }
+      /* sqlite3_test_control(int, db, int) */
+    case SQLITE_TESTCTRL_OPTIMIZATIONS:
+      if( nArg==3 ){
+        unsigned int opt = (unsigned int)strtol(azArg[2], 0, 0);
+        rc2 = sqlite3_test_control(testctrl, p->db, opt);
+        isOk = 3;
       }
-    }
-    if( testctrl<0 ){
-      utf8_printf(stderr,"Error: unknown test-control: %s\n"
-                         "Use \".testctrl --help\" for help\n", zCmd);
-    }else{
-      switch(testctrl){
-
-        /* sqlite3_test_control(int, db, int) */
-        case SQLITE_TESTCTRL_OPTIMIZATIONS:
-          if( nArg==3 ){
-            unsigned int opt = (unsigned int)strtol(azArg[2], 0, 0);
-            rc2 = sqlite3_test_control(testctrl, p->db, opt);
-            isOk = 3;
-          }
-          break;
+      break;
 
-        /* sqlite3_test_control(int) */
-        case SQLITE_TESTCTRL_PRNG_SAVE:
-        case SQLITE_TESTCTRL_PRNG_RESTORE:
-        case SQLITE_TESTCTRL_BYTEORDER:
-          if( nArg==2 ){
-            rc2 = sqlite3_test_control(testctrl);
-            isOk = testctrl==SQLITE_TESTCTRL_BYTEORDER ? 1 : 3;
-          }
-          break;
+      /* sqlite3_test_control(int) */
+    case SQLITE_TESTCTRL_PRNG_SAVE:
+    case SQLITE_TESTCTRL_PRNG_RESTORE:
+    case SQLITE_TESTCTRL_BYTEORDER:
+      if( nArg==2 ){
+        rc2 = sqlite3_test_control(testctrl);
+        isOk = testctrl==SQLITE_TESTCTRL_BYTEORDER ? 1 : 3;
+      }
+      break;
 
-        /* sqlite3_test_control(int, uint) */
-        case SQLITE_TESTCTRL_PENDING_BYTE:
-          if( nArg==3 ){
-            unsigned int opt = (unsigned int)integerValue(azArg[2]);
-            rc2 = sqlite3_test_control(testctrl, opt);
-            isOk = 3;
-          }
-          break;
+      /* sqlite3_test_control(int, uint) */
+    case SQLITE_TESTCTRL_PENDING_BYTE:
+      if( nArg==3 ){
+        unsigned int opt = (unsigned int)integerValue(azArg[2]);
+        rc2 = sqlite3_test_control(testctrl, opt);
+        isOk = 3;
+      }
+      break;
 
-        /* sqlite3_test_control(int, int, sqlite3*) */
-        case SQLITE_TESTCTRL_PRNG_SEED:
-          if( nArg==3 || nArg==4 ){
-            int ii = (int)integerValue(azArg[2]);
-            sqlite3 *db;
-            if( ii==0 && strcmp(azArg[2],"random")==0 ){
-              sqlite3_randomness(sizeof(ii),&ii);
-              printf("-- random seed: %d\n", ii);
-            }
-            if( nArg==3 ){
-              db = 0;
-            }else{
-              db = p->db;
-              /* Make sure the schema has been loaded */
-              sqlite3_table_column_metadata(db, 0, "x", 0, 0, 0, 0, 0, 0);
-            }
-            rc2 = sqlite3_test_control(testctrl, ii, db);
-            isOk = 3;
-          }
-          break;
+      /* sqlite3_test_control(int, int, sqlite3*) */
+    case SQLITE_TESTCTRL_PRNG_SEED:
+      if( nArg==3 || nArg==4 ){
+        int ii = (int)integerValue(azArg[2]);
+        sqlite3 *db;
+        if( ii==0 && strcmp(azArg[2],"random")==0 ){
+          sqlite3_randomness(sizeof(ii),&ii);
+          printf("-- random seed: %d\n", ii);
+        }
+        if( nArg==3 ){
+          db = 0;
+        }else{
+          db = p->db;
+          /* Make sure the schema has been loaded */
+          sqlite3_table_column_metadata(db, 0, "x", 0, 0, 0, 0, 0, 0);
+        }
+        rc2 = sqlite3_test_control(testctrl, ii, db);
+        isOk = 3;
+      }
+      break;
 
-        /* sqlite3_test_control(int, int) */
-        case SQLITE_TESTCTRL_ASSERT:
-        case SQLITE_TESTCTRL_ALWAYS:
-          if( nArg==3 ){
-            int opt = booleanValue(azArg[2]);
-            rc2 = sqlite3_test_control(testctrl, opt);
-            isOk = 1;
-          }
-          break;
+      /* sqlite3_test_control(int, int) */
+    case SQLITE_TESTCTRL_ASSERT:
+    case SQLITE_TESTCTRL_ALWAYS:
+      if( nArg==3 ){
+        int opt = booleanValue(azArg[2]);
+        rc2 = sqlite3_test_control(testctrl, opt);
+        isOk = 1;
+      }
+      break;
 
-        /* sqlite3_test_control(int, int) */
-        case SQLITE_TESTCTRL_LOCALTIME_FAULT:
-        case SQLITE_TESTCTRL_NEVER_CORRUPT:
-          if( nArg==3 ){
-            int opt = booleanValue(azArg[2]);
-            rc2 = sqlite3_test_control(testctrl, opt);
-            isOk = 3;
-          }
-          break;
+      /* sqlite3_test_control(int, int) */
+    case SQLITE_TESTCTRL_LOCALTIME_FAULT:
+    case SQLITE_TESTCTRL_NEVER_CORRUPT:
+      if( nArg==3 ){
+        int opt = booleanValue(azArg[2]);
+        rc2 = sqlite3_test_control(testctrl, opt);
+        isOk = 3;
+      }
+      break;
 
-        /* sqlite3_test_control(sqlite3*) */
-        case SQLITE_TESTCTRL_INTERNAL_FUNCTIONS:
-          rc2 = sqlite3_test_control(testctrl, p->db);
-          isOk = 3;
-          break;
+      /* sqlite3_test_control(sqlite3*) */
+    case SQLITE_TESTCTRL_INTERNAL_FUNCTIONS:
+      rc2 = sqlite3_test_control(testctrl, p->db);
+      isOk = 3;
+      break;
 
-        case SQLITE_TESTCTRL_IMPOSTER:
-          if( nArg==5 ){
-            rc2 = sqlite3_test_control(testctrl, p->db,
-                          azArg[2],
-                          integerValue(azArg[3]),
-                          integerValue(azArg[4]));
-            isOk = 3;
-          }
-          break;
+    case SQLITE_TESTCTRL_IMPOSTER:
+      if( nArg==5 ){
+        rc2 = sqlite3_test_control(testctrl, p->db,
+                                   azArg[2],
+                                   integerValue(azArg[3]),
+                                   integerValue(azArg[4]));
+        isOk = 3;
+      }
+      break;
 
-        case SQLITE_TESTCTRL_SEEK_COUNT: {
-          u64 x = 0;
-          rc2 = sqlite3_test_control(testctrl, p->db, &x);
-          utf8_printf(p->out, "%llu\n", x);
-          isOk = 3;
-          break;
-        }
+    case SQLITE_TESTCTRL_SEEK_COUNT: {
+      u64 x = 0;
+      rc2 = sqlite3_test_control(testctrl, p->db, &x);
+      utf8_printf(p->out, "%llu\n", x);
+      isOk = 3;
+      break;
+    }
 
 #ifdef YYCOVERAGE
-        case SQLITE_TESTCTRL_PARSER_COVERAGE: {
-          if( nArg==2 ){
-            sqlite3_test_control(testctrl, p->out);
-            isOk = 3;
-          }
-          break;
-        }
+    case SQLITE_TESTCTRL_PARSER_COVERAGE: {
+      if( nArg==2 ){
+        sqlite3_test_control(testctrl, p->out);
+        isOk = 3;
+      }
+      break;
+    }
 #endif
 #ifdef SQLITE_DEBUG
-        case SQLITE_TESTCTRL_TUNE: {
-          if( nArg==4 ){
-            int id = (int)integerValue(azArg[2]);
-            int val = (int)integerValue(azArg[3]);
-            sqlite3_test_control(testctrl, id, &val);
-            isOk = 3;
-          }else if( nArg==3 ){
-            int id = (int)integerValue(azArg[2]);
-            sqlite3_test_control(testctrl, -id, &rc2);
-            isOk = 1;
-          }else if( nArg==2 ){
-            int id = 1;
-            while(1){
-              int val = 0;
-              rc2 = sqlite3_test_control(testctrl, -id, &val);
-              if( rc2!=SQLITE_OK ) break;
-              if( id>1 ) utf8_printf(p->out, "  ");
-              utf8_printf(p->out, "%d: %d", id, val);
-              id++;
-            }
-            if( id>1 ) utf8_printf(p->out, "\n");
-            isOk = 3;
-          }
-          break;
+    case SQLITE_TESTCTRL_TUNE: {
+      if( nArg==4 ){
+        int id = (int)integerValue(azArg[2]);
+        int val = (int)integerValue(azArg[3]);
+        sqlite3_test_control(testctrl, id, &val);
+        isOk = 3;
+      }else if( nArg==3 ){
+        int id = (int)integerValue(azArg[2]);
+        sqlite3_test_control(testctrl, -id, &rc2);
+        isOk = 1;
+      }else if( nArg==2 ){
+        int id = 1;
+        while(1){
+          int val = 0;
+          rc2 = sqlite3_test_control(testctrl, -id, &val);
+          if( rc2!=SQLITE_OK ) break;
+          if( id>1 ) utf8_printf(p->out, "  ");
+          utf8_printf(p->out, "%d: %d", id, val);
+          id++;
         }
-#endif
+        if( id>1 ) utf8_printf(p->out, "\n");
+        isOk = 3;
       }
+      break;
     }
-    if( isOk==0 && iCtrl>=0 ){
-      utf8_printf(p->out, "Usage: .testctrl %s %s\n", zCmd,aCtrl[iCtrl].zUsage);
-      rc = 1;
-    }else if( isOk==1 ){
-      raw_printf(p->out, "%d\n", rc2);
-    }else if( isOk==2 ){
-      raw_printf(p->out, "0x%08x\n", rc2);
+#endif
     }
-  }else
-#endif /* !defined(SQLITE_UNTESTABLE) */
-
-  if( c=='t' && n>4 && strncmp(azArg[0], "timeout", n)==0 ){
-    open_db(p, 0);
-    sqlite3_busy_timeout(p->db, nArg>=2 ? (int)integerValue(azArg[1]) : 0);
-  }else
-
-  if( c=='t' && n>=5 && strncmp(azArg[0], "timer", n)==0 ){
-    if( nArg==2 ){
-      enableTimer = booleanValue(azArg[1]);
-      if( enableTimer && !HAS_TIMER ){
-        raw_printf(stderr, "Error: timer not available on this system.\n");
-        enableTimer = 0;
+  }
+  if( isOk==0 && iCtrl>=0 ){
+    utf8_printf(p->out, "Usage: .testctrl %s %s\n", zCmd,aCtrl[iCtrl].zUsage);
+    return 1;
+  }else if( isOk==1 ){
+    raw_printf(p->out, "%d\n", rc2);
+  }else if( isOk==2 ){
+    raw_printf(p->out, "0x%08x\n", rc2);
+  }
+  return 0;
+}
+DISPATCHABLE_COMMAND( timeout 4 2 2 ){
+  open_db(p, 0);
+  sqlite3_busy_timeout(p->db, nArg>=2 ? (int)integerValue(azArg[1]) : 0);
+  return 0;
+}
+DISPATCHABLE_COMMAND( timer ? 2 2 ){
+  enableTimer = booleanValue(azArg[1]);
+  if( enableTimer && !HAS_TIMER ){
+    raw_printf(stderr, "Error: timer not available on this system.\n");
+    enableTimer = 0;
+  }
+  return 0;
+}
+DISPATCHABLE_COMMAND( trace ? 0 0 ){
+  int mType = 0;
+  int jj;
+  open_db(p, 0);
+  for(jj=1; jj<nArg; jj++){
+    const char *z = azArg[jj];
+    if( z[0]=='-' ){
+      if( optionMatch(z, "expanded") ){
+        p->eTraceType = SHELL_TRACE_EXPANDED;
+      }
+#ifdef SQLITE_ENABLE_NORMALIZE
+      else if( optionMatch(z, "normalized") ){
+        p->eTraceType = SHELL_TRACE_NORMALIZED;
+      }
+#endif
+      else if( optionMatch(z, "plain") ){
+        p->eTraceType = SHELL_TRACE_PLAIN;
+      }
+      else if( optionMatch(z, "profile") ){
+        mType |= SQLITE_TRACE_PROFILE;
+      }
+      else if( optionMatch(z, "row") ){
+        mType |= SQLITE_TRACE_ROW;
+      }
+      else if( optionMatch(z, "stmt") ){
+        mType |= SQLITE_TRACE_STMT;
+      }
+      else if( optionMatch(z, "close") ){
+        mType |= SQLITE_TRACE_CLOSE;
+      }
+      else {
+        raw_printf(stderr, "Unknown option \"%s\" on \".trace\"\n", z);
+        return 1;
       }
     }else{
-      raw_printf(stderr, "Usage: .timer on|off\n");
-      rc = 1;
+      output_file_close(p->traceOut);
+      p->traceOut = output_file_open(azArg[1], 0);
     }
-  }else
+  }
+  if( p->traceOut==0 ){
+    sqlite3_trace_v2(p->db, 0, 0, 0);
+  }else{
+    if( mType==0 ) mType = SQLITE_TRACE_STMT;
+    sqlite3_trace_v2(p->db, mType, sql_trace_callback, p);
+  }
+  return 0;
+}
 
-#ifndef SQLITE_OMIT_TRACE
-  if( c=='t' && strncmp(azArg[0], "trace", n)==0 ){
-    int mType = 0;
-    int jj;
-    open_db(p, 0);
-    for(jj=1; jj<nArg; jj++){
-      const char *z = azArg[jj];
-      if( z[0]=='-' ){
-        if( optionMatch(z, "expanded") ){
-          p->eTraceType = SHELL_TRACE_EXPANDED;
-        }
-#ifdef SQLITE_ENABLE_NORMALIZE
-        else if( optionMatch(z, "normalized") ){
-          p->eTraceType = SHELL_TRACE_NORMALIZED;
-        }
+/*****************
+ * The .user command
+ * Because there is no help text for .user, it does its own argument validation.
+ */
+CONDITION_COMMAND( user SQLITE_USER_AUTHENTICATION );
+DISPATCHABLE_COMMAND( user ? 0 0 ){
+  int rc;
+  const char *usage
+    = "Usage: .user SUBCOMMAND ...\n"
+      "Subcommands are:\n"
+      "   login USER PASSWORD\n"
+      "   delete USER\n"
+      "   add USER PASSWORD ISADMIN\n"
+      "   edit USER PASSWORD ISADMIN\n"
+    ;
+  if( nArg<2 ){
+  teach_fail:
+    raw_printf(stderr, usage);
+    return 1;
+  }
+  open_db(p, 0);
+  if( strcmp(azArg[1],"login")==0 ){
+    if( nArg!=4 ){
+      raw_printf(stderr, usage);
+      return 1;
+    }
+    rc = sqlite3_user_authenticate(p->db, azArg[2], azArg[3],
+                                   strlen30(azArg[3]));
+    if( rc ){
+      utf8_printf(stderr, "Authentication failed for user %s\n", azArg[2]);
+      return 1;
+    }
+  }else if( strcmp(azArg[1],"add")==0 ){
+    if( nArg!=5 ){
+      goto teach_fail;
+    }
+    rc = sqlite3_user_add(p->db, azArg[2], azArg[3], strlen30(azArg[3]),
+                          booleanValue(azArg[4]));
+    if( rc ){
+      raw_printf(stderr, "User-Add failed: %d\n", rc);
+      return 1;
+    }
+  }else if( strcmp(azArg[1],"edit")==0 ){
+    if( nArg!=5 ){
+      goto teach_fail;
+    }
+    rc = sqlite3_user_change(p->db, azArg[2], azArg[3], strlen30(azArg[3]),
+                             booleanValue(azArg[4]));
+    if( rc ){
+      raw_printf(stderr, "User-Edit failed: %d\n", rc);
+      return 1;
+    }
+  }else if( strcmp(azArg[1],"delete")==0 ){
+    if( nArg!=3 ){
+      goto teach_fail;
+    }
+    rc = sqlite3_user_delete(p->db, azArg[2]);
+    if( rc ){
+      raw_printf(stderr, "User-Delete failed: %d\n", rc);
+      return 1;
+    }
+  }else{
+    goto teach_fail;
+  }
+  return 0;
+}
+
+/*****************
+ * The .vfsinfo, .vfslist, .vfsname and .version commands
+ */
+COLLECT_HELP_TEXT[
+  ".version                 Show a variety of version info",
+  ".vfsinfo ?AUX?           Information about the top-level VFS",
+  ".vfslist                 List all available VFSes",
+  ".vfsname ?AUX?           Print the name of the VFS stack",
+];
+DISPATCHABLE_COMMAND( version ? 1 1 ){
+  utf8_printf(p->out, "SQLite %s %s\n" /*extra-version-info*/,
+              sqlite3_libversion(), sqlite3_sourceid());
+#if SQLITE_HAVE_ZLIB
+  utf8_printf(p->out, "zlib version %s\n", zlibVersion());
 #endif
-        else if( optionMatch(z, "plain") ){
-          p->eTraceType = SHELL_TRACE_PLAIN;
-        }
-        else if( optionMatch(z, "profile") ){
-          mType |= SQLITE_TRACE_PROFILE;
-        }
-        else if( optionMatch(z, "row") ){
-          mType |= SQLITE_TRACE_ROW;
-        }
-        else if( optionMatch(z, "stmt") ){
-          mType |= SQLITE_TRACE_STMT;
-        }
-        else if( optionMatch(z, "close") ){
-          mType |= SQLITE_TRACE_CLOSE;
-        }
-        else {
-          raw_printf(stderr, "Unknown option \"%s\" on \".trace\"\n", z);
-          rc = 1;
-          goto meta_command_exit;
-        }
-      }else{
-        output_file_close(p->traceOut);
-        p->traceOut = output_file_open(azArg[1], 0);
-      }
+#define CTIMEOPT_VAL_(opt) #opt
+#define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt)
+#if defined(__clang__) && defined(__clang_major__)
+  utf8_printf(p->out, "clang-" CTIMEOPT_VAL(__clang_major__) "."
+              CTIMEOPT_VAL(__clang_minor__) "."
+              CTIMEOPT_VAL(__clang_patchlevel__) "\n");
+#elif defined(_MSC_VER)
+  utf8_printf(p->out, "msvc-" CTIMEOPT_VAL(_MSC_VER) "\n");
+#elif defined(__GNUC__) && defined(__VERSION__)
+  utf8_printf(p->out, "gcc-" __VERSION__ "\n");
+#endif
+  return 0;
+}
+DISPATCHABLE_COMMAND( vfsinfo ? 1 2 ){
+  const char *zDbName = nArg==2 ? azArg[1] : "main";
+  sqlite3_vfs *pVfs = 0;
+  if( p->db ){
+    sqlite3_file_control(p->db, zDbName, SQLITE_FCNTL_VFS_POINTER, &pVfs);
+    if( pVfs ){
+      utf8_printf(p->out, "vfs.zName      = \"%s\"\n", pVfs->zName);
+      raw_printf(p->out, "vfs.iVersion   = %d\n", pVfs->iVersion);
+      raw_printf(p->out, "vfs.szOsFile   = %d\n", pVfs->szOsFile);
+      raw_printf(p->out, "vfs.mxPathname = %d\n", pVfs->mxPathname);
     }
-    if( p->traceOut==0 ){
-      sqlite3_trace_v2(p->db, 0, 0, 0);
-    }else{
-      if( mType==0 ) mType = SQLITE_TRACE_STMT;
-      sqlite3_trace_v2(p->db, mType, sql_trace_callback, p);
+  }
+  return 0;
+}
+DISPATCHABLE_COMMAND( vfslist ? 1 1 ){
+  sqlite3_vfs *pVfs;
+  sqlite3_vfs *pCurrent = 0;
+  if( p->db ){
+    sqlite3_file_control(p->db, "main", SQLITE_FCNTL_VFS_POINTER, &pCurrent);
+  }
+  for(pVfs=sqlite3_vfs_find(0); pVfs; pVfs=pVfs->pNext){
+    utf8_printf(p->out, "vfs.zName      = \"%s\"%s\n", pVfs->zName,
+                pVfs==pCurrent ? "  <--- CURRENT" : "");
+    raw_printf(p->out, "vfs.iVersion   = %d\n", pVfs->iVersion);
+    raw_printf(p->out, "vfs.szOsFile   = %d\n", pVfs->szOsFile);
+    raw_printf(p->out, "vfs.mxPathname = %d\n", pVfs->mxPathname);
+    if( pVfs->pNext ){
+      raw_printf(p->out, "-----------------------------------\n");
     }
-  }else
-#endif /* !defined(SQLITE_OMIT_TRACE) */
-
-#if defined(SQLITE_DEBUG) && !defined(SQLITE_OMIT_VIRTUALTABLE)
-  if( c=='u' && strncmp(azArg[0], "unmodule", n)==0 ){
-    int ii;
-    int lenOpt;
-    char *zOpt;
-    if( nArg<2 ){
-      raw_printf(stderr, "Usage: .unmodule [--allexcept] NAME ...\n");
-      rc = 1;
-      goto meta_command_exit;
+  }
+  return 0;
+}
+DISPATCHABLE_COMMAND( vfsname ? 0 0 ){
+  const char *zDbName = nArg==2 ? azArg[1] : "main";
+  char *zVfsName = 0;
+  if( p->db ){
+    sqlite3_file_control(p->db, zDbName, SQLITE_FCNTL_VFSNAME, &zVfsName);
+    if( zVfsName ){
+      utf8_printf(p->out, "%s\n", zVfsName);
+      sqlite3_free(zVfsName);
     }
-    open_db(p, 0);
-    zOpt = azArg[1];
-    if( zOpt[0]=='-' && zOpt[1]=='-' && zOpt[2]!=0 ) zOpt++;
-    lenOpt = (int)strlen(zOpt);
-    if( lenOpt>=3 && strncmp(zOpt, "-allexcept",lenOpt)==0 ){
-      assert( azArg[nArg]==0 );
-      sqlite3_drop_modules(p->db, nArg>2 ? (const char**)(azArg+2) : 0);
+  }
+  return 0;
+}
+
+/*****************
+ * The .width and .wheretrace commands
+ * The .wheretrace command has no help.
+ */
+COLLECT_HELP_TEXT[
+  ".width NUM1 NUM2 ...     Set minimum column widths for columnar output",
+  "     Negative values right-justify",
+];
+DISPATCHABLE_COMMAND( width ? 1 0 ){
+  int j;
+  assert( nArg<=ArraySize(azArg) );
+  p->nWidth = nArg-1;
+  p->colWidth = realloc(p->colWidth, p->nWidth*sizeof(int)*2);
+  if( p->colWidth==0 && p->nWidth>0 ) shell_out_of_memory();
+  if( p->nWidth ) p->actualWidth = &p->colWidth[p->nWidth];
+  for(j=1; j<nArg; j++){
+    p->colWidth[j-1] = (int)integerValue(azArg[j]);
+  }
+  return 0;
+}
+DISPATCHABLE_COMMAND( wheretrace ? 1 2 ){
+  unsigned int x = nArg>=2 ? (unsigned int)integerValue(azArg[1]) : 0xffffffff;
+  sqlite3_test_control(SQLITE_TESTCTRL_TRACEFLAGS, 3, &x);
+  return 0;
+}
+
+/* Define and populate command dispatch table. */
+static struct DispatchEntry {
+  const char * cmdName;
+  int (*cmdDoer)(char *azArg[], int nArg, ShellState *);
+  unsigned char minLen, minArgs, maxArgs;
+} command_table[] = {
+  EMIT_DISPATCH(2);
+  { 0, 0, 0, -1, -1 }
+};
+static unsigned numCommands = sizeof(command_table)/sizeof(struct DispatchEntry) - 1;
+
+/*****************
+ * Command dispatcher
+ */
+
+int dispatchCommand(char *azArg[], int nArg, ShellState *pSS)
+{
+  const char *cmdName = azArg[0];
+  int cmdLen = strlen30(cmdName);
+  struct DispatchEntry *pde = 0;
+  int ixb = 0, ixe = numCommands-1;
+  while( ixb <= ixe ){
+    int ixm = (ixb+ixe)/2;
+    int md = strncmp(cmdName, command_table[ixm].cmdName, cmdLen);
+    if( md>0 ){
+      ixb = ixm+1;
+    }else if( md<0 ){
+      ixe = ixm-1;
     }else{
-      for(ii=1; ii<nArg; ii++){
-        sqlite3_create_module(p->db, azArg[ii], 0, 0);
+      if( command_table[ixm].minLen > cmdLen ){
+        return NO_SUCH_COMMAND;
       }
+      pde = &command_table[ixm];
+      break;
     }
-  }else
+  }
+  if( 0==pde ){
+    return NO_SUCH_COMMAND;
+  }
+  if((pde->minArgs > 0 && pde->minArgs > nArg)||(pde->maxArgs > 0 && pde->maxArgs < nArg)){
+    return INVALID_ARGS;
+  }
+  return (pde->cmdDoer)(azArg, nArg, pSS);
+}
+
+
+/*
+** If an input line begins with "." then invoke this routine to
+** process that line.
+**
+** Return 1 on error, 2 to exit, and 0 otherwise.
+*/
+static int do_meta_command(char *zLine, ShellState *p){
+  int h = 1;
+  int nArg = 0;
+  int n, c;
+  int rc = 0;
+  char *azArg[52];
+
+#ifndef SQLITE_OMIT_VIRTUALTABLE
+  if( p->expert.pExpert ){
+    expertFinish(p, 1, 0);
+  }
 #endif
 
-#if SQLITE_USER_AUTHENTICATION
-  if( c=='u' && strncmp(azArg[0], "user", n)==0 ){
-    if( nArg<2 ){
-      raw_printf(stderr, "Usage: .user SUBCOMMAND ...\n");
-      rc = 1;
-      goto meta_command_exit;
-    }
-    open_db(p, 0);
-    if( strcmp(azArg[1],"login")==0 ){
-      if( nArg!=4 ){
-        raw_printf(stderr, "Usage: .user login USER PASSWORD\n");
-        rc = 1;
-        goto meta_command_exit;
-      }
-      rc = sqlite3_user_authenticate(p->db, azArg[2], azArg[3],
-                                     strlen30(azArg[3]));
-      if( rc ){
-        utf8_printf(stderr, "Authentication failed for user %s\n", azArg[2]);
-        rc = 1;
-      }
-    }else if( strcmp(azArg[1],"add")==0 ){
-      if( nArg!=5 ){
-        raw_printf(stderr, "Usage: .user add USER PASSWORD ISADMIN\n");
-        rc = 1;
-        goto meta_command_exit;
-      }
-      rc = sqlite3_user_add(p->db, azArg[2], azArg[3], strlen30(azArg[3]),
-                            booleanValue(azArg[4]));
-      if( rc ){
-        raw_printf(stderr, "User-Add failed: %d\n", rc);
-        rc = 1;
-      }
-    }else if( strcmp(azArg[1],"edit")==0 ){
-      if( nArg!=5 ){
-        raw_printf(stderr, "Usage: .user edit USER PASSWORD ISADMIN\n");
-        rc = 1;
-        goto meta_command_exit;
-      }
-      rc = sqlite3_user_change(p->db, azArg[2], azArg[3], strlen30(azArg[3]),
-                              booleanValue(azArg[4]));
-      if( rc ){
-        raw_printf(stderr, "User-Edit failed: %d\n", rc);
-        rc = 1;
-      }
-    }else if( strcmp(azArg[1],"delete")==0 ){
-      if( nArg!=3 ){
-        raw_printf(stderr, "Usage: .user delete USER\n");
-        rc = 1;
-        goto meta_command_exit;
+  /* Parse the input line into tokens.
+  */
+  while( zLine[h] && nArg<ArraySize(azArg)-1 ){
+    while( IsSpace(zLine[h]) ){ h++; }
+    if( zLine[h]==0 ) break;
+    if( zLine[h]=='\'' || zLine[h]=='"' ){
+      int delim = zLine[h++];
+      azArg[nArg++] = &zLine[h];
+      while( zLine[h] && zLine[h]!=delim ){
+        if( zLine[h]=='\\' && delim=='"' && zLine[h+1]!=0 ) h++;
+        h++;
       }
-      rc = sqlite3_user_delete(p->db, azArg[2]);
-      if( rc ){
-        raw_printf(stderr, "User-Delete failed: %d\n", rc);
-        rc = 1;
+      if( zLine[h]==delim ){
+        zLine[h++] = 0;
       }
+      if( delim=='"' ) resolve_backslashes(azArg[nArg-1]);
     }else{
-      raw_printf(stderr, "Usage: .user login|add|edit|delete ...\n");
-      rc = 1;
-      goto meta_command_exit;
+      azArg[nArg++] = &zLine[h];
+      while( zLine[h] && !IsSpace(zLine[h]) ){ h++; }
+      if( zLine[h] ) zLine[h++] = 0;
+      resolve_backslashes(azArg[nArg-1]);
     }
-  }else
-#endif /* SQLITE_USER_AUTHENTICATION */
+  }
+  azArg[nArg] = 0;
+
+  /* Process the input line.
+  */
+  if( nArg==0 ) return 0; /* no tokens, no error */
+  n = strlen30(azArg[0]);
+  c = azArg[0][0];
+  clearTempFile(p);
+
+  /* Check for the special, non-dispatched meta-commands.
+  */
 
-  if( c=='v' && strncmp(azArg[0], "version", n)==0 ){
-    utf8_printf(p->out, "SQLite %s %s\n" /*extra-version-info*/,
-        sqlite3_libversion(), sqlite3_sourceid());
-#if SQLITE_HAVE_ZLIB
-    utf8_printf(p->out, "zlib version %s\n", zlibVersion());
-#endif
-#define CTIMEOPT_VAL_(opt) #opt
-#define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt)
-#if defined(__clang__) && defined(__clang_major__)
-    utf8_printf(p->out, "clang-" CTIMEOPT_VAL(__clang_major__) "."
-                    CTIMEOPT_VAL(__clang_minor__) "."
-                    CTIMEOPT_VAL(__clang_patchlevel__) "\n");
-#elif defined(_MSC_VER)
-    utf8_printf(p->out, "msvc-" CTIMEOPT_VAL(_MSC_VER) "\n");
-#elif defined(__GNUC__) && defined(__VERSION__)
-    utf8_printf(p->out, "gcc-" __VERSION__ "\n");
-#endif
+  if( c=='e' && strncmp(azArg[0], "exit", n)==0 ){
+    if( nArg>1 && (rc = (int)integerValue(azArg[1]))!=0 ) exit(rc);
+    rc = 2;
   }else
 
-  if( c=='v' && strncmp(azArg[0], "vfsinfo", n)==0 ){
-    const char *zDbName = nArg==2 ? azArg[1] : "main";
-    sqlite3_vfs *pVfs = 0;
-    if( p->db ){
-      sqlite3_file_control(p->db, zDbName, SQLITE_FCNTL_VFS_POINTER, &pVfs);
-      if( pVfs ){
-        utf8_printf(p->out, "vfs.zName      = \"%s\"\n", pVfs->zName);
-        raw_printf(p->out, "vfs.iVersion   = %d\n", pVfs->iVersion);
-        raw_printf(p->out, "vfs.szOsFile   = %d\n", pVfs->szOsFile);
-        raw_printf(p->out, "vfs.mxPathname = %d\n", pVfs->mxPathname);
-      }
-    }
+  if( c=='q' && strncmp(azArg[0], "quit", n)==0 ){
+    rc = 2;
   }else
 
-  if( c=='v' && strncmp(azArg[0], "vfslist", n)==0 ){
-    sqlite3_vfs *pVfs;
-    sqlite3_vfs *pCurrent = 0;
-    if( p->db ){
-      sqlite3_file_control(p->db, "main", SQLITE_FCNTL_VFS_POINTER, &pCurrent);
-    }
-    for(pVfs=sqlite3_vfs_find(0); pVfs; pVfs=pVfs->pNext){
-      utf8_printf(p->out, "vfs.zName      = \"%s\"%s\n", pVfs->zName,
-           pVfs==pCurrent ? "  <--- CURRENT" : "");
-      raw_printf(p->out, "vfs.iVersion   = %d\n", pVfs->iVersion);
-      raw_printf(p->out, "vfs.szOsFile   = %d\n", pVfs->szOsFile);
-      raw_printf(p->out, "vfs.mxPathname = %d\n", pVfs->mxPathname);
-      if( pVfs->pNext ){
-        raw_printf(p->out, "-----------------------------------\n");
+#ifdef SQLITE_DEBUG
+  /* Undocumented commands for internal testing.  
+   * Subject to change without notice.
+   * These are not dispatch via lookup because the command word varies.
+   */
+  if( c=='s' && n>=10 && strncmp(azArg[0], "selftest-", 9)==0 ){
+    if( strncmp(azArg[0]+9, "boolean", n-9)==0 ){
+      int i, v;
+      for(i=1; i<nArg; i++){
+        v = booleanValue(azArg[i]);
+        utf8_printf(p->out, "%s: %d 0x%x\n", azArg[i], v, v);
       }
     }
-  }else
-
-  if( c=='v' && strncmp(azArg[0], "vfsname", n)==0 ){
-    const char *zDbName = nArg==2 ? azArg[1] : "main";
-    char *zVfsName = 0;
-    if( p->db ){
-      sqlite3_file_control(p->db, zDbName, SQLITE_FCNTL_VFSNAME, &zVfsName);
-      if( zVfsName ){
-        utf8_printf(p->out, "%s\n", zVfsName);
-        sqlite3_free(zVfsName);
+    if( strncmp(azArg[0]+9, "integer", n-9)==0 ){
+      int i; sqlite3_int64 v;
+      for(i=1; i<nArg; i++){
+        char zBuf[200];
+        v = integerValue(azArg[i]);
+        sqlite3_snprintf(sizeof(zBuf),zBuf,"%s: %lld 0x%llx\n", azArg[i],v,v);
+        utf8_printf(p->out, "%s", zBuf);
       }
     }
   }else
+#endif
 
   if( c=='w' && strncmp(azArg[0], "wheretrace", n)==0 ){
-    unsigned int x = nArg>=2 ? (unsigned int)integerValue(azArg[1]) : 0xffffffff;
-    sqlite3_test_control(SQLITE_TESTCTRL_TRACEFLAGS, 3, &x);
-  }else
-
-  if( c=='w' && strncmp(azArg[0], "width", n)==0 ){
-    int j;
-    assert( nArg<=ArraySize(azArg) );
-    p->nWidth = nArg-1;
-    p->colWidth = realloc(p->colWidth, p->nWidth*sizeof(int)*2);
-    if( p->colWidth==0 && p->nWidth>0 ) shell_out_of_memory();
-    if( p->nWidth ) p->actualWidth = &p->colWidth[p->nWidth];
-    for(j=1; j<nArg; j++){
-      p->colWidth[j-1] = (int)integerValue(azArg[j]);
-    }
   }else
     /* The meta-command is not among the specially handled ones. Dispatch it. */
   {
@@ -10678,234 +10893,8 @@ static void process_sqliterc(
 }
 
 COLLECT_HELP_TEXT[
-  ".excel                   Display the output of next command in spreadsheet",
-  "   --bom                   Put a UTF8 byte-order mark on intermediate file",
   ".exit ?CODE?             Exit this program with return-code CODE",
-  ".expert                  EXPERIMENTAL. Suggest indexes for queries",
-  ".explain ?on|off|auto?   Change the EXPLAIN formatting mode.  Default: auto",
-  ".filectrl CMD ...        Run various sqlite3_file_control() operations",
-  "   --schema SCHEMA         Use SCHEMA instead of \"main\"",
-  "   --help                  Show CMD details",
-  ".fullschema ?--indent?   Show schema and the content of sqlite_stat tables",
-  ".headers on|off          Turn display of headers on or off",
-  ".import FILE TABLE       Import data from FILE into TABLE",
-  "   Options:",
-  "     --ascii               Use \\037 and \\036 as column and row separators",
-  "     --csv                 Use , and \\n as column and row separators",
-  "     --skip N              Skip the first N rows of input",
-  "     -v                    \"Verbose\" - increase auxiliary output",
-  "   Notes:",
-  "     *  If TABLE does not exist, it is created.  The first row of input",
-  "        determines the column names.",
-  "     *  If neither --csv or --ascii are used, the input mode is derived",
-  "        from the \".mode\" output mode",
-  "     *  If FILE begins with \"|\" then it is a command that generates the",
-  "        input text.",
-];
-COLLECT_HELP_TEXT[
-#ifndef SQLITE_OMIT_TEST_CONTROL
-  ".imposter INDEX TABLE    Create imposter table TABLE on index INDEX",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".indexes ?TABLE?         Show names of indexes",
-  "                           If TABLE is specified, only show indexes for",
-  "                           tables matching TABLE using the LIKE operator.",
-];
-COLLECT_HELP_TEXT[
-#ifdef SQLITE_ENABLE_IOTRACE
-  ".iotrace FILE            Enable I/O diagnostic logging to FILE",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".limit ?LIMIT? ?VAL?     Display or change the value of an SQLITE_LIMIT",
-  ".lint OPTIONS            Report potential schema issues.",
-  "     Options:",
-  "        fkey-indexes     Find missing foreign key indexes",
-];
-COLLECT_HELP_TEXT[
-#ifndef SQLITE_OMIT_LOAD_EXTENSION
-  ".load FILE ?ENTRY?       Load an extension library",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".log FILE|off            Turn logging on or off.  FILE can be stderr/stdout",
-  ".mode MODE ?TABLE?       Set output mode",
-  "   MODE is one of:",
-  "     ascii     Columns/rows delimited by 0x1F and 0x1E",
-  "     box       Tables using unicode box-drawing characters",
-  "     csv       Comma-separated values",
-  "     column    Output in columns.  (See .width)",
-  "     html      HTML <table> code",
-  "     insert    SQL insert statements for TABLE",
-  "     json      Results in a JSON array",
-  "     line      One value per line",
-  "     list      Values delimited by \"|\"",
-  "     markdown  Markdown table format",
-  "     quote     Escape answers as for SQL",
-  "     table     ASCII-art table",
-  "     tabs      Tab-separated values",
-  "     tcl       TCL list elements",
-  ".nullvalue STRING        Use STRING in place of NULL values",
-  ".once ?OPTIONS? ?FILE?   Output for the next SQL command only to FILE",
-  "     If FILE begins with '|' then open as a pipe",
-  "       --bom  Put a UTF8 byte-order mark at the beginning",
-  "       -e     Send output to the system text editor",
-  "       -x     Send output as CSV to a spreadsheet (same as \".excel\")",
-];
-COLLECT_HELP_TEXT[
-#ifdef SQLITE_DEBUG
-  ".oom ?--repeat M? ?N?    Simulate an OOM error on the N-th allocation",
-#endif 
-];
-COLLECT_HELP_TEXT[
-  ".open ?OPTIONS? ?FILE?   Close existing database and reopen FILE",
-  "     Options:",
-  "        --append        Use appendvfs to append database to the end of FILE",
-#ifndef SQLITE_OMIT_DESERIALIZE
-  "        --deserialize   Load into memory using sqlite3_deserialize()",
-  "        --hexdb         Load the output of \"dbtotxt\" as an in-memory db",
-  "        --maxsize N     Maximum size for --hexdb or --deserialized database",
-#endif
-  "        --new           Initialize FILE to an empty database",
-  "        --nofollow      Do not follow symbolic links",
-  "        --readonly      Open FILE readonly",
-  "        --zip           FILE is a ZIP archive",
-  ".output ?FILE?           Send output to FILE or stdout if FILE is omitted",
-  "   If FILE begins with '|' then open it as a pipe.",
-  "   Options:",
-  "     --bom                 Prefix output with a UTF8 byte-order mark",
-  "     -e                    Send output to the system text editor",
-  "     -x                    Send output as CSV to a spreadsheet",
-  ".parameter CMD ...       Manage SQL parameter bindings",
-  "   clear                   Erase all bindings",
-  "   init                    Initialize the TEMP table that holds bindings",
-  "   list                    List the current parameter bindings",
-  "   set PARAMETER VALUE     Given SQL parameter PARAMETER a value of VALUE",
-  "                           PARAMETER should start with one of: $ : @ ?",
-  "   unset PARAMETER         Remove PARAMETER from the binding table",
-  ".print STRING...         Print literal STRING",
-];
-COLLECT_HELP_TEXT[
-#ifndef SQLITE_OMIT_PROGRESS_CALLBACK
-  ".progress N              Invoke progress handler after every N opcodes",
-  "   --limit N                 Interrupt after N progress callbacks",
-  "   --once                    Do no more than one progress interrupt",
-  "   --quiet|-q                No output except at interrupts",
-  "   --reset                   Reset the count for each input and interrupt",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".prompt MAIN CONTINUE    Replace the standard prompts",
   ".quit                    Exit this program",
-  ".read FILE               Read input from FILE",
-];
-COLLECT_HELP_TEXT[
-#if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB)
-  ".recover                 Recover as much data as possible from corrupt db.",
-  "   --freelist-corrupt       Assume the freelist is corrupt",
-  "   --recovery-db NAME       Store recovery metadata in database file NAME",
-  "   --lost-and-found TABLE   Alternative name for the lost-and-found table",
-  "   --no-rowids              Do not attempt to recover rowid values",
-  "                            that are not also INTEGER PRIMARY KEYs",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".restore ?DB? FILE       Restore content of DB (default \"main\") from FILE",
-  ".scanstats on|off        Turn sqlite3_stmt_scanstatus() metrics on or off",
-  ".schema ?PATTERN?        Show the CREATE statements matching PATTERN",
-  "   Options:",
-  "      --indent             Try to pretty-print the schema",
-  "      --nosys              Omit objects whose names start with \"sqlite_\"",
-  ".selftest ?OPTIONS?      Run tests defined in the SELFTEST table",
-  "    Options:",
-  "       --init               Create a new SELFTEST table",
-  "       -v                   Verbose output",
-  ".separator COL ?ROW?     Change the column and row separators",
-];
-COLLECT_HELP_TEXT[
-#if defined(SQLITE_ENABLE_SESSION)
-  ".session ?NAME? CMD ...  Create or control sessions",
-  "   Subcommands:",
-  "     attach TABLE             Attach TABLE",
-  "     changeset FILE           Write a changeset into FILE",
-  "     close                    Close one session",
-  "     enable ?BOOLEAN?         Set or query the enable bit",
-  "     filter GLOB...           Reject tables matching GLOBs",
-  "     indirect ?BOOLEAN?       Mark or query the indirect status",
-  "     isempty                  Query whether the session is empty",
-  "     list                     List currently open session names",
-  "     open DB NAME             Open a new session on DB",
-  "     patchset FILE            Write a patchset into FILE",
-  "   If ?NAME? is omitted, the first defined session is used.",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".sha3sum ...             Compute a SHA3 hash of database content",
-  "    Options:",
-  "      --schema              Also hash the sqlite_schema table",
-  "      --sha3-224            Use the sha3-224 algorithm",
-  "      --sha3-256            Use the sha3-256 algorithm (default)",
-  "      --sha3-384            Use the sha3-384 algorithm",
-  "      --sha3-512            Use the sha3-512 algorithm",
-  "    Any other argument is a LIKE pattern for tables to hash",
-];
-COLLECT_HELP_TEXT[
-#ifndef SQLITE_NOHAVE_SYSTEM
-  ".shell CMD ARGS...       Run CMD ARGS... in a system shell",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".show                    Show the current values for various settings",
-  ".stats ?ARG?             Show stats or turn stats on or off",
-  "   off                      Turn off automatic stat display",
-  "   on                       Turn on automatic stat display",
-  "   stmt                     Show statement stats",
-  "   vmstep                   Show the virtual machine step count only",
-];
-COLLECT_HELP_TEXT[
-#ifndef SQLITE_NOHAVE_SYSTEM
-  ".system CMD ARGS...      Run CMD ARGS... in a system shell",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".tables ?TABLE?          List names of tables matching LIKE pattern TABLE",
-  ".testcase NAME           Begin redirecting output to 'testcase-out.txt'",
-  ".testctrl CMD ...        Run various sqlite3_test_control() operations",
-  "                           Run \".testctrl\" with no arguments for details",
-  ".timeout MS              Try opening locked tables for MS milliseconds",
-  ".timer on|off            Turn SQL timer on or off",
-];
-COLLECT_HELP_TEXT[
-#ifndef SQLITE_OMIT_TRACE
-  ".trace ?OPTIONS?         Output each SQL statement as it is run",
-  "    FILE                    Send output to FILE",
-  "    stdout                  Send output to stdout",
-  "    stderr                  Send output to stderr",
-  "    off                     Disable tracing",
-  "    --expanded              Expand query parameters",
-#ifdef SQLITE_ENABLE_NORMALIZE
-  "    --normalized            Normal the SQL statements",
-#endif
-  "    --plain                 Show SQL as it is input",
-  "    --stmt                  Trace statement execution (SQLITE_TRACE_STMT)",
-  "    --profile               Profile statements (SQLITE_TRACE_PROFILE)",
-  "    --row                   Trace each row (SQLITE_TRACE_ROW)",
-  "    --close                 Trace connection close (SQLITE_TRACE_CLOSE)",
-#endif /* SQLITE_OMIT_TRACE */
-];
-COLLECT_HELP_TEXT[
-#ifdef SQLITE_DEBUG
-  ".unmodule NAME ...       Unregister virtual table modules",
-  "    --allexcept             Unregister everything except those named",
-#endif
-];
-COLLECT_HELP_TEXT[
-  ".vfsinfo ?AUX?           Information about the top-level VFS",
-  ".vfslist                 List all available VFSes",
-  ".vfsname ?AUX?           Print the name of the VFS stack",
-  ".width NUM1 NUM2 ...     Set minimum column widths for columnar output",
-  "     Negative values right-justify",
 ];
 
 /*
index a98232dcd72e967853399fbdda3ff73665048562..0ee3dc49f7a3807cdc5c866ceae5536d8e9f0995 100644 (file)
@@ -22,7 +22,7 @@ set headComment {/* DO NOT EDIT!
 ** Most of the code found below comes from the "src/shell.c.in" file in
 ** the canonical SQLite source tree.  That main file contains "INCLUDE"
 ** lines that specify other files in the canonical source tree that are
-** inserted to getnerate this complete program source file.
+** inserted and transformed to generate this complete program source file.
 **
 ** The code from multiple files is combined into this single "shell.c"
 ** source file to help make the command-line program easier to compile.
@@ -68,6 +68,7 @@ set ::cmd_help [dict create]
 set ::cmd_dispatch [dict create]
 set ::cmd_condition [dict create]
 set ::iShuffleErrors 0
+regexp {(\{)(\})} "{}" ma ::lb ::rb ; # Ease use of { and }.
 
 # Setup dispatching function signature and table entry struct .
 set ::dispCfg [dict create \
@@ -75,9 +76,9 @@ set ::dispCfg [dict create \
   STORAGE_CLASS static \
   ARGS_SIGNATURE "char *\$arg4\\\[\\\], int \$arg5, ShellState *\$arg6" \
   DISPATCH_ENTRY \
-   "\x7B \"\$cmd\", \$\x7Bcmd\x7DCommand, \$arg1,\$arg2,\$arg3 \x7D," \
+   "{ \"\$cmd\", \${cmd}Command, \$arg1,\$arg2,\$arg3 }," \
   DISPATCHEE_NAME {${cmd}Command} \
-  CMD_CAPTURE_RE "^\\s*\x7B\\s*\"(\\w+)\"" \
+  CMD_CAPTURE_RE "^\\s*$::lb\\s*\"(\\w+)\"" \
 ]
 # Other config keys:
 #  DC_ARG_COUNT=<number of arguments to DISPATCHABLE_COMMAND()>
@@ -184,7 +185,7 @@ array set ::macroUsages [list \
   COLLECT_HELP_TEXT "\[\n   <help text lines>\n  \];" \
   CONDITION_COMMAND "( name pp_expr );" \
   DISPATCH_CONFIG "\[\n   <NAME=value lines>\n  \];" \
-  DISPATCHABLE_COMMAND "( name args... )\x7B\n   <code lines>\n  \x7D" \
+  DISPATCHABLE_COMMAND "( name args... ){\n   <code lines>\n  }" \
   EMIT_DISPATCH "( indent );" \
   EMIT_HELP_TEXT "( indent );" \
 ]
@@ -262,7 +263,7 @@ proc DISPATCHABLE_COMMAND {hFile tailCapture ostrm} {
   # Generate and emit a function definition, maybe wrapped as set by
   # CONDITION_COMMAND(), and generate/collect its dispatch table entry.
   lassign $tailCapture args tc
-  if {$tc ne "\x7B"} {
+  if {$tc ne $::lb} {
     yap_usage "DISPATCHABLE_COMMAND($args)$tc" DISPATCHABLE_COMMAND
     incr $::iShuffleErrors
     return 0
@@ -288,10 +289,10 @@ proc DISPATCHABLE_COMMAND {hFile tailCapture ostrm} {
     }
     set body {}
     while {![eof $hFile]} {
-      set lb [gets $hFile]
+      set bl [gets $hFile]
       incr iAte
-      lappend body $lb
-      if {[regexp "^\x7D\\s*\$" $lb]} { break }
+      lappend body $bl
+      if {[regexp "^$::rb\\s*\$" $bl]} { break }
     }
     for {set aix 1} {$aix < $na} {incr aix} {
       set av [lindex $args $aix]
@@ -306,7 +307,7 @@ proc DISPATCHABLE_COMMAND {hFile tailCapture ostrm} {
       set rsct "$rsct [dict get $::dispCfg RETURN_TYPE]"
       set argexp [subst [dict get $::dispCfg ARGS_SIGNATURE]]
       set fname [subst [dict get $::dispCfg DISPATCHEE_NAME]]
-      set funcOpen "$rsct $fname\($argexp\)\x7B"
+      set funcOpen "$rsct $fname\($argexp\)$::lb"
       set dispEntry [subst [dict get $::dispCfg DISPATCH_ENTRY]]
       emit_conditionally $cmd [linsert $body 0 $funcOpen] $ostrm
       dict set ::cmd_dispatch $cmd [list $dispEntry]