TCL extension made to use shell's line inputter with prompting and line editing/history.

FossilOrigin-Name: fbf0eb0d12932513bba4c6a6ef31d9972d704ab38690c806098504a4cd67786d
This commit is contained in:
larrybr
2022-03-27 23:33:43 +00:00
parent ccf4a2bc69
commit ef1a2bf778
5 changed files with 237 additions and 98 deletions
+190 -78
View File
@@ -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; ia<nArgs; ++ia ) Tcl_DecrRefCount(ppo[ia]);
return TCL_OK;
return rc;
}else{
/* Fail now (instead of recursing back into this handler.) */
Tcl_AppendResult(interp,
@@ -606,8 +712,14 @@ int sqlite3_tclshext_init(
pShExtLink->pvExtensionObject = 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);
+8 -8
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
c9aa76bf88401d193a536bc6576405aaad06681504996916b492962d890bc9e0
fbf0eb0d12932513bba4c6a6ef31d9972d704ab38690c806098504a4cd67786d
+27 -8
View File
@@ -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<argcToFree; i++) free(argvToFree[i]);
+11 -3
View File
@@ -226,6 +226,11 @@ typedef struct ScriptHooks {
ShellExState *, char **pzErrMsg);
} ScriptHooks;
typedef struct Prompts {
const char *zMain;
const char *zContinue;
} Prompts;
typedef struct ExtensionHelpers {
int helperCount; /* Helper count, not including sentinel */
union {
@@ -242,17 +247,20 @@ typedef struct ExtensionHelpers {
int (*nowInteractive)(ShellExState *p);
const char * (*shellInvokedAs)(void);
const char * (*shellStartupDir)(void);
char * (*oneInputLine)(struct InSource *pInSrc, char *zPrior,
int isContinuation, Prompts *pCue);
void (*freeInputLine)(char *zLine);
int (*enable_load_extension)(sqlite3 *db, int onoff);
void (*sentinel)(void);
} named ;
void (*nameless[10+1])(); /* Same as named but anonymous plus a sentinel. */
void (*nameless[13+1])(); /* Same as named but anonymous plus a sentinel. */
} helpers;
} ExtensionHelpers;
/* This enum is stable excepting that it grows at the end. Members will not
* change value across successive shell versions, except for NK_CountOf. An
* extension which is built to rely upon particular notifications can pass
* an NK_CountOf value upon which it relies to subscribe(...) as nkMin,
* an NK_CountOf value upon which it relies to subscribeEvents(...) as nkMin,
* which call will fail if the hosting shell's NK_CountOf value is lower.
*/
typedef enum {
@@ -269,7 +277,7 @@ typedef enum {
* pvSubject is the newly set .dbUser value. */
NK_DbUserVanishing, /* Current ShellExState .dbUser will soon vanish,
* pvSubject is the vanishing .dbUser value. */
NK_DbAboutToClose, /* A maybe-user-visible DB will soon be closed,
NK_DbAboutToClose, /* A ShellExState-visible DB will soon be closed,
* pvSubject is the sqlite3 pointer soon to close. */
NK_CountOf /* Present count of preceding members (evolves) */
} NoticeKind;