From: larrybr Date: Sun, 27 Mar 2022 23:33:43 +0000 (+0000) Subject: TCL extension made to use shell's line inputter with prompting and line editing/history. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ef1a2bf778e54168e2e4e6b76ddeb62e878e1010;p=thirdparty%2Fsqlite.git TCL extension made to use shell's line inputter with prompting and line editing/history. FossilOrigin-Name: fbf0eb0d12932513bba4c6a6ef31d9972d704ab38690c806098504a4cd67786d --- diff --git a/ext/misc/tclshext.c.in b/ext/misc/tclshext.c.in index a5a4127be5..108ae52766 100644 --- a/ext/misc/tclshext.c.in +++ b/ext/misc/tclshext.c.in @@ -103,6 +103,132 @@ static void copy_complaint(char **pzErr, Tcl_Interp *pi){ } } +/* The .tcl REPL script is one of the 3 following string literals, + * selected at build time for these different purposes: + * 1st: a simple input collection, reading only stdin, which may + * be (handily) used as a fallback for debugging purposes. + * 2nd: input collection which honors the shell's input switching + * and otherwise has low dependency upon shell features, which + * means that it has no input line editing or history recall. + * 3rd: an input collection which fully leverages the shell's + * input collection. It has higher shell dependency, and for + * that it gains the shell's line editing and history recall, + * in addition to working with the shell's input switching. + */ +#ifdef TCL_REPL_STDIN_ONLY +# define TCL_REPL 1 +#elif defined(TCL_REPL_LOW_DEPENDENCY) +# define TCL_REPL 2 +#else +# define TCL_REPL 3 +#endif + +static const char * const zREPL = +#if TCL_REPL==1 /* a fallback for debug */ + "set line {}\n" + "while {![eof stdin]} {\n" + "if {$line!=\"\"} {\n" + "puts -nonewline \"> \"\n" + "} else {\n" + "puts -nonewline \"% \"\n" + "}\n" + "flush stdout\n" + "append line [gets stdin]\n" + "if {$line eq \".\"} break\n" + "if {[info complete $line]} {\n" + "if {[catch {uplevel #0 $line} result]} {\n" + "puts stderr \"Error: $result\"\n" + "} elseif {$result!=\"\"} {\n" + "puts $result\n" + "}\n" + "set line {}\n" + "} else {\n" + "append line \\n\n" + "}\n" + "}\n" + "if {$line ne \".\"} {puts {}}\n" + "read stdin 0\n" +#elif TCL_REPL==2 /* minimal use of shell's read */ + "namespace eval ::REPL {\n" + "variable line {}\n" + "variable at_end 0\n" + "variable prompting [now_interactive]\n" + "}\n" + "while {!$::REPL::at_end} {\n" + "if {$::REPL::prompting} {\n" + "if {$::REPL::line!=\"\"} {\n" + "puts -nonewline \"...> \"\n" + "} else {\n" + "puts -nonewline \"tcl% \"\n" + "}\n" + "}\n" + "flush stdout\n" + "set ::REPL::li [get_input_line]\n" + "if {$::REPL::li eq \"\"} {\n" + "set ::REPL::at_end 1\n" + "} elseif {[string trimright $::REPL::li] eq \".\"} {\n" + "if {$::REPL::line ne \"\"} {\n" + "throw {NONE} {incomplete input at EOF}\n" + "}\n" + "set ::REPL::at_end 1\n" + "} else {\n" + "append ::REPL::line $::REPL::li\n" + "if {[string trim $::REPL::line] eq \"\"} {\n" + "set ::REPL::line \"\"\n" + "continue\n" + "}\n" + "if {[info complete $::REPL::line]} {\n" + "set ::REPL::rc [catch {uplevel #0 $::REPL::line} ::REPL::result]\n" + "if {$::REPL::rc == 0} {\n" + "if {$::REPL::result!=\"\" && $::REPL::prompting} {\n" + "puts $::REPL::result\n" + "}\n" + "} elseif {$::REPL::rc == 1} {\n" + "puts stderr \"Error: $::REPL::result\"\n" + "} elseif {$::REPL::rc == 2} {\n" + "set ::REPL::at_end 1\n" + "}\n" + "set ::REPL::line {}\n" + "}\n" + "}\n" + "}\n" + "if {$::REPL::prompting && $::REPL::li ne \".\\n\"} {puts {}}\n" + "namespace delete ::REPL\n" + "read stdin 0\n" +#elif TCL_REPL==3 + /* using shell's input collection with line editing (if configured) */ + "namespace eval ::REPL {\n" + "variable at_end 0\n" + "variable interactive [now_interactive] 0\n" + "}\n" + "while {!$::REPL::at_end} {\n" + "foreach {::REPL::group ::REPL::ready} [get_input_line_group] {}\n" + "set ::REPL::trimmed [string trim $::REPL::group]\n" + "if {$::REPL::group eq \"\" && !$::REPL::ready} break\n" + "if {$::REPL::trimmed eq \"\"} continue\n" + "if {!$::REPL::ready && $::REPL::trimmed ne \"\"} {\n" + "throw {NONE} {incomplete input at EOF}\n" + "}\n" + "if {$::REPL::trimmed eq \".\"} break\n" + "set ::REPL::rc [catch {uplevel #0 $::REPL::group} ::REPL::result]\n" + "if {$::REPL::rc == 0} {\n" + "if {$::REPL::result!=\"\" && $::REPL::interactive} {\n" + "puts $::REPL::result\n" + "}\n" + "} elseif {$::REPL::rc == 1} {\n" + "puts stderr \"Error: $::REPL::result\"\n" + "} elseif {$::REPL::rc == 2} {\n" + "set ::REPL::at_end 1\n" + "}\n" + "}\n" + "if {$::REPL::interactive && $::REPL::trimmed ne \".\"} {puts {}}\n" + "namespace delete ::REPL\n" + "read stdin 0\n" +#else + "throw {NONE} {not built for REPL}\n" +#endif + ; /* zREPL */ + DERIVED_METHOD(DotCmdRC, execute, MetaCommand,TclCmd, 4, (ShellExState *psx, char **pzErrMsg, int nArgs, char *azArgs[])){ FILE *out = pExtHelpers->currentOutputFile(psx); @@ -117,80 +243,6 @@ DERIVED_METHOD(DotCmdRC, execute, MetaCommand,TclCmd, 4, } }else{ /* Enter a REPL */ - static const char * const zREPL = -#ifdef TCL_REPL_STDIN_ONLY /* a fallback for debug */ - "set line {}\n" - "while {![eof stdin]} {\n" - "if {$line!=\"\"} {\n" - "puts -nonewline \"> \"\n" - "} else {\n" - "puts -nonewline \"% \"\n" - "}\n" - "flush stdout\n" - "append line [gets stdin]\n" - "if {$line eq \".\"} break\n" - "if {[info complete $line]} {\n" - "if {[catch {uplevel #0 $line} result]} {\n" - "puts stderr \"Error: $result\"\n" - "} elseif {$result!=\"\"} {\n" - "puts $result\n" - "}\n" - "set line {}\n" - "} else {\n" - "append line \\n\n" - "}\n" - "}\n" - "if {$line ne \".\"} {puts {}}\n" - "read stdin 0\n" -#else - "namespace eval ::REPL {\n" - "variable line {}\n" - "variable at_end 0\n" - "variable prompting [now_interactive]\n" - "}\n" - "while {!$::REPL::at_end} {\n" - "if {$::REPL::prompting} {\n" - "if {$::REPL::line!=\"\"} {\n" - "puts -nonewline \"...> \"\n" - "} else {\n" - "puts -nonewline \"tcl% \"\n" - "}\n" - "}\n" - "flush stdout\n" - "set ::REPL::li [get_input_line]\n" - "if {$::REPL::li eq \"\"} {\n" - "set ::REPL::at_end 1\n" - "} elseif {[string trimright $::REPL::li] eq \".\"} {\n" - "if {$::REPL::line ne \"\"} {\n" - "throw {NONE} {incomplete input at EOF}\n" - "}\n" - "set ::REPL::at_end 1\n" - "} else {\n" - "append ::REPL::line $::REPL::li\n" - "if {[string trim $::REPL::line] eq \"\"} {\n" - "set ::REPL::line \"\"\n" - "continue\n" - "}\n" - "if {[info complete $::REPL::line]} {\n" - "set ::REPL::rc [catch {uplevel #0 $::REPL::line} ::REPL::result]\n" - "if {$::REPL::rc == 0} {\n" - "if {$::REPL::result!=\"\" && $::REPL::prompting} {\n" - "puts $::REPL::result\n" - "}\n" - "} elseif {$::REPL::rc == 1} {\n" - "puts stderr \"Error: $::REPL::result\"\n" - "} elseif {$::REPL::rc == 2} {\n" - "set ::REPL::at_end 1\n" - "}\n" - "set ::REPL::line {}\n" - "}\n" - "}\n" - "}\n" - "if {$::REPL::prompting && $::REPL::li ne \".\\n\"} {puts {}}\n" - "namespace delete ::REPL\n" - "read stdin 0\n" -#endif - ; //... ToDo: Get line editing working here. Reuse shell's line entry. rc = Tcl_Eval(getInterp(ptc), zREPL); clearerr(stdin); /* Cure issue where stdin gets stuck after keyboard EOF. */ } @@ -250,6 +302,7 @@ static DotCmdRC tclRunScript(void *pvState, const char *zScript, #define GETLINE_MAXLEN 1000 +#if TCL_REPL==2 /* C implementation of TCL proc, get_input_line */ static int getInputLine(void *pvSS, Tcl_Interp *interp, int nArgs, const char *azArgs[]){ @@ -268,6 +321,61 @@ static int getInputLine(void *pvSS, Tcl_Interp *interp, return TCL_ERROR; } } +#endif + +#if TCL_REPL==3 +/* C implementation of TCL proc, get_input_line_group + * This routine returns a 2 element list consisting of: + * the collected input lines, joined with "\n", as a string + * and + * the line group status, as an integer. + * The status is either 0, meaning input EOF was encountered, + * or 1, meaning the input is a complete TCL line group. + * There are only these return combinations: + * { Empty 0 } => no input obtained and no more to be had + * { Other 0 } => input collected, but is invalid TCL + * { Other 1 } => input collected, may be valid TCL + * By design, this combination is never returned: + * { Empty 1 } => no input collected but valid TCL + */ +static int getInputLineGroup(void *pvSS, Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[]){ + if( objc==1 ){ + static Prompts cueTcl = { "tcl% ", " > " }; + ShellExState *psx = (ShellExState *)pvSS; + struct InSource *pis = pExtHelpers->currentInputSource(psx); + int isComplete = 0; + char *zIn = 0; + int isContinuation = 0; + do { + zIn = pExtHelpers->oneInputLine(pis, zIn, isContinuation, &cueTcl); + if( isContinuation ){ + if( zIn ){ + Tcl_AppendResult(interp, "\n", zIn, (char*)0); + isComplete = Tcl_CommandComplete(Tcl_GetStringResult(interp)); + } + }else if( zIn ){ + isComplete = Tcl_CommandComplete(zIn); + Tcl_SetResult(interp, zIn, TCL_VOLATILE); + } + isContinuation = 1; + } while( zIn && !isComplete ); + if( zIn ) pExtHelpers->freeInputLine(zIn); + { + Tcl_Obj *const objv[] = { + Tcl_NewStringObj(Tcl_GetStringResult(interp) , -1), + Tcl_NewIntObj(isComplete) + }; /* These unowned objects go directly into result, becoming owned. */ + Tcl_ResetResult(interp); + Tcl_SetObjResult(interp, Tcl_NewListObj(2, objv)); + } + return TCL_OK; + }else{ + Tcl_SetResult(interp, "too many arguments", TCL_STATIC); + return TCL_ERROR; + } +} +#endif /* C implementation of TCL proc, now_interactive */ static int nowInteractive(void *pvSS, Tcl_Interp *interp, @@ -314,9 +422,7 @@ static int unknownDotDelegate(void *pvSS, Tcl_Interp *interp, } }else{ /* Defer to the TCL-default ::unknown command, or fail here. */ - int haveUnkCmd = (0!=Tcl_FindCommand(interp, UNKNOWN_RENAME, - 0, TCL_GLOBAL_ONLY)); - if( haveUnkCmd ){ + if( 0!=Tcl_FindCommand(interp, UNKNOWN_RENAME, 0, TCL_GLOBAL_ONLY) ){ Tcl_Obj **ppo = sqlite3_malloc((nArgs+1)*sizeof(Tcl_Obj*)); if( ppo==0 ) return TCL_ERROR; ppo[0] = Tcl_NewStringObj(UNKNOWN_RENAME, -1); @@ -327,7 +433,7 @@ static int unknownDotDelegate(void *pvSS, Tcl_Interp *interp, } rc = Tcl_EvalObjv(interp, nArgs, ppo, TCL_EVAL_GLOBAL); for( ia=0; iapvExtensionObject = pudb; } pShExtApi->hookScripting(psx, sqlite3_tclshext_init, &sh); +#if TCL_REPL==2 Tcl_CreateCommand(tclcmd.interp, "get_input_line", getInputLine, psx, 0); +#endif +#if TCL_REPL==3 + Tcl_CreateObjCommand(tclcmd.interp, + "get_input_line_group", getInputLineGroup, psx, 0); +#endif Tcl_CreateCommand(tclcmd.interp, "now_interactive", nowInteractive, psx, 0); Tcl_Eval(tclcmd.interp, "rename unknown "UNKNOWN_RENAME); diff --git a/manifest b/manifest index d79f156e61..1bb31b88f9 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C In\sTCL\sshell\sextension,\sadd\sTCL\scommand\s"udb".\sThis\sgets\sthe\sTCL\senvironment\sto\snear\sparity\swith\spre-extended\sutility,\sexcept\sfor\squery\sresult\sdisplay\s(TBD\ssoon). -D 2022-03-27T03:08:02.170 +C TCL\sextension\smade\sto\suse\sshell's\sline\sinputter\swith\sprompting\sand\sline\sediting/history. +D 2022-03-27T23:33:43.299 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -329,7 +329,7 @@ F ext/misc/showauth.c 732578f0fe4ce42d577e1c86dc89dd14a006ab52 F ext/misc/spellfix.c 94df9bbfa514a563c1484f684a2df3d128a2f7209a84ca3ca100c68a0163e29f F ext/misc/sqlar.c 0ace5d3c10fe736dc584bf1159a36b8e2e60fab309d310cd8a0eecd9036621b6 F ext/misc/stmt.c 35063044a388ead95557e4b84b89c1b93accc2f1c6ddea3f9710e8486a7af94a -F ext/misc/tclshext.c.in b5d5cd1b1ade04ec4ed6346ac72ddb19dc7ba1082386475ca2b140c9cb779f67 +F ext/misc/tclshext.c.in c5f99f74a5f39a216b0e7a4a7f344459a2d0615036e5e404560aab7b91d867cf F ext/misc/templatevtab.c 8a16a91a5ceaccfcbd6aaaa56d46828806e460dd194965b3f77bf38f14b942c4 F ext/misc/totype.c fa4aedeb07f66169005dffa8de3b0a2b621779fd44f85c103228a42afa71853b F ext/misc/uint.c 053fed3bce2e89583afcd4bf804d75d659879bbcedac74d0fa9ed548839a030b @@ -556,8 +556,8 @@ F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c F src/resolve.c ea935b87d6fb36c78b70cdc7b28561dc8f33f2ef37048389549c7b5ef9b0ba5e F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 F src/select.c c366c05e48e836ea04f8ecefb9c1225745dc250c3f01bdb39e9cbb0dc25e3610 -F src/shell.c.in de495287d860d9d26150c09fa3cb6d0de020046618df05b17e74bcc6089d27f6 x -F src/shext_linkage.h 4fd85ce40389c576b66cc2d142cb3b4f4e7c2c8c7fa5a8b6fe7cc7973dbb6495 +F src/shell.c.in c95182f95dc2b1278214812e02fb5131a847851c93b95c548a8a825b87f47aa1 x +F src/shext_linkage.h 68aca955c86a0ef7d1f007a4e776a7e9c17542cfde66195db1d7333695fcef7d F src/sqlite.h.in 2a35f62185eb5e7ecc64a2f68442b538ce9be74f80f28a00abc24837edcf1c17 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h f49e28c25bd941e79794db5415fdf7b202deb3bc072ed6f1ed273d578703684e @@ -1951,8 +1951,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 8402e5e78a83af3ba015bb6ab173350c1b73e04ce37ef933f9a847a7cf5e6157 -R 26d8ade198b7b866e1cc4ce801f7977d +P c9aa76bf88401d193a536bc6576405aaad06681504996916b492962d890bc9e0 +R c4eefad0434fe8c4082bc59b2abace09 U larrybr -Z a49fe933efd58515fcb9ecde174d1264 +Z 5b99de2d0fe36315b33974a1d59f1b54 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 141070bbe4..eb6d19d574 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -c9aa76bf88401d193a536bc6576405aaad06681504996916b492962d890bc9e0 \ No newline at end of file +fbf0eb0d12932513bba4c6a6ef31d9972d704ab38690c806098504a4cd67786d \ No newline at end of file diff --git a/src/shell.c.in b/src/shell.c.in index b9f9b10088..961a380e70 100755 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -469,6 +469,7 @@ static char startupDir[PATH_MAX+1] = {0}; */ static char mainPrompt[20]; /* First line prompt. default: "sqlite> "*/ static char continuePrompt[20]; /* Continuation prompt. default: " ...> " */ +static Prompts shellPrompts = { mainPrompt, continuePrompt }; /* ** Render output like fprintf(). Except, if the output is going to the @@ -798,8 +799,12 @@ static char *local_getline(char *zLine, InSource *pInSrc){ ** routine that can be reused. ** ** The result is stored in space obtained from malloc() and must either -** be freed by the caller or else passed back into this routine via the -** zPrior argument for reuse. +** be freed by the caller, using the same allocator[a], or else passed +** back into this routine via the zPrior argument for reuse. +** +** [a. This function is exposed for use by shell extensions which may +** have no access to "the same allocator". This is why the function +** following this one exists, also exposed to shell extensions. ] ** ** If this function is called until it returns NULL, and the prior return ** has been passed in for resuse, then the caller need/must not free it. @@ -809,11 +814,15 @@ static char *local_getline(char *zLine, InSource *pInSrc){ ** The trailing newline (or its ilk), if any, is trimmed. ** The input line number is adjusted (via delegation or directly.) */ -static char *one_input_line(InSource *pInSrc, char *zPrior, int isContinuation){ +static char *one_input_line(InSource *pInSrc, char *zPrior, + int isContinuation, Prompts *pCue){ if( !INSOURCE_IS_INTERACTIVE(pInSrc) ){ return local_getline(zPrior, pInSrc); }else{ - char *zPrompt = isContinuation ? continuePrompt : mainPrompt; + static Prompts cueDefault = { "$ ","> " }; + const char *zPrompt; + if( pCue==0 ) pCue = &cueDefault; + zPrompt = isContinuation ? pCue->zContinue : pCue->zMain; #if SHELL_USE_LOCAL_GETLINE printf("%s", zPrompt); fflush(stdout); @@ -829,6 +838,11 @@ static char *one_input_line(InSource *pInSrc, char *zPrior, int isContinuation){ } } +/* For use by shell extensions. See footnote [a] to above function. */ +void free_input_line(char *z){ + free(z); +} + /* ** Return the value of a hexadecimal digit. Return -1 if the input ** is not a hex digit. @@ -7890,7 +7904,7 @@ static MetaCommand * findMetaCommand(const char *, ShellExState *, int *); static DotCmdRC runMetaCommand(MetaCommand*, char *[], int na, ShellExState*); static ExtensionHelpers extHelpers = { - 11, + 13, { failIfSafeMode, currentOutputFile, @@ -7902,6 +7916,8 @@ static ExtensionHelpers extHelpers = { nowInteractive, shellInvokedAs, shellStartupDir, + one_input_line, + free_input_line, sqlite3_enable_load_extension, 0 } @@ -14001,7 +14017,8 @@ static DotCmdRC process_input(ShellInState *psi){ int iStartline = 0; /* starting line number of group */ fflush(psi->out); - zLineInput = one_input_line(psi->pInSource, zLineInput, nGroupLines>0); + zLineInput = one_input_line(psi->pInSource, zLineInput, + nGroupLines>0, &shellPrompts); if( zLineInput==0 ){ bInputEnd = 1; inKind = Eof; @@ -14121,7 +14138,8 @@ static DotCmdRC process_input(ShellInState *psi){ pncLineUse = &ncLineAcc; } /* Read in next line of group, (if available.) */ - zLineInput = one_input_line(psi->pInSource, zLineInput, nGroupLines>0); + zLineInput = one_input_line(psi->pInSource, zLineInput, + nGroupLines>0, &shellPrompts); if( zLineInput==0 ){ bInputEnd = 1; inKind = Eof; @@ -15127,10 +15145,11 @@ int SQLITE_CDECL SHELL_MAIN(int argc, wchar_t **wargv){ clearTempFile(&data); sqlite3_free(data.zEditor); #if SHELL_DYNAMIC_EXTENSION + notify_subscribers(&data, NK_DbAboutToClose, datax.dbShell); + sqlite3_close(datax.dbShell); notify_subscribers(&data, NK_ShutdownImminent, 0); free_all_shext_tracking(&data); subscribe_events(&datax, 0, &datax, NK_Unsubscribe, 0); - sqlite3_close(datax.dbShell); #endif #if !SQLITE_SHELL_IS_UTF8 for(i=0; i