]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Sync w/3.38, add .parameter ls to CLI
authorlarrybr <larrybr@noemail.net>
Tue, 22 Feb 2022 22:28:37 +0000 (22:28 +0000)
committerlarrybr <larrybr@noemail.net>
Tue, 22 Feb 2022 22:28:37 +0000 (22:28 +0000)
FossilOrigin-Name: 8c9a5fb26ba045edef1269c5f5e8c8d87fa890b88ddb1121be72514a389a845d

1  2 
manifest
manifest.uuid
src/shell.c.in
test/shell9.test

diff --cc manifest
index 9aeb5cef0644b291457b9b5244bf5381bbebda9f,5ca957c2441cb1ae3163b3b02e64b0e0bcf2f9b4..e5f4607332841c8ee02efb5946afaa23d67bff43
+++ b/manifest
@@@ -1,5 -1,5 +1,5 @@@
- C Sync\swith\strunk
- D 2022-02-15T17:04:37.699
 -C Version\s3.38.0
 -D 2022-02-22T18:58:40.488
++C Sync\sw/3.38,\sadd\s.parameter\sls\sto\sCLI
++D 2022-02-22T22:28:37.952
  F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
  F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
  F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@@ -553,8 -553,8 +553,8 @@@ F src/random.c 097dc8b31b8fba5a9aca1697
  F src/resolve.c ea935b87d6fb36c78b70cdc7b28561dc8f33f2ef37048389549c7b5ef9b0ba5e
  F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92
  F src/select.c 3baa9dd8cf240654773c7974e2bcce398ac9dd24419c36684156963defe43b35
- F src/shell.c.in c87d57f80663efb5aff3935bda7e6264d25636f41108a58edca9b628d9bc3b02
- F src/sqlite.h.in 7047c4b60fa550264d6363bb1d983540e7828fb19d2d1e5aa43b52ca13144807
 -F src/shell.c.in 14cdfba32c73cb06169e50cd448632c28359f2bab2a0f803dc4a7f46dfc5b6fa
++F src/shell.c.in 5a8a5350b7284f185aed21c140e6c81a33b54873f89a3568600cf6a37dd4acac
+ F src/sqlite.h.in e30cedf008d9c51511f4027a3739b727a588da553424748b48d2393f85dbde41
  F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8
  F src/sqlite3ext.h a95cb9ed106e3d39e2118e4dcc15a14faec3fa50d0093425083d340d9dfd96e6
  F src/sqliteInt.h f8814239fb1f95056555e2d7fa475750e64681cac4221fb03610d1fde0b79d53
@@@ -1390,13 -1390,12 +1390,13 @@@ F test/shared_err.test 32634e404a3317ee
  F test/sharedlock.test 5ede3c37439067c43b0198f580fd374ebf15d304
  F test/shell1.test b224e0793c5f48aa3749e65d8c64b93a30731bd206f2e41e6c5f1bee1bdb16c6
  F test/shell2.test 89e4b2db062d52baed75022227b462d085cff495809de1699652779d8e0257d6
 -F test/shell3.test a50628ab1d78d90889d9d3f32fb2c084ee15674771e96afe954aaa0accd1de3c
 -F test/shell4.test 8f6c0fce4abed19a8a7f7262517149812a04caa905d01bdc8f5e92573504b759
 +F test/shell3.test 4ddea2bd182e7e03249911b23ae249e7cb8a91cdc86e695198725affabe8ecd3
 +F test/shell4.test 867e0675d7b096d6b93de534541e07c7f5ffafa3e6612695ecf55180802e1115
- F test/shell5.test 3be444397eb1e91619ce289a6216a8df9ac690cc45d5e9595f60e750a944161f
+ F test/shell5.test 2b521446f55146c9aafccd0946bdb44ae288b0d25bd48f722e041974fdeeb04a
  F test/shell6.test 1ceb51b2678c472ba6cf1e5da96679ce8347889fe2c3bf93a0e0fa73f00b00d3
  F test/shell7.test 115132f66d0463417f408562cc2cf534f6bbc6d83a6d50f0072a9eb171bae97f
  F test/shell8.test 388471d16e4de767333107e30653983f186232c0e863f4490bb230419e830aae
- F test/shell9.test b6f07789fef57b5d194a3b2b8aba8292d363f23273cdc4541de7626ea659bc91
++F test/shell9.test 03e28206c016b27b3c79f43c3b09c843468dd8961f980e60d37117ff1ce32908
  F test/shmlock.test 3dbf017d34ab0c60abe6a44e447d3552154bd0c87b41eaf5ceacd408dd13fda5
  F test/shortread1.test bb591ef20f0fd9ed26d0d12e80eee6d7ac8897a3
  F test/show_speedtest1_rtree.tcl 32e6c5f073d7426148a6936a0408f4b5b169aba5
@@@ -1945,8 -1944,10 +1945,8 @@@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a9
  F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
  F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
  F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
- P 9c664984fd61d5858b436952d876bf3560333ee0edd8e1956cfe74cf9649511d 9edaeed56f2282fd4da935454178c38ab49d259aed96d4e720aae09050a53006
- R 241a1161ccd2087a62b9ac8bd7212d06
 -P 7e3c9594390ac8defaf9825e14b4c19ef8c123b747971dd3d4df16110f443d3b
 -R 2222d568cabc2aa5ac864df535d62eb2
 -T +sym-release *
 -T +sym-version-3.38.0 *
 -U drh
 -Z 2297eaa4fd3e9ed1fd7ae8af6c650431
++P 2b4a295c58a908288840157c3e08289af8c1d287f72e5f14b7adfd98065bf241 40fa792d359f84c3b9e9d6623743e1a59826274e221df1bde8f47086968a1bab
++R 5520524d725423f68aaec6a9faa61a16
 +U larrybr
- Z bc8dcf303171aacc369bf09b47a22c1e
++Z 1b4999d38bc60cdea13bcadad5fd8a8e
  # Remove this line to create a well-formed Fossil manifest.
diff --cc manifest.uuid
index 92ab286c2ce015d807b965ff635e16e2809c6449,b9b6e8d9ec5535d715c9b534011f464b1d8c81db..f2c7191aa183683c366ff69d7331409f3cbaa32d
@@@ -1,1 -1,1 +1,1 @@@
- 2b4a295c58a908288840157c3e08289af8c1d287f72e5f14b7adfd98065bf241
 -40fa792d359f84c3b9e9d6623743e1a59826274e221df1bde8f47086968a1bab
++8c9a5fb26ba045edef1269c5f5e8c8d87fa890b88ddb1121be72514a389a845d
diff --cc src/shell.c.in
index 35b5d75c8f4390f79993fb473b98b26848f559be,354c9a849bb324cde8b6fcf1ef0ba41e3cb08059..543e76149f8fb89faf2df3edb939734077a5bb8f
@@@ -4649,25 -4365,14 +4650,26 @@@ static const char *(azHelp[]) = 
    "     --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",
 +  ".parameter CMD ...       Manage SQL parameter bindings and scripts table",
 +  "   clear ?NAMES?           Erase all or only given named parameters",
 +#ifndef SQLITE_NOHAVE_SYSTEM
 +  "   edit ?OPT? NAME ...     Use edit() to create or alter parameter NAME",
 +  "      OPT may be -t or -e to use edited value as text or evaluate it first.",
 +#endif
 +  "   init                    Initialize TEMP table for bindings and scripts",
-   "   list                    List parameters table binding and script values",
++  "   list ?PATTERNS?         List parameters table binding and script values",
++  "      Alternatively, to list just some or all names: ls ?PATTERNS?",
 +  "   load ?FILE? ?NAMES?     Load some or all named parameters from FILE",
 +  "      If FILE missing, empty or '~', it defaults to ~/sqlite_params.sdb",
 +  "   save ?FILE? ?NAMES?     Save some or all named parameters into FILE",
 +  "      If FILE missing, empty or '~', it defaults to ~/sqlite_params.sdb",
 +  "   set ?TOPT? NAME VALUE   Give SQL parameter NAME a value of VALUE",
 +  "      NAME must begin with one of $,:,@,? for bindings, or with a letter",
 +  "      to be executable; the value is following argument list, space-joined.",
 +  "      Option TOPT may be one of {-b -i -n -r -t} to cast effective value",
 +  "      to BLOB, INT, NUMERIC, REAL or TEXT respectively.",
 +  "   unset ?NAMES?           Remove named parameter(s) from parameters table",
 +  ".print STRING...         Print literal STRING, then a newline",
  #ifndef SQLITE_OMIT_PROGRESS_CALLBACK
    ".progress N              Invoke progress handler after every N opcodes",
    "   --limit N                 Interrupt after N progress callbacks",
@@@ -8190,544 -7826,8 +8192,602 @@@ static int recoverDatabaseCmd(ShellStat
  }
  #endif /* !(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_ENABLE_DBPAGE_VTAB) */
  
- /* list subcommand for .parameter dot-command */
- static void list_params(ShellState *p, ParamTableUse ptu){
 +/*
 +** The .shxopts command, for setting or listing shell extension options.
 + */
 +#if SHELL_EXTENSIONS
 +static int shxoptsCommand(char *azArg[], int nArg, ShellState *p, char **pzE){
 +  static struct { const char *name; u8 mask; } shopts[] = {
 +#if SHELL_DYNAMIC_COMMANDS
 +    {"dyn_cmds", 1<<SHEXT_DYNCMDS_BIT},
 +#endif
 +#if SHELL_EXTENDED_PARSING
 +    {"parsing", 1<<SHEXT_PARSING_BIT},
 +#endif
 +#if SHELL_VARIABLE_EXPANSION
 +    {"dot_vars", 1<<SHEXT_VAREXP_BIT},
 +#endif
 +    {"all_opts", SHELL_ALL_EXTENSIONS}
 +  };
 +  const char *zMoan = 0, *zAbout = 0;
 +  int ia, io;
 +  if( nArg>1 ){
 +    for( ia=1; ia<nArg; ++ia ){
 +      char cs = azArg[ia][0];
 +      if( cs!='+' && cs!='-' ){
 +        zMoan = "arguments must have a sign prefix.";
 +        zAbout = azArg[0];
 +        goto moan_error;
 +      }
 +      for( io=0; io<ArraySize(shopts); ++io ){
 +        if( strcmp(azArg[ia]+1, shopts[io].name)==0 ){
 +          if( cs=='+' ) p->bExtendedDotCmds |= shopts[io].mask;
 +          else p->bExtendedDotCmds &= ~shopts[io].mask;
 +          break;
 +        }
 +      }
 +      if( io==ArraySize(shopts) ){
 +        zAbout = azArg[ia];
 +        zMoan = "is not a recognized option name";
 +        goto moan_error;
 +      }
 +    }
 +  }else{
 +    raw_printf(p->out,
 +               "     name    value  \"-shxopts set\"\n"
 +               "   --------  -----  ---------------\n");
 +    for( io=0; io<ArraySize(shopts); ++io ){
 +      unsigned m = shopts[io].mask;
 +      unsigned v = ((p->bExtendedDotCmds & m) == m)? 1 : 0;
 +      raw_printf(p->out,
 +                 "  %9s   %2d    \"-shxopts 0x%02X\"\n",
 +                 shopts[io].name,  v, m);
 +    }
 +  }
 +  return 0;
 + moan_error:
 +  raw_printf(stderr, "Error: %s %s\n", zAbout, zMoan);
 +  return 1;
 +}
 +#endif
 +
 +static int execute_variables(char *azArg[], int nArg, ShellState *p){
 +  int ia, rc, nErrors = 0;
 +  sqlite3_stmt *pStmt = 0;
 +  open_db(p, 0);
 +  if( p->db==0 ){
 +    utf8_printf(stderr, ".x can only be done with a database open.\n");
 +    return 1;
 +  }
 +  if( sqlite3_table_column_metadata(p->db, PARAM_TABLE_SCHEMA,
 +                                    PARAM_TABLE_NAME,
 +                                    "key", 0, 0, 0, 0, 0)!=SQLITE_OK ){
 +    utf8_printf(stderr, "No "PARAM_TABLE_SNAME" table exists.\n");
 +    return 1;
 +  }
 +  rc = sqlite3_prepare_v2
 +    (p->db, "SELECT value FROM "PARAM_TABLE_SNAME
 +     " WHERE key=$1 AND uses=1",
 +     -1, &pStmt, 0);
 +  if( rc!=SQLITE_OK ){
 +    utf8_printf(stderr, PARAM_TABLE_SNAME" is wrongly created.\n");
 +    return 1;
 +  }
 +  for( ia=1; ia < nArg; ++ia ){
 +    if( isalpha(azArg[ia][0]) ){
 +      rc = sqlite3_reset(pStmt);
 +      rc = sqlite3_bind_text(pStmt, 1, azArg[ia], -1, 0);
 +      rc = sqlite3_step(pStmt);
 +      if( rc==SQLITE_ROW ){
 +        const unsigned char *zValue = sqlite3_column_text(pStmt, 0);
 +        int nb = sqlite3_column_bytes(pStmt, 0);
 +        while( nb>0 && IsSpace(zValue[nb-1]) ) --nb;
 +        if( nb>0 ){
 +          /* The trailing newline (or some other placeholder) is important
 +           * because one (or some other character) will likely be put in
 +           * its place during process_input() line/group handling, along
 +           * with a terminating NUL character. Without it, the NULL could
 +           * land past the end of the allocation made just below.
 +           */
 +          int nle = zValue[nb-1]=='\n';
 +          char *zSubmit = sqlite3_mprintf( "%.*s%s", nb, zValue, "\n"+nle );
 +          InSource inRedir
 +            = INSOURCE_STR_REDIR(zSubmit, azArg[ia], p->pInSource);
 +          shell_check_oom(zSubmit);
 +          p->pInSource = &inRedir;
 +          rc = process_input(p);
 +          sqlite3_free(zSubmit);
 +          p->pInSource = inRedir.pFrom;
 +        }else{
 +          continue; /* All white, ignore. */
 +        }
 +      }else{
 +        utf8_printf(stderr,
 +                    "Skipping parameter '%s' (not set and executable.)\n",
 +                    azArg[ia]);
 +        ++nErrors;
 +      }
 +    }else{
 +      utf8_printf(stderr,
 +                  "Skipping badly named %s. Run \".help x\"\n", azArg[ia]);
 +      ++nErrors;
 +    }
 +  }
 +  sqlite3_finalize(pStmt);
 +  return (rc==2)? 2 : nErrors>0;
 +}
 +
 +struct param_row { char * value; int uses; int hits; };
 +
 +static int param_find_callback(void *pData, int nc, char **pV, char **pC){
 +  assert(nc>=1);
 +  assert(strcmp(pC[0],"value")==0);
 +  struct param_row *pParam = (struct param_row *)pData;
 +  assert(pParam->value==0); /* key values are supposedly unique. */
 +  if( pParam->value!=0 ) sqlite3_free( pParam->value );
 +  pParam->value = sqlite3_mprintf("%s", pV[0]); /* source owned by statement */
 +  if( nc>1 ) pParam->uses = (int)integerValue(pV[1]);
 +  ++pParam->hits;
 +  return 0;
 +}
 +
 +static void append_in_clause(sqlite3_str *pStr,
 +                             const char **azBeg, const char **azLim);
++static void append_glob_terms(sqlite3_str *pStr, const char *zColName,
++                              const char **azBeg, const char **azLim);
 +static char *find_home_dir(int clearFlag);
 +
 +/* Create a home-relative pathname from ~ prefixed path.
 + * Return it, or 0 for any error.
 + * Caller must sqlite3_free() it.
 + */
 +static char *home_based_path( const char *zPath ){
 +  char *zHome = find_home_dir(0);
 +  char *zErr = 0;
 +  assert( zPath[0]=='~' );
 +  if( zHome==0 ){
 +    zErr = "Cannot find home directory.";
 +  }else if( zPath[0]==0 || (zPath[1]!='/'
 +#if defined(_WIN32) || defined(WIN32)
 +                            && zPath[1]!='\\'
 +#endif
 +                            ) ){
 +    zErr = "Malformed pathname";
 +  }else{
 +    return sqlite3_mprintf("%s%s", zHome, zPath+1);
 +  }
 +  utf8_printf(stderr, "Error: %s\n", zErr);
 +  return 0;
 +}
 +
 +/* Transfer selected parameters between two parameter tables, for save/load.
 + * Argument bSaveNotLoad determines transfer direction and other actions.
 + * If it is true, the store DB will be created if not existent, and its
 + * table for keeping parameters will be created. Or failure is returned.
 + * If it is false, the store DB will be opened for read and its presumed
 + * table for keeping parameters will be read. Or failure is returned.
 + *
 + * Arguments azNames and nNames reference the ?NAMES? save/load arguments.
 + * If it is an empty list, all parameters will be saved or loaded.
 + * Otherwise, only the named parameters are transferred, if they exist.
 + * It is not an error to specify a name that cannot be transferred
 + * because it does not exist in the source table.
 + *
 + * Returns are SQLITE_OK for success, or other codes for failure.
 + */
 +static int param_xfr_table(sqlite3 *db, const char *zStoreDbName,
 +                           int bSaveNotLoad, const char *azNames[], int nNames){
 +  int rc = 0;
 +  char *zSql = 0; /* to be sqlite3_free()'ed */
 +  sqlite3_str *sbCopy = 0;
 +  const char *zHere = PARAM_TABLE_SNAME;
 +  const char *zThere = PARAM_STORE_SNAME;
 +  const char *zTo = (bSaveNotLoad)? zThere : zHere;
 +  const char *zFrom = (bSaveNotLoad)? zHere : zThere;
 +  sqlite3 *dbStore = 0;
 +  int openFlags = (bSaveNotLoad)
 +    ? SQLITE_OPEN_URI|SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE
 +    : SQLITE_OPEN_READONLY;
 +
 +  /* Ensure store DB can be opened and/or created appropriately. */
 +  rc = sqlite3_open_v2(zStoreDbName, &dbStore, openFlags, 0);
 +  if( rc!=SQLITE_OK ){
 +    utf8_printf(stderr, "Error: Cannot %s parameter store DB %s\n",
 +                bSaveNotLoad? "open/create" : "read", zStoreDbName);
 +    return rc;
 +  }
 +  /* Ensure it has the parameter store table, or handle its absence. */
 +  assert(dbStore!=0);
 +  if( sqlite3_table_column_metadata
 +      (dbStore, "main", PARAM_STORE_NAME, 0, 0, 0, 0, 0, 0)!=SQLITE_OK ){
 +    if( !bSaveNotLoad ){
 +      utf8_printf(stderr, "Error: No parameters ever stored in DB %s\n",
 +                  zStoreDbName);
 +      rc = 1;
 +    }else{
 +      /* The saved parameters table is not there yet; create it. */
 +      const char *zCT =
 +        "CREATE TABLE IF NOT EXISTS "PARAM_STORE_NAME"(\n"
 +        "  key TEXT PRIMARY KEY,\n"
 +        "  value,\n"
 +        "  uses INT\n"
 +        ") WITHOUT ROWID;";
 +      rc = sqlite3_exec(dbStore, zCT, 0, 0, 0);
 +      if( rc!=SQLITE_OK ){
 +        utf8_printf(stderr, "Cannot create table %s. Nothing saved.", zThere);
 +      }
 +    }
 +  }
 +  sqlite3_close(dbStore);
 +  if( rc!=0 ) return rc;
 +
 +  zSql = sqlite3_mprintf("ATTACH %Q AS %s;", zStoreDbName, PARAM_STORE_SCHEMA);
 +  shell_check_oom(zSql);
 +  rc = sqlite3_exec(db, zSql, 0, 0, 0);
 +  sqlite3_free(zSql);
 +  if( rc!=SQLITE_OK ) return rc;
 +
 +  sbCopy = sqlite3_str_new(db);
 +  sqlite3_str_appendf
 +      (sbCopy, "INSERT OR REPLACE INTO %s(key,value,uses)"
 +       "SELECT key, value, uses FROM %s WHERE key ", zTo, zFrom);
 +  append_in_clause(sbCopy, azNames, azNames+nNames);
 +  zSql = sqlite3_str_finish(sbCopy);
 +  shell_check_oom(zSql);
 +  rc = sqlite3_exec(db, zSql, 0, 0, 0);
 +  sqlite3_free(zSql);
 +
 +  sqlite3_exec(db, "DETACH "PARAM_STORE_SCHEMA";", 0, 0, 0);
 +  return rc;
 +}
 +
 +/* Default location of parameters store DB for .parameters save/load. */
 +static const char *zDefaultParamStore = "~/sqlite_params.sdb";
 +
 +/* Possibly generate a derived path from input spec, with defaulting
 + * and conversion of leading (or only) tilde as home directory.
 + * The above-set default is used for zSpec NULL, "" or "~".
 + * When return is 0, there is an error; what needs doing cannnot be done.
 + * If the return is exactly the input, it must not be sqlite3_free()'ed.
 + * If the return differs from the input, it must be sqlite3_free()'ed.
 + */
 +static const char *params_store_path(const char *zSpec){
 +  if( zSpec==0 || zSpec[0]==0 || strcmp(zSpec,"~")==0 ){
 +    return home_based_path(zDefaultParamStore);
 +  }else if ( zSpec[0]=='~' ){
 +    return home_based_path(zSpec);
 +  }
 +  return zSpec;
 +}
 +
 +/* Load some or all parameters. Arguments are "load FILE ?NAMES?". */
 +static int parameters_load(sqlite3 *db, const char *azArg[], int nArg){
 +  const char *zStore = params_store_path((nArg>1)? azArg[1] : 0);
 +  if( zStore==0 ){
 +    utf8_printf(stderr, "Cannot form parameter load path. Nothing loaded.\n");
 +    return 1;
 +  }else{
 +    const char **pzFirst = (nArg>2)? azArg+2 : 0;
 +    int nNames = (nArg>2)? nArg-2 : 0;
 +    int rc = param_xfr_table(db, zStore, 0, pzFirst, nNames);
 +    if( nArg>1 && zStore!=azArg[1] ) sqlite3_free((void*)zStore);
 +    return rc;
 +  }
 +}
 +
 +/* Save some or all parameters. Arguments are "save FILE ?NAMES?". */
 +static int parameters_save(sqlite3 *db, const char *azArg[], int nArg){
 +  const char *zStore = params_store_path((nArg>1)? azArg[1] : 0);
 +  if( zStore==0 ){
 +    utf8_printf(stderr, "Cannot form parameter save path. Nothing saved.\n");
 +    return 1;
 +  }else{
 +    const char **pzFirst = (nArg>2)? azArg+2 : 0;
 +    int nNames = (nArg>2)? nArg-2 : 0;
 +    int rc = param_xfr_table(db, zStore, 1, pzFirst, nNames);
 +    if( nArg>1 && zStore!=azArg[1] ) sqlite3_free((void*)zStore);
 +    return rc;
 +  }
 +}
 +
 +#ifndef SQLITE_NOHAVE_SYSTEM
 +/*
 + * Edit one named parameter in the parameters table. If it does not
 + * yet exist, create it. If eval is true, the value is treated as a
 + * bare expression, otherwise it is a text value. The uses argument
 + * sets the 3rd column in the parameters table, and may also serve
 + * to partition the key namespace. (This is not done now.)
 + */
 +static int edit_one_param(sqlite3 *db, char *name, int eval,
 +                          ParamTableUse uses, const char * zEditor){
 +  struct param_row paramVU = {0,0,0};
 +  int rc;
 +  char * zVal = 0;
 +  char * zSql = sqlite3_mprintf
 +    ("SELECT value, uses FROM " PARAM_TABLE_SNAME " WHERE key=%Q", name);
 +  shell_check_oom(zSql);
 +  sqlite3_exec(db, zSql, param_find_callback, &paramVU, 0);
 +  sqlite3_free(zSql);
 +  assert(paramVU.hits<2);
 +  if( paramVU.hits==1 && paramVU.uses==uses){
 +    /* Editing an existing value of same kind. */
 +    sqlite3_free(paramVU.value);
 +    if( eval!=0 ){
 +      zSql = sqlite3_mprintf
 +        ("SELECT edit(value, %Q) FROM " PARAM_TABLE_SNAME
 +         " WHERE key=%Q AND uses=%d", zEditor, name, uses);
 +      zVal = db_text(db, zSql, 1);
 +      sqlite3_free(zSql);
 +      zSql = sqlite3_mprintf
 +        ("UPDATE "PARAM_TABLE_SNAME" SET value=(SELECT %s) WHERE"
 +         " key=%Q AND uses=%d", zVal, name, uses);
 +    }else{
 +      zSql = sqlite3_mprintf
 +        ("UPDATE "PARAM_TABLE_SNAME" SET value=edit(value, %Q) WHERE"
 +         " key=%Q AND uses=%d", zEditor, name, uses);
 +    }
 +  }else{
 +    /* Editing a new value of same kind. */
 +    assert(paramVU.value==0 || paramVU.uses!=uses);
 +    if( eval!=0 ){
 +      zSql = sqlite3_mprintf
 +        ("SELECT edit('-- %q%s', %Q)", name, "\n", zEditor);
 +      zVal = db_text(db, zSql, 1);
 +      sqlite3_free(zSql);
 +      zSql = sqlite3_mprintf
 +        ("INSERT INTO "PARAM_TABLE_SNAME"(key,value,uses)"
 +         " VALUES (%Q,(SELECT %s LIMIT 1),%d)",
 +         name, zVal, uses);
 +    }else{
 +      zSql = sqlite3_mprintf
 +        ("INSERT INTO "PARAM_TABLE_SNAME"(key,value,uses)"
 +         " VALUES (%Q,edit('-- %q%s', %Q),%d)",
 +         name, name, "\n", zEditor, uses);
 +    }
 +  }
 +  shell_check_oom(zSql);
 +  if( eval!=0 ){
 +  }
 +  rc = sqlite3_exec(db, zSql, 0, 0, 0);
 +  sqlite3_free(zSql);
 +  sqlite3_free(zVal);
 +  return rc!=SQLITE_OK;
 +}
 +#endif
 +
 +/* Space-join values in an argument list. *valLim is not included. */
 +char *values_join( char **valBeg, char **valLim ){
 +  char *z = 0;
 +  const char *zSep = 0;
 +  while( valBeg < valLim ){
 +    z = sqlite3_mprintf("%z%s%s", z, zSep, *valBeg);
 +    zSep = " ";
 +    ++valBeg;
 +  }
 +  return z;
 +}
 +
 +/* Get a named parameter value in form of stepped prepared statement,
 + * ready to have its value taken from the 0th column. If the name
 + * cannot be found for the given ParamTableUse, 0 is returned.
 + * The caller is responsible for calling sqlite3_finalize(pStmt),
 + * where pStmt is the return from this function.
 + */
 +static sqlite3_stmt *get_param_value(sqlite3 *db, char *name,
 +                                     ParamTableUse ptu){
 +  sqlite3_stmt *rv = 0;
 +  int rc;
 +  char *zSql = sqlite3_mprintf
 +    ( "SELECT value FROM "PARAM_TABLE_SNAME
 +      " WHERE key=%Q AND uses=%d", name, ptu );
 +  shell_check_oom(zSql);
 +  rc = sqlite3_prepare_v2(db, zSql, -1, &rv, 0);
 +  sqlite3_free(zSql);
 +  if( SQLITE_OK==rc ){
 +    if( SQLITE_ROW==sqlite3_step(rv) ) return rv;
 +    sqlite3_finalize(rv);
 +  }
 +  return 0;
 +}
 +
 +static struct ParamSetOpts {
 +  const char cCast;
 +  const char *zTypename;
 +  int evalKind;
 +} param_set_opts[] = {
 +  /* { 'q', 0, 2 }, */
 +  /* { 'x', 0, 1 }, */
 +  { 'i', "INT", 1 },
 +  { 'r', "REAL", 1 },
 +  { 'b', "BLOB", 1 },
 +  { 't', "TEXT", 0 },
 +  { 'n', "NUMERIC", 1 }
 +};
 +
 +/* Return an option character if it is single and prefixed by - or --,
 + * else return 0.
 + */
 +static char option_char(char *zArg){
 +  if( zArg[0]=='-' ){
 +    ++zArg;
 +    if( zArg[0]=='-' ) ++zArg;
 +    if( zArg[0]!=0 && zArg[1]==0 ) return zArg[0];
 +  }
 +  return 0;
 +}
 +
 +static int param_set(sqlite3 *db, char cCast,
 +                     char *name, char **valBeg, char **valLim,
 +                     ParamTableUse ptu){
 +  char *zSql = 0;
 +  int rc = SQLITE_OK, retries = 0, needsEval = 1;
 +  char *zValGlom = (valLim-valBeg>1)? values_join(valBeg, valLim) : 0;
 +  sqlite3_stmt *pStmtSet = 0;
 +  const char *zCastTo = 0;
 +  char *zValue = (zValGlom==0)? *valBeg : zValGlom;
 +  if( cCast ){
 +    struct ParamSetOpts *pSO = param_set_opts;
 +    for(; pSO-param_set_opts < ArraySize(param_set_opts); ++pSO ){
 +      if( cCast==pSO->cCast ){
 +        zCastTo = pSO->zTypename;
 +        needsEval = pSO->evalKind > 0;
 +        break;
 +      }
 +    }
 +  }
 +  if( needsEval ){
 +    if( zCastTo!=0 ){
 +      zSql = sqlite3_mprintf
 +        ( "REPLACE INTO "PARAM_TABLE_SNAME"(key,value,uses)"
 +          " VALUES(%Q,CAST((%s) AS %s),%d);", name, zValue, zCastTo, ptu );
 +    }else{
 +      zSql = sqlite3_mprintf
 +        ( "REPLACE INTO "PARAM_TABLE_SNAME"(key,value,uses)"
 +          "VALUES(%Q,(%s),%d);", name, zValue, ptu );
 +    }
 +    shell_check_oom(zSql);
 +    rc = sqlite3_prepare_v2(db, zSql, -1, &pStmtSet, 0);
 +    sqlite3_free(zSql);
 +  }
 +  if( !needsEval || rc!=SQLITE_OK ){
 +    /* Reach here when value either requested to be cast to text, or must be. */
 +    sqlite3_finalize(pStmtSet);
 +    pStmtSet = 0;
 +    zSql = sqlite3_mprintf
 +      ( "REPLACE INTO "PARAM_TABLE_SNAME"(key,value,uses)"
 +        "VALUES(%Q,%Q,%d);", name, zValue, ptu );
 +    shell_check_oom(zSql);
 +    rc = sqlite3_prepare_v2(db, zSql, -1, &pStmtSet, 0);
 +    assert(rc==SQLITE_OK);
 +    sqlite3_free(zSql);
 +  }
 +  sqlite3_step(pStmtSet);
 +  sqlite3_finalize(pStmtSet);
 +  sqlite3_free(zValGlom);
 +  return rc;
 +}
 +
-   int len = 0;
-   int rc = sqlite3_prepare_v2
-     (p->db, "SELECT max(length(key)) FROM "
-      PARAM_TABLE_SNAME" WHERE ?1=3 OR uses=?1", -1, &pStmt, 0);
++/* list or ls subcommand for .parameter dot-command */
++static void list_params(ShellState *p, ParamTableUse ptu, u8 bShort,
++                        char **pzArgs, int nArg){
 +  sqlite3_stmt *pStmt = 0;
-     utf8_printf(p->out, "%-*s %-8s %s\n", len, "name", "usage", "value");
-     rc = sqlite3_prepare_v2
-       (p->db, "SELECT key, uses, quote(value) FROM " PARAM_TABLE_SNAME
-        " WHERE ?1=3 OR uses=?1 ORDER BY key", -1, &pStmt, 0);
-     sqlite3_bind_int(pStmt, 1, ptu);
-     while( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
-       ParamTableUse ptux = sqlite3_column_int(pStmt,1);
-       const char *zUse;
-       switch( ptux ){
-       case PTU_Binding: zUse = "binding"; break;
-       case PTU_Script:  zUse = "script"; break;
-       default:          zUse = "unknown";
++  sqlite3_str *sbList = sqlite3_str_new(p->db);
++  int len = 0, rc;
++  char *zFromWhere = 0;
++  char *zSql = 0;
++  sqlite3_str_appendf(sbList, "FROM "PARAM_TABLE_SNAME
++                      " WHERE (?1=3 OR uses=?1) AND ");
++  append_glob_terms(sbList, "key",
++                    (const char **)pzArgs, (const char **)pzArgs+nArg);
++  zFromWhere = sqlite3_str_finish(sbList);
++  shell_check_oom(zFromWhere);
++  zSql = sqlite3_mprintf("SELECT max(length(key)) %s", zFromWhere);
++  shell_check_oom(zSql);
++  rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
 +  if( rc==SQLITE_OK ){
 +    sqlite3_bind_int(pStmt, 1, ptu);
 +    if( sqlite3_step(pStmt)==SQLITE_ROW ){
 +      len = sqlite3_column_int(pStmt, 0);
 +      if( len>40 ) len = 40;
 +      if( len<4 ) len = 4;
 +    }
 +  }
 +  sqlite3_finalize(pStmt);
 +  pStmt = 0;
 +  if( len ){
-       utf8_printf(p->out, "%-*s %-8s %s\n", len, sqlite3_column_text(pStmt,0),
-                   zUse, sqlite3_column_text(pStmt,2));
++    sqlite3_free(zSql);
++    if( !bShort ){
++      int nBindings = 0, nScripts = 0;
++      zSql = sqlite3_mprintf("SELECT key, uses, iif(uses, value, quote(value))"
++                             " %z ORDER BY uses, key", zFromWhere);
++      shell_check_oom(zSql);
++      rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
++      sqlite3_bind_int(pStmt, 1, ptu);
++      while( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
++        ParamTableUse ptux = sqlite3_column_int(pStmt,1);
++        switch( ptux ){
++        case PTU_Binding:
++          if( nBindings++ == 0 ){
++            utf8_printf(p->out, "Binding Values:\n%-*s %s\n",
++                        len, "name", "value");
++          }
++          utf8_printf(p->out, "%-*s %s\n", len, sqlite3_column_text(pStmt,0),
++                      sqlite3_column_text(pStmt,2));
++          break;
++        case PTU_Script:
++          if( nScripts++ == 0 ){
++            utf8_printf(p->out, "Scripts\n%-*s %s\n", len, "name", "value");
++          }
++          utf8_printf(p->out, "%-*s %s\n", len, sqlite3_column_text(pStmt,0),
++                      sqlite3_column_text(pStmt,2));
++          break;
++        default: break; /* Ignore */
++        }
 +      }
++    }else{
++      int nc = 0, ncw = 78/(len+2);
++      zSql = sqlite3_mprintf("SELECT key %z ORDER BY key", zFromWhere);
++      shell_check_oom(zSql);
++      rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
++      sqlite3_bind_int(pStmt, 1, ptu);
++      while( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
++        utf8_printf(p->out, "%s  %-*s", ((++nc%ncw==0)? "\n" : ""),
++                    len, sqlite3_column_text(pStmt,0));
++      }
++      if( nc>0 ) utf8_printf(p->out, "\n");
 +    }
 +    sqlite3_finalize(pStmt);
++  }else{
++    sqlite3_free(zFromWhere);
++  }
++  sqlite3_free(zSql);
++}
++
++/* Append an OR'ed series of GLOB terms comparing a given column
++ * name to a series of patterns. Result is an appended expression.
++ * For an empty pattern series, expression is true for non-NULL.
++ */
++static void append_glob_terms(sqlite3_str *pStr, const char *zColName,
++                              const char **azBeg, const char **azLim){
++  if( azBeg==azLim ) sqlite3_str_appendf(pStr, "%s NOT NULL", zColName);
++  else{
++    char *zSep = "(";
++    while( azBeg<azLim ){
++      sqlite3_str_appendf(pStr, "%sglob(%Q,%s)", zSep, *azBeg, zColName);
++      zSep = " OR ";
++      ++azBeg;
++    }
++    sqlite3_str_appendf(pStr, ")");
 +  }
 +}
 +
 +/* Append either an IN clause or an always true test to some SQL.
 + *
 + * An empty IN list is the same as always true (for non-NULL LHS)
 + * for this clause, which assumes a trailing LHS operand and space.
 + * If that is not the right result, guard the call against it.
 + * This is used for ".parameter dostuff ?NAMES?" options,
 + * where a missing list means all the qualifying entries.
 + *
 + * The empty list may be signified by azBeg and azLim both 0.
 + */
 +static void append_in_clause(sqlite3_str *pStr,
 +                             const char **azBeg, const char **azLim){
 +  if( azBeg==azLim ) sqlite3_str_appendf(pStr, "NOT NULL");
 +  else{
 +    char cSep = '(';
 +    sqlite3_str_appendf(pStr, "IN");
 +    while( azBeg<azLim ){
 +      sqlite3_str_appendf(pStr, "%c%Q", cSep, *azBeg);
 +      cSep = ',';
 +      ++azBeg;
 +    }
 +    sqlite3_str_appendf(pStr, ")");
 +  }
 +}
 +
  
 -/* 
 +/*
   * zAutoColumn(zCol, &db, ?) => Maybe init db, add column zCol to it.
   * zAutoColumn(0, &db, ?) => (db!=0) Form columns spec for CREATE TABLE,
   *   close db and set it to 0, and return the columns spec, to later
@@@ -10705,30 -9735,7 +10765,32 @@@ static int do_meta_command(char *zLine
      ** Create it if necessary.
      */
      if( nArg==2 && strcmp(azArg[1],"init")==0 ){
 -      bind_table_init(p);
 +      param_table_init(p);
 +    }else
 +
-     /* .parameter list
-     ** List all bind parameters.
++    /* .parameter list|ls
++    ** List all or selected bind parameters.
++    ** list displays names, values and uses.
++    ** ls displays just the names.
 +    */
-     if( nArg==2 && strcmp(azArg[1],"list")==0 ){
-       /* Future: Allow selection of categories. */
-       list_params(p, PTU_Nil);
++    if( nArg>=2 && ((strcmp(azArg[1],"list")==0)
++                    || (strcmp(azArg[1],"ls")==0)) ){
++      list_params(p, PTU_Nil, azArg[1][1]=='s', azArg+2, nArg-2);
 +    }else
 +
 +    /* .parameter load
 +    ** Load all or named parameters from specified or default (DB) file.
 +    */
 +    if( strcmp(azArg[1],"load")==0 ){
 +      param_table_init(p);
 +      rc = parameters_load(p->db, (const char **)azArg+1, nArg-1);
 +    }else
 +
 +    /* .parameter save
 +    ** Save all or named parameters into specified or default (DB) file.
 +    */
 +    if( strcmp(azArg[1],"save")==0 ){
 +      rc = parameters_save(p->db, (const char **)azArg+1, nArg-1);
      }else
  
      /* .parameter set NAME VALUE
index d5df53d58690bd7aa0f949a696b32dd0c7502e11,0000000000000000000000000000000000000000..6113ce748a9691973284ef5e4ea4564305fcacc4
mode 100644,000000..100644
--- /dev/null
@@@ -1,253 -1,0 +1,260 @@@
- } {0 {name  usage    value
- expr  script   7
- text  script   '1 + 2*3'
- ttext script   '1 + 2*3'}}
 +# 2022 Feb 5
 +#
 +# The author disclaims copyright to this source code.  In place of
 +# a legal notice, here is a blessing:
 +#
 +#    May you do good and not evil.
 +#    May you find forgiveness for yourself and forgive others.
 +#    May you share freely, never taking more than you give.
 +#
 +#***********************************************************************
 +#
 +# The focus of this file is testing the CLI shell tool enhanced parsing,
 +# new .parameter subcommands and uses, and the new .x meta-command.
 +#
 +#
 +
 +# Test plan:
 +#
 +#   shell9-1.*: command line parsing and acting accordingly
 +#   shell9-2.*: Basic "dot" command, cross-line token parsing
 +#   shell9-3.*: .parameter set options and types
 +#   shell9-4.*: .parameter save/load operation
 +#   shell9-5.*: Ensure "dot" commands and SQL intermix ok.
 +#   shell9-6.*: .x command operation and refusal
 +#
 +set testdir [file dirname $argv0]
 +source $testdir/tester.tcl
 +set CLI [test_find_cli]
 +db close
 +forcedelete test.db test.db-journal test.db-wal
 +forcedelete x.db xn.db
 +
 +sqlite3 db test.db
 +
 +#----------------------------------------------------------------------------
 +# Test cases shell9-1.*: command line parsing and acting accordingly
 +
 +do_test shell9-1.1 {
 +  set res [catchcmd ":memory: -cmd .quit" ""]
 +} {0 {}}
 +
 +do_test shell9-1.2 {
 +  set res [catchcmd ":memory: -shxopts 1 -cmd .shxopts -cmd .quit" ""]
 +} {0 {     name    value  "-shxopts set"
 +   --------  -----  ---------------
 +    parsing    1    "-shxopts 0x01"
 +   all_opts    0    "-shxopts 0x07"}}
 +
 +do_test shell9-1.3 {
 +  set res [catchcmd ":memory: -cmd .shxopts -cmd .quit" ""]
 +} {0 {     name    value  "-shxopts set"
 +   --------  -----  ---------------
 +    parsing    0    "-shxopts 0x01"
 +   all_opts    0    "-shxopts 0x07"}}
 +
 +#----------------------------------------------------------------------------
 +# Test cases shell9-2.*: Basic "dot" command, cross-line token parsing
 +
 +set cmds ".print 'l1\nl2'\n.print 'a\\\nb'"
 +do_test shell9-2.1 {
 +  set res [catchcmd ":memory: -shxopts 1" $cmds]
 +} {0 {l1
 +l2
 +ab}}
 +
 +set cmds " .print \"l1\nl2\"\n .print \"a\\\nb\" \n# c\n  ## c"
 +do_test shell9-2.2 {
 +  set res [catchcmd ":memory: -shxopts 1" $cmds]
 +} {0 {l1
 +l2
 +ab}}
 +
 +set cmds ".echo on\n.seeargs 'a'\\\n'b'\n#!"
 +do_test shell9-2.3 {
 +  set res [catchcmd ":memory: -shxopts 1" $cmds]
 +} {0 {.seeargs 'a''b'
 +a|b|}}
 +
 +set cmds ".echo on\n.seeargs a\\\nb\n#!"
 +do_test shell9-2.4 {
 +  set res [catchcmd ":memory: -shxopts 1" $cmds]
 +} {0 {.seeargs ab
 +ab|}}
 +
 +set cmds ".echo 1\n.print \"\\\"\nq\\\"\""
 +do_test shell9-2.5 {
 +  set res [catchcmd ":memory: -shxopts 1" $cmds]
 +} {0 {.print "\"
 +q\""
 +"
 +q"}}
 +
 +#----------------------------------------------------------------------------
 +# Test cases shell9-3.*: .parameter set options and types
 +
 +set cmds {
 +.pa set -b b x'a5a5'
 +.pa set -i ii 33-11
 +.pa set -i ir 3.3-1.1
 +.pa set -n ni 3-1
 +.pa set -n nr 3.3-1.1
 +.pa set -r ri 1
 +.pa set -r rr 1.2
 +.pa set -t t 123
 +.mode list
 +select typeof(value) from temp.sqlite_parameters order by key;
 +}
 +do_test shell9-3.1 {
 +  set res [catchcmd ":memory:" $cmds]
 +} {0 {blob
 +integer
 +integer
 +integer
 +real
 +real
 +real
 +text}}
 +
 +set cmds {
 +.pa set expr 1 + 2 * 3
 +.pa set text "'1 + 2*3'"
 +.pa set -t ttext 1 + 2*3
 +.pa list
 +}
 +do_test shell9-3.2 {
 +  set res [catchcmd ":memory:" $cmds]
- } {0 {name usage    value
- a    script   'a'
- b    script   'b'
- c    script   'c'
- name usage    value
- b    script   'b'
- c    script   'c'
- name usage    value
- c    script   'c'
- name usage    value
- name usage    value}}
++} {0 {Scripts
++name  value
++expr  7
++text  1 + 2*3
++ttext 1 + 2*3}}
 +
 +set cmds {
 +.pa set a "'a'"
 +.pa set b "'b'"
 +.pa set c "'c'"
 +.pa unset
 +.pa list
 +.pa clear a
 +.pa list
 +.pa unset b
 +.pa list
 +.pa clear
 +.pa list
 +.pa set d "'e'"
 +.pa set e "'e'"
 +.pa unset d e
 +.pa list
 +}
 +do_test shell9-3.3 {
 +  set res [catchcmd ":memory:" $cmds]
- } {0 {name usage    value
- $n   binding  7
- x    script   '.print Ex'
- name usage    value
- x    script   '.print Ex'
- name usage    value
- $n   binding  7}}
++} {0 {Scripts
++name value
++a    a
++b    b
++c    c
++Scripts
++name value
++b    b
++c    c
++Scripts
++name value
++c    c}}
 +
 +if {$::tcl_platform(platform)=="unix"} {
 +  proc set_ed {sayWhat} {
 +    global env
 +    set env(VISUAL) "echo SELECT $sayWhat ';' >"
 +    return 1
 +  }
 +} elseif {$::tcl_platform(platform)=="windows"} {
 +  proc set_ed {sayWhat} {
 +    global env
 +    set env(VISUAL) "echo SELECT $sayWhat ; >"
 +    return 1
 +  }
 +} else { return 0 }
 +
 +if {[set_ed @name]} {
 +  set cmds {
 +.pa set @name Fido
 +.pa edit -t dog
 +.x dog
 +  }
 +  do_test shell9-3.4 {
 +    set res [catchcmd ":memory: -quiet 1 -shxopts 1 -interactive" $cmds]
 +  } {0 {.pa set @name Fido
 +.pa edit -t dog
 +.x dog
 +Fido
 +  }}
 +}
 +
 +#----------------------------------------------------------------------------
 +# Test cases shell9-4.*: .parameter save/load operation
 +
 +set cmds {
 +  .pa set -t x '.print Ex'
 +  .pa set -i $n 7
 +  .pa save xn.db
 +  .pa save x.db x
 +  .pa clear
 +  .pa load xn.db
 +  .pa list
 +  .pa clear
 +  .pa load x.db
 +  .pa list
 +  .pa clear
 +  .pa load xn.db $n
 +  .pa list
 +}
 +do_test shell9-4.1 {
 +  set res [catchcmd ":memory: -shxopts 1" $cmds]
++} {0 {Binding Values:
++name value
++$n   7
++Scripts
++name value
++x    .print Ex
++Scripts
++name value
++x    .print Ex
++Binding Values:
++name value
++$n   7}}
 +
 +forcedelete x.db xn.db
 +
 +#----------------------------------------------------------------------------
 +# Test cases shell9-5.*: Ensure "dot" commands and SQL intermix ok.
 +
 +set cmds {
 + .pa set -t mixed "
 + .print Hi.
 + select 'Hi.';
 + .print 'Good\
 + Bye.'
 + select 'Good'||
 + ' Bye.';
 + "
 + .x mixed
 +}
 +do_test shell9-5.1 {
 +  set res [catchcmd ":memory: -shxopts 1" $cmds]
 +} {0 {Hi.
 +Hi.
 +Good Bye.
 +Good Bye.}}
 +
 +#----------------------------------------------------------------------------
 +# Test cases shell9-6.*: .x command operation and refusal
 +set cmds {
 + .pa set -t $v '.print Ok'
 + .x $v
 +}
 +do_test shell9-6.1 {
 +  set res [catchcmd ":memory: -bail -shxopts 1" $cmds]
 +} {1 {Skipping badly named $v. Run ".help x"}}
 +
 +finish_test