From: larrybr Date: Sun, 30 Jan 2022 01:21:12 +0000 (+0000) Subject: Enhance shell parser for multi-line dot-commands X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=c8a1bd95674cdf1fd126b29c2de5e0d42a971336;p=thirdparty%2Fsqlite.git Enhance shell parser for multi-line dot-commands FossilOrigin-Name: 5ed528e27b84466f165c0af52028242d95cc54dc53d6bb4d7afcbb081e6e11de --- diff --git a/manifest b/manifest index 9d76dc6f89..f6e3c939b3 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Take\sCLI\sinput\sredirect\srecursion\slimit\sfrom\strunk -D 2022-01-24T07:11:25.201 +C Enhance\sshell\sparser\sfor\smulti-line\sdot-commands +D 2022-01-30T01:21:12.906 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -553,7 +553,7 @@ F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c F src/resolve.c 359bc0e445d427583d2ab6110433a5dc777f64a0ecdf8d24826d8b475233ead9 F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 F src/select.c ab5717255420972e69b9b9ce4d1c4730fe82cfbdc14b7743e389a8bdb79ca027 -F src/shell.c.in a97789adf08a352a98e8de2e225253e02b06535301c7e8c0d430fee15828ceaa +F src/shell.c.in 7763f8af2cf54a9fef0f6893dfeb64551d4bcaa3ca4e0753f86954d4795c5219 F src/sqlite.h.in 31c2c8d737814369bd3b71f3849c4a97ef7ede0aa3ce976ecb11632fa5f1f863 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h 5d54cf13d3406d8eb65d921a0d3c349de6126b732e695e79ecd4830ce86b4f8a @@ -1387,8 +1387,8 @@ F test/shared_err.test 32634e404a3317eeb94abc7a099c556a346fdb8fb3858dbe222a4cbb8 F test/sharedlock.test 5ede3c37439067c43b0198f580fd374ebf15d304 F test/shell1.test 70f46b5d07776a107335c3c2c9cbd0431d44637bfeae1f6b9ded5e33b4c7c0bf F test/shell2.test f00a0501c00583cbc46f7510e1d713366326b2b3e63d06d15937284171a8787c -F test/shell3.test cb4b835a901742c9719437a89171172ecc4a8823ad97349af8e4e841e6f82566 -F test/shell4.test d817597bb7f11f97ea86e4ccad0480c21183bcdc2083cd9de05a11303d9ee577 +F test/shell3.test 3fed756f9e254d638b012fc018f5125e2f15bf79a824a4f474405b3aad9f0fb8 +F test/shell4.test 823b84d39d4dc7a78b7570342b7e43dc32805fa8ee92e5b40a89775c3170dac1 F test/shell5.test b85069bfcf3159b225228629ab2c3e69aa923d098fea8ea074b5dcd743522e2c F test/shell6.test 1ceb51b2678c472ba6cf1e5da96679ce8347889fe2c3bf93a0e0fa73f00b00d3 F test/shell7.test 115132f66d0463417f408562cc2cf534f6bbc6d83a6d50f0072a9eb171bae97f @@ -1941,8 +1941,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 2f2f1aaed691ba31ba70577012f785aae1f4a53ac7582bf30d26fbbec1eb3f3c 7a073931752d16ba71f1a606091461e427ca5ccf4d135d3c5141bfdd4e67e2d5 -R 2171c849337bd842c8b5ea7647d1f610 +P 5e7020441514d1febdd6d97d6c5be4429f65310e9bcde2eb50c071aa9459a333 +R 693114489bf2df60d1eddd83c00667db U larrybr -Z 86bfffa52bc0d70bd85c5a9f759d466b +Z 3baa5035e980892de21c7db56cd26902 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 5dfa7fb4c4..15ae8da434 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -5e7020441514d1febdd6d97d6c5be4429f65310e9bcde2eb50c071aa9459a333 \ No newline at end of file +5ed528e27b84466f165c0af52028242d95cc54dc53d6bb4d7afcbb081e6e11de \ No newline at end of file diff --git a/src/shell.c.in b/src/shell.c.in index 74154decab..49fdd517af 100644 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -629,6 +629,8 @@ static FILE * openChrSource(const char *zFile){ ** to the text. NULL is returned at end of file, or if malloc() ** fails. ** +** The trailing newline (or other line-end chars) are stripped. +** ** If zLine is not NULL then it is a malloced buffer returned from ** a previous call to this routine that may be reused. */ @@ -772,7 +774,14 @@ static char *getline_from( char *zLine, InSource *pSource ){ ** ** 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 (and eventual free by the caller.) +** zPrior argument for reuse. +** +** If this function is called until it returns NULL, and the prior return +** has been passed in for resuse, then the caller need not free it. +** Otherwise, (in case of an early termination of reading from the given +** input), the caller is responsible for freeing a prior, non-NULL return. +** +** The trailing newline (or its ilk), if any, is trimmed. */ static char *one_input_line(InSource *pInSrc, char *zPrior, int isContinuation){ char *zPrompt; @@ -1170,6 +1179,13 @@ struct ShellState { u8 eTraceType; /* SHELL_TRACE_* value for type of trace */ u8 bSafeMode; /* True to prohibit unsafe operations */ u8 bSafeModePersist; /* The long-term value of bSafeMode */ +#ifndef SHELL_OMIT_EXTENDED_PARSING + u8 bExtendedDotCmds; /* True if dot-command parsing extensions enabled */ +# define SHEXT_PARSING_MASK 1 +# define SHEXT_PARSING(pSS) (pSS->bExtendedDotCmds & SHEXT_PARSING_MASK !=0) +#else +# define SHEXT_PARSING(pSS) 0 +#endif unsigned statsOn; /* True to display memory stats before each finalize */ unsigned mEqpLines; /* Mask of veritical lines in the EQP output graph */ int inputNesting; /* Track nesting level of .read and other redirects */ @@ -3644,6 +3660,16 @@ static int expertDotCommand( } #endif /* ifndef SQLITE_OMIT_VIRTUALTABLE */ +/* This saves little code and source volume, but provides a nice breakpoint. +** It is called when input is ready to be run, (or would be run if it was +** not about to dumped as a no-op. For shell # comments, "processable" is a +** slight misnomer.) Someday, a tracing facility may enhance this function's +** output to show where and at what line input has originated. +*/ +static void echo_processable_input(ShellState *p, const char *zDo){ + if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zDo); +} + /* ** Execute a statement or set of statements. Print ** any result rows/columns depending on the current mode @@ -3700,9 +3726,7 @@ static int shell_exec( } /* echo the sql statement if echo on */ - if( pArg && ShellHasFlag(pArg, SHFLG_Echo) ){ - utf8_printf(pArg->out, "%s\n", zStmtSql ? zStmtSql : zSql); - } + if( pArg ) echo_processable_input( pArg, zStmtSql ? zStmtSql : zSql); /* Show the EXPLAIN QUERY PLAN if .eqp is on */ if( pArg && pArg->autoEQP && sqlite3_stmt_isexplain(pStmt)==0 ){ @@ -4310,6 +4334,10 @@ static const char *(azHelp[]) = { " Any other argument is a LIKE pattern for tables to hash", #ifndef SQLITE_NOHAVE_SYSTEM ".shell CMD ARGS... Run CMD ARGS... in a system shell", +#endif +#ifndef SHELL_OMIT_EXTENSIONS + ".shxopts ?SIGNED_OPTS? Show or alter shell extension options", + " Run without arguments to see their self-descriptive names", #endif ".show Show the current values for various settings", ".stats ?ARG? Show stats or turn stats on or off", @@ -4437,7 +4465,7 @@ static int showHelp(FILE *out, const char *zPattern){ if( azHelpText[i][0]=='.' ) j = i; if( sqlite3_strlike(zPat, azHelpText[i], 0)==0 ){ utf8_printf(out, "%s\n", azHelpText[j]); - while( j1 ){ + for( ia=1; iabExtendedDotCmds |= 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{ + for( io=0; iobExtendedDotCmds&shopts[io].mask; + raw_printf(p->out, " %8s: %2X (-shxopts %02X)\n", shopts[io].name, m, 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; @@ -7762,10 +7831,14 @@ static int execute_variables(char *azArg[], int nArg, ShellState *p){ if( rc==SQLITE_ROW ){ const unsigned char *zValue = sqlite3_column_text(pStmt, 0); int nb = sqlite3_column_bytes(pStmt, 0); - /* If it's empty, just silently pretend to execute it. */ - if( nb==0 ) continue; - while( IsSpace(zValue[nb-1]) ) --nb; + while( nb>0 && IsSpace(zValue[nb-1]) ) --nb; if( nb>0 ){ + /* The trailing newline (or some other placeholder) is important + * because it or some other character is likely to be put in its + * place during process_input() line or group handling, along + * with a terminating NUL character. Without it, the NULL could + * land past the end of the allocation made at this next line. + */ char *zSubmit = sqlite3_mprintf( "%.*s\n", nb, zValue ); InSource inSourceDivert = {0, zSubmit, 0, azArg[ia], p->pInSource }; @@ -7792,7 +7865,7 @@ static int execute_variables(char *azArg[], int nArg, ShellState *p){ } } sqlite3_finalize(pStmt); - return nErrors>0; + return (rc==2)? 2 : nErrors>0; } /* @@ -9384,7 +9457,7 @@ static int do_meta_command(char *zLine, ShellState *p){ sqlite3_free(zFile); }else - if( c=='p' && n>=3 && strncmp(azArg[0], "parameter", n)==0 ){ + if( c=='p' && n>=2 && strncmp(azArg[0], "parameter", n)==0 ){ open_db(p,0); if( nArg<=1 ) goto parameter_syntax_error; @@ -10344,7 +10417,11 @@ static int do_meta_command(char *zLine, ShellState *p){ utf8_printf(p->out, "%12.12s: %s\n", "filename", p->pAuxDb->zDbFilename ? p->pAuxDb->zDbFilename : ""); }else - +#ifndef SHELL_OMIT_EXTENSIONS + if( c=='s' && strncmp(azArg[0], "shxopts", n)==0 ){ + rc = shxoptsCommand(azArg, nArg, p, 0); + }else +#endif if( c=='s' && strncmp(azArg[0], "stats", n)==0 ){ if( nArg==2 ){ if( strcmp(azArg[1],"stmt")==0 ){ @@ -11065,19 +11142,19 @@ static QuickScanState quickscan(char *zLine, QuickScanState qss){ } /* -** Return TRUE if the line typed in is an SQL command terminator other -** than a semi-colon. The SQL Server style "go" command is understood -** as is the Oracle "/". +** If the line typed in is an SQL command terminator other than ';', +** return a pointer to the terminator. Otherwise return 0. +** The SQL Server style "go" command and Oracle "/" are understood. */ -static int line_is_command_terminator(char *zLine){ +static char *line_is_command_terminator(char *zLine){ + int iSkip = 0; while( IsSpace(zLine[0]) ){ zLine++; }; if( zLine[0]=='/' ) - zLine += 1; /* Oracle */ + iSkip = 1; /* Oracle */ else if ( ToLower(zLine[0])=='g' && ToLower(zLine[1])=='o' ) - zLine += 2; /* SQL Server */ - else - return 0; - return quickscan(zLine,QSS_Start)==QSS_Start; + iSkip = 2; /* SQL Server */ + if( iSkip>0 && quickscan(zLine+iSkip,QSS_Start)==QSS_Start ) return zLine; + else return 0; } /* @@ -11144,125 +11221,418 @@ static int runOneSqlLine(ShellState *p, char *zSql, int bAltIn, int startline){ return 0; } +#ifndef SHELL_OMIT_EXTENDED_PARSING +/* Resumable line classsifier for dot-commands +** +** Determines if a dot-command is open, having either an unclosed +** quoted argument or an escape sequence opener ('\') at its end. +** +** The FSM design/behavior assumes/requires that a terminating '\' +** is not part of the character sequence being classified -- that +** it represents an escaped newline which is removed as physical +** lines are spliced to accumulate logical lines. +** +** The line or added line-portion is passed as zCmd. +** The pScanState pointer must reference an (opaque) DCmd_ScanState, +** which must be set to DCSS_Start to initialize the scanner state. +** Resumed scanning should always be done with zCmd logically just +** past the last non-0 char of the text previously passed in, with +** any previously scanned, trailing newline escape first trimmed. +** Returns are: 0 => not open (aka complete), 1 => is open (incomplete) +** The following macros may be applied to the scan state: +*/ +#define DCSS_InDarkArg(dcss) ((dcss)&argPosMask==inDqArg) +#define DCSS_EndEscaped(dcss) (((dcss)&endEscaped)!=0) +#define DCSS_IsOpen(dcss) (((dcss) & isOpenMask)!=0) +typedef enum { + DCSS_Start = 0, + twixtArgs = 0, inSqArg = 1, inDarkArg = 2, inDqArg = 3, /* ordered */ + endEscaped = 4, /* bit used */ + argPosMask = 3, /* bits used */ + isOpenMask = 1|4 /* bit test */ +} DCmd_ScanState; + +static int dot_command_open(char *zCmd, DCmd_ScanState *pScanState){ + DCmd_ScanState ss = *pScanState & ~endEscaped; + char c = (ss&isOpenMask)? 1 : *zCmd++; + while( c!=0 ){ + switch( ss ){ + twixt: + case twixtArgs: + while( IsSpace(c) ){ + if( (c=*zCmd++)==0 ) goto atEnd; + } + switch( c ){ + case '\\': + if( *zCmd==0 ){ + ss |= endEscaped; + goto atEnd; + }else goto inDark; + case '\'': ss = inSqArg; goto inSq; + case '"': ss = inDqArg; goto inDq; + default: ss = inDarkArg; goto inDark; + } + inSq: + case inSqArg: + while( (c=*zCmd++)!='\'' ){ + if( c==0 ) goto atEnd; + if( c=='\\' && *zCmd==0 ){ + ss |= endEscaped; + goto atEnd; + } + } + ss = twixtArgs; + c = *zCmd++; + continue; + inDq: + case inDqArg: + do { + if( (c=*zCmd++)==0 ) goto atEnd; + if( c=='\\' ){ + if( (c=*zCmd++)==0 ){ + ss |= endEscaped; + goto atEnd; + } + if( (c=*zCmd++)==0 ) goto atEnd; + } + } while( c!='"' ); + ss = twixtArgs; + c = *zCmd++; + continue; + inDark: + case inDarkArg: + while( !IsSpace(c) ){ + if( c=='\\' && *zCmd==0 ){ + ss |= endEscaped; + goto atEnd; + } + if( (c=*zCmd++)==0 ) goto atEnd; + } + ss = twixtArgs; + c = *zCmd++; + continue; + } + } + atEnd: + *pScanState = ss; + return DCSS_IsOpen(ss); +} +#else +# define dot_command_open(x) +#endif /* !defined(SHELL_OMIT_EXTENDED_PARSING) */ + +/* Utility functions for process_input. */ + +static char *skipWhite( char *z ){ + while( IsSpace(*z) ) ++z; + return z; +} + +static void grow_line_buffer(char **pz, int *pna, int ncNeed){ + if( ncNeed > *pna ){ + *pna += *pna + (*pna>>1) + 100; + *pz = realloc(*pz, *pna); + shell_check_oom(*pz); + } +} /* -** Read input from *in and process it. If *in==0 then input -** is interactive - the user is typing it it. Otherwise, input -** is coming from a file or device. A prompt is issued and history -** is saved only if input is interactive. An interrupt signal will -** cause this routine to exit immediately, unless input is interactive. +** Read input from designated source (p->pInSource) and process it. +** If pInSource==0 then input is interactive - the user is typing it. +** Otherwise, input is coming from a file, stream device or string. +** Prompts issue and history is saved only for interactive input. +** An interrupt signal will cause this routine to exit immediately, +** with "exit demanded" code returned, unless input is interactive. ** -** Return the number of errors. +** Returns: 0 => no errors, 1 => errors>0, 2 => exit demanded. */ static int process_input(ShellState *p){ - char *zLine = 0; /* A single input line */ - char *zSql = 0; /* Accumulated SQL text */ - int nLine; /* Length of current line */ - int nSql = 0; /* Bytes of zSql[] used */ - int nAlloc = 0; /* Allocated zSql[] space */ - int rc; /* Error code */ - int errCnt = 0; /* Number of errors seen */ - int startline = 0; /* Line number for start of current input */ - QuickScanState qss = QSS_Start; /* Accumulated line status (so far) */ - + char *zLineInput = 0; /* a line-at-a-time input buffer or usable result */ + char *zLineAccum = 0; /* accumulation buffer, used for multi-line input */ + /* Above two pointers could be local to the group handling loop, but are + * not so that the number of memory allocations can be reduced. They are + * reused from one incoming group to another, realloc()'ed as needed. */ + int naAccum = 0; /* tracking how big zLineAccum buffer has become */ + /* Some flags for ending the overall group processing loop */ + u8 bErrorBail=0, bInputEnd=0, bExitDemand=0, bInterrupted=0; + /* Flag to affect prompting and interrupt action */ + u8 bInteractive = (p->pInSource==0 && stdin_is_interactive); + int nErrors = 0; /* count of errors during execution or its prep */ + + /* Block overly-recursive or absurdly nested input redirects. */ if( p->inputNesting==MAX_INPUT_NESTING ){ InSource *pInSrc = p->pInSource; + int i; utf8_printf - (stderr, - "Input nesting limit (%d) reached, from line %d of \"%s\",\n", - MAX_INPUT_NESTING, p->lineno-1, pInSrc->zSourceSay); - for( rc=0; rc<3 && (pInSrc=pInSrc->pFrom)!=0; ++rc ) + (stderr, "Input nesting limit (%d) reached, from line %d of \"%s\",\n", + MAX_INPUT_NESTING, p->lineno, pInSrc->zSourceSay); + for( i=0; i<3 && (pInSrc=pInSrc->pFrom)!=0; ++i ) utf8_printf(stderr, " from \"%s\"", pInSrc->zSourceSay); - utf8_printf(stderr, " ...\nCheck recursion.\n"); + utf8_printf(stderr, " ...\nERROR: Check recursion.\n"); return 1; } ++p->inputNesting; p->lineno = 0; - while( errCnt==0 - || !bail_on_error - || (p->pInSource==0 && stdin_is_interactive) ){ + + /* line-group processing loop (per SQL block, dot-command or comment) */ + while( !bErrorBail && !bInputEnd && !bExitDemand && !bInterrupted ){ + int nGroupLines = 0; /* count of lines belonging to this group */ + int ncLineIn = 0; /* how many (non-zero) chars are in zLineInput */ + int ncLineAcc = 0; /* how many (non-zero) chars are in zLineAccum */ + int iLastLine = 0; /* index of last accumulated line start */ + /* Initialize resumable scanner(s). */ + QuickScanState qss = QSS_Start; /* for SQL scan */ +#ifndef SHELL_OMIT_EXTENDED_PARSING + DCmd_ScanState dcScanState = DCSS_Start; /* for dot-command scan */ + int ndcLeadWhite = 0; /* for skip over initial whitespace to . or # */ + char cLineEnd = '\n'; /* May be swallowed or replaced with space. */ +#else +# define ndcLeadWhite 0 /* For legacy parsing, no white before . or # . */ +# define cLineEnd '\n' /* For legacy parsing, this always joins lines. */ +#endif + /* An ordered enum to record kind of incoming line group. Its ordering + * means than a value greater than Comment implies something runnable. + */ + enum { Tbd = 0, Eof, Comment, Sql, Cmd /*, Tcl */ } inKind = Tbd; + /* An enum signifying the group disposition state */ + enum { + Incoming, Runnable, Dumpable, Erroneous, Ignore + } disposition = Incoming; + char **pzLineUse = &zLineInput; /* line to be processed */ + int iStartline = 0; /* starting line number of group */ + fflush(p->out); - zLine = one_input_line(p->pInSource, zLine, nSql>0); - if( zLine==0 ){ - /* End of input */ - if( p->pInSource==0 && stdin_is_interactive ) printf("\n"); - break; - } - if( seenInterrupt ){ - if( p->pInSource!=0 ) break; - seenInterrupt = 0; - } - p->lineno++; - if( QSS_INPLAIN(qss) - && line_is_command_terminator(zLine) - && line_is_complete(zSql, nSql) ){ - memcpy(zLine,";",2); - } - qss = quickscan(zLine, qss); - if( QSS_PLAINWHITE(qss) && nSql==0 ){ - if( ShellHasFlag(p, SHFLG_Echo) ) - printf("%s\n", zLine); - /* Just swallow single-line whitespace */ - qss = QSS_Start; - continue; - } - if( zLine && (zLine[0]=='.' || zLine[0]=='#') && nSql==0 ){ - if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine); - if( zLine[0]=='.' ){ - rc = do_meta_command(zLine, p); - if( rc==2 ){ /* exit requested */ + zLineInput = one_input_line(p->pInSource, zLineInput, nGroupLines>0); + if( zLineInput==0 ){ + bInputEnd = 1; + inKind = Eof; + disposition = Ignore; + if( bInteractive ) printf("\n"); + }else{ + ++nGroupLines; + p->lineno++; + iStartline = p->lineno; + ncLineIn = strlen30(zLineInput); + if( seenInterrupt ){ + if( p->pInSource!=0 ) break; + bInterrupted = 1; /* This will be honored, or not, later. */ + seenInterrupt = 0; + disposition = Dumpable; + } + /* Classify and check for single-line dispositions, prep for more. */ +#ifndef SHELL_OMIT_EXTENDED_PARSING + ndcLeadWhite = (SHEXT_PARSING(p)) + ? skipWhite(zLineInput)-zLineInput + : 0; /* Disallow leading whitespace for . or # in legacy mode. */ +#endif + switch( zLineInput[ndcLeadWhite] ){ + case '.': + inKind = Cmd; + dot_command_open(zLineInput+ndcLeadWhite, &dcScanState); + break; + case '#': + inKind = Comment; + disposition = Dumpable; + break; + default: + { + /* Might be SQL, or a swallowable whole SQL comment. */ + qss = quickscan(zLineInput, qss); + if( QSS_PLAINWHITE(qss) ){ + /* It's either all blank or a whole SQL comment. Swallow it. */ + inKind = Comment; + disposition = Dumpable; + }else{ + /* Something dark, not a # comment or dot-command. Must be SQL. */ + inKind = Sql; + } + } + break; + } /* end classification switch */ + } /* end read/classify initial group input line */ + + /* Here, if not at end of input, the initial line of group is in, and + * it has been scanned and classified. Next, do the processing needed + * to recognize whether the initial line or accumulated group so far + * is complete such that it may be run, and perform joining of more + * lines into the group if it is not so complete. This loop finishes + * with the input group line(s) ready to be run, or if the input ends + * before it is ready, issues an error instead of marking it as ready. + */ + while( disposition==Incoming ){ + /* Check whether more to accumulate, or ready for final disposition. */ + switch( inKind ){ + case Comment: + /* This is almost redundant, but for open SQL comments being closed. */ + disposition = Dumpable; + continue; + case Cmd: + { +#ifndef SHELL_OMIT_EXTENDED_PARSING + if( SHEXT_PARSING(p) ){ + /* It's ready only if has no open argument or escaped newline. */ + int bOpen = DCSS_IsOpen(dcScanState); + int bEscNewline = DCSS_EndEscaped(dcScanState); + switch( bEscNewline<<1 | bOpen ){ + case 0: /* neither */ + /* It's ready to run as-is. */ + disposition = Runnable; + cLineEnd = '\n'; + break; + case 1: /* only an open argument */ + /* Open argument, without escaped newline. + * Newline becomes part of the quoted argument. */ + cLineEnd = '\n'; + break; + case 2: /* only escaped newline */ + /* Escaped newline but otherwise ready. + * Handle these two cases: + * a. The linebreak terminates an unquoted argument + * b. The linebreak follows some whitespace. */ + if( DCSS_InDarkArg(dcScanState) ){ + /* case a, swallow the newline, splicing lines */ + cLineEnd = 0; + }else{ + /* case b, replace the newline with a space. */ + cLineEnd = ' '; + } + break; + case 3: /* both */ + /* Escaped newline within a quoted argument. + * Newline is to be incorporated into the argument. */ + cLineEnd = '\n'; + break; + } + if( bEscNewline ){ + /* Swallow the trailing escape character. */ + (*pzLineUse)[--ncLineIn] = 0; + } + }else +#endif + { + /* In legacy parsing, any dot-command line is deemed ready. */ + assert(cLineEnd=='\n'); + disposition = Runnable; + } + } + break; + case Sql: + { + /* Check to see if it is complete and ready to run. */ + if( QSS_SEMITERM(qss) && sqlite3_complete(*pzLineUse)){ + disposition = Runnable; + }else if( QSS_PLAINWHITE(qss) ){ + /* It's a single-line or multi-line comment. */ + disposition = Runnable; + inKind = Comment; + }else{ + char *zT = line_is_command_terminator(zLineInput); + if( zT!=0 ){ + /* Last line is a lone go or / -- prep for running it. */ + if( nGroupLines>1 ){ + disposition = Runnable; + memcpy(*pzLineUse+iLastLine,";\n",3); + ncLineAcc = iLastLine + 2; + }else{ + /* Unless nothing preceded it, then dump it. */ + disposition = Dumpable; + } + } + } + } + break; + } /* end switch on inKind */ + /* Collect and accumulate more input if not yet a complete group. */ + if( disposition==Incoming ){ + grow_line_buffer(&zLineAccum, &naAccum, ncLineAcc+ncLineIn+2); + if( nGroupLines==1 ){ + /* Copy line just input */ + iLastLine = ncLineAcc; + memcpy(zLineAccum, zLineInput, ncLineIn); + ncLineAcc = ncLineIn; + if( cLineEnd!=0 ) zLineAccum[ncLineAcc++] = cLineEnd; + zLineAccum[ncLineAcc] = 0; + pzLineUse = &zLineAccum; + } + /* Read in next line of group, (if available.) */ + zLineInput = one_input_line(p->pInSource, zLineInput, nGroupLines>0); + if( zLineInput==0 ){ + bInputEnd = 1; + inKind = Eof; + disposition = Erroneous; + if( bInteractive ) printf("\n"); + continue; + } + ++nGroupLines; + p->lineno++; + ncLineIn = strlen30(zLineInput); + /* Scan line just input (if needed) and append to accumulation. */ + switch( inKind ){ + case Cmd: + dot_command_open(zLineInput, &dcScanState); + break; + case Sql: + qss = quickscan(zLineInput, qss); break; - }else if( rc ){ - errCnt++; } + grow_line_buffer(&zLineAccum, &naAccum, ncLineAcc+ncLineIn+2); + iLastLine = ncLineAcc; + memcpy(zLineAccum+ncLineAcc, zLineInput, ncLineIn); + ncLineAcc += ncLineIn; + if( cLineEnd!=0 ) zLineAccum[ncLineAcc++] = cLineEnd; + zLineAccum[ncLineAcc] = 0; + } + } /* end group collection loop */ + /* Here, the group is fully collected or known to be incomplete forever. */ + switch( disposition ){ + case Dumpable: + echo_processable_input(p, *pzLineUse); + break; + case Runnable: + switch( inKind ){ + case Sql: + nErrors += runOneSqlLine(p, *pzLineUse, p->pInSource!=0, iStartline); + if( bail_on_error && nErrors>0 ) bErrorBail = 1; + break; + case Cmd: + { + int rc; + echo_processable_input(p, *pzLineUse); + rc = do_meta_command(*pzLineUse, p); + if( rc==2 ){ /* exit requested */ + bExitDemand = 1; + }else if( rc!=0 ){ + if( bail_on_error ) bErrorBail = 1; + ++nErrors; + } + } + break; + default: + assert(inKind!=Tbd); + break; } - qss = QSS_Start; - continue; - } - /* No single-line dispositions remain; accumulate line(s). */ - nLine = strlen30(zLine); - if( nSql+nLine+2>=nAlloc ){ - /* Grow buffer by half-again increments when big. */ - nAlloc = nSql+(nSql>>1)+nLine+100; - zSql = realloc(zSql, nAlloc); - shell_check_oom(zSql); - } - if( nSql==0 ){ - int i; - for(i=0; zLine[i] && IsSpace(zLine[i]); i++){} - assert( nAlloc>0 && zSql!=0 ); - memcpy(zSql, zLine+i, nLine+1-i); - startline = p->lineno; - nSql = nLine-i; - }else{ - zSql[nSql++] = '\n'; - memcpy(zSql+nSql, zLine, nLine+1); - nSql += nLine; - } - if( nSql && QSS_SEMITERM(qss) && sqlite3_complete(zSql) ){ - errCnt += runOneSqlLine(p, zSql, p->pInSource!=0, startline); - nSql = 0; - if( p->outCount ){ - output_reset(p); - p->outCount = 0; - }else{ - clearTempFile(p); + break; + case Erroneous: + { + const char *zSrc = (p->pInSource!=0) + ? p->pInSource->zSourceSay : ""; + utf8_printf(stderr, "Error: Input incomplete at line %d of \"%s\"\n", + p->lineno, zSrc); + if( bail_on_error ) bErrorBail = 1; + ++nErrors; } - p->bSafeMode = p->bSafeModePersist; - qss = QSS_Start; - }else if( nSql && QSS_PLAINWHITE(qss) ){ - if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zSql); - nSql = 0; - qss = QSS_Start; + break; + case Ignore: + break; + default: assert(0); } - } - if( nSql && QSS_PLAINDARK(qss) ){ - errCnt += runOneSqlLine(p, zSql, p->pInSource!=0, startline); - } - free(zSql); - free(zLine); - --p->inputNesting; - return errCnt>0; + } /* end group consume/prep/(run or dump) loop */ + + /* Cleanup and determine return value based on flags and error count. */ + free(zLineInput); + free(zLineAccum); + + return bErrorBail? 2 : (nErrors>0)? 1 : 0; } /* @@ -11431,6 +11801,7 @@ static const char zOptions[] = " -readonly open the database read-only\n" " -safe enable safe-mode\n" " -separator SEP set output column separator. Default: '|'\n" + " -shxopts BMASK enable shell extensions and options\n" #ifdef SQLITE_ENABLE_SORTER_REFERENCES " -sorterref SIZE sorter references threshold size\n" #endif @@ -11772,6 +12143,10 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ sqlite3MemTraceActivate(stderr); }else if( strcmp(z,"-bail")==0 ){ bail_on_error = 1; +#ifndef SHELL_OMIT_EXTENSIONS + }else if( strcmp(z,"-shxopts")==0 ){ + data.bExtendedDotCmds = (u8)integerValue(argv[++i]); +#endif }else if( strcmp(z,"-nonce")==0 ){ free(data.zNonce); data.zNonce = strdup(argv[++i]); @@ -11926,6 +12301,10 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ ShellSetFlag(&data, SHFLG_Backslash); }else if( strcmp(z,"-bail")==0 ){ /* No-op. The bail_on_error flag should already be set. */ +#ifndef SHELL_OMIT_EXTENSIONS + }else if( strcmp(z,"-shxopts")==0 ){ + i++; /* Handled on first pass. */ +#endif }else if( strcmp(z,"-version")==0 ){ printf("%s %s\n", sqlite3_libversion(), sqlite3_sourceid()); return 0; diff --git a/test/shell3.test b/test/shell3.test index 243da976fa..c9367f7fc1 100644 --- a/test/shell3.test +++ b/test/shell3.test @@ -98,7 +98,7 @@ do_test shell3-2.6 { } {0 {}} do_test shell3-2.7 { catchcmd "foo.db" "CREATE TABLE" -} {1 {Error: near line 1: in prepare, incomplete input (1)}} +} {1 {Error: Input incomplete at line 1 of "stdin"}} #---------------------------------------------------------------------------- diff --git a/test/shell4.test b/test/shell4.test index 107125ed6f..17ee3b8e30 100644 --- a/test/shell4.test +++ b/test/shell4.test @@ -144,8 +144,8 @@ do_test shell4-4.1 { puts $fd ".read t1.txt" close $fd catchcmd ":memory:" ".read t1.txt" -} {1 {Input nesting limit (25) reached, from line 0 of "t1.txt", +} {1 {Input nesting limit (25) reached, from line 1 of "t1.txt", from "t1.txt" from "t1.txt" from "t1.txt" ... -Check recursion.}} +ERROR: Check recursion.}} finish_test