Index: src/func.c ================================================================== --- src/func.c +++ src/func.c @@ -1088,11 +1088,11 @@ /* ** Append to pStr text that is the SQL literal representation of the ** value contained in pValue. */ -void sqlite3QuoteValue(StrAccum *pStr, sqlite3_value *pValue){ +void sqlite3QuoteValue(StrAccum *pStr, sqlite3_value *pValue, int bEscape){ /* As currently implemented, the string must be initially empty. ** we might relax this requirement in the future, but that will ** require enhancements to the implementation. */ assert( pStr!=0 && pStr->nChar==0 ); @@ -1136,20 +1136,119 @@ } break; } case SQLITE_TEXT: { const unsigned char *zArg = sqlite3_value_text(pValue); - sqlite3_str_appendf(pStr, "%Q", zArg); + sqlite3_str_appendf(pStr, bEscape ? "%#Q" : "%Q", zArg); break; } default: { assert( sqlite3_value_type(pValue)==SQLITE_NULL ); sqlite3_str_append(pStr, "NULL", 4); break; } } } + +/* +** Return true if z[] begins with N hexadecimal digits, and write +** a decoding of those digits into *pVal. Or return false if any +** one of the first N characters in z[] is not a hexadecimal digit. +*/ +static int isNHex(const char *z, int N, u32 *pVal){ + int i; + int v = 0; + for(i=0; i0 ){ + memmove(&zOut[j], &zIn[i], n); + j += n; + i += n; + } + if( zIn[i+1]=='\\' ){ + i += 2; + zOut[j++] = '\\'; + }else if( sqlite3Isxdigit(zIn[i+1]) ){ + if( !isNHex(&zIn[i+1], 4, &v) ) goto unistr_error; + i += 5; + j += sqlite3AppendOneUtf8Character(&zOut[j], v); + }else if( zIn[i+1]=='+' ){ + if( !isNHex(&zIn[i+2], 6, &v) ) goto unistr_error; + i += 8; + j += sqlite3AppendOneUtf8Character(&zOut[j], v); + }else if( zIn[i+1]=='u' ){ + if( !isNHex(&zIn[i+2], 4, &v) ) goto unistr_error; + i += 6; + j += sqlite3AppendOneUtf8Character(&zOut[j], v); + }else if( zIn[i+1]=='U' ){ + if( !isNHex(&zIn[i+2], 8, &v) ) goto unistr_error; + i += 10; + j += sqlite3AppendOneUtf8Character(&zOut[j], v); + }else{ + goto unistr_error; + } + } + zOut[j] = 0; + sqlite3_result_text64(context, zOut, j, sqlite3_free, SQLITE_UTF8); + return; + +unistr_error: + sqlite3_free(zOut); + sqlite3_result_error(context, "invalid Unicode escape", -1); + return; +} + /* ** Implementation of the QUOTE() function. ** ** The quote(X) function returns the text of an SQL literal which is the @@ -1156,18 +1255,22 @@ ** value of its argument suitable for inclusion into an SQL statement. ** Strings are surrounded by single-quotes with escapes on interior quotes ** as needed. BLOBs are encoded as hexadecimal literals. Strings with ** embedded NUL characters cannot be represented as string literals in SQL ** and hence the returned string literal is truncated prior to the first NUL. +** +** If sqlite3_user_data() is non-zero, then the UNISTR_QUOTE() function is +** implemented instead. The difference is that UNISTR_QUOTE() uses the +** UNISTR() function to escape control characters. */ static void quoteFunc(sqlite3_context *context, int argc, sqlite3_value **argv){ sqlite3_str str; sqlite3 *db = sqlite3_context_db_handle(context); assert( argc==1 ); UNUSED_PARAMETER(argc); sqlite3StrAccumInit(&str, db, 0, 0, db->aLimit[SQLITE_LIMIT_LENGTH]); - sqlite3QuoteValue(&str,argv[0]); + sqlite3QuoteValue(&str,argv[0],SQLITE_PTR_TO_INT(sqlite3_user_data(context))); sqlite3_result_text(context, sqlite3StrAccumFinish(&str), str.nChar, SQLITE_DYNAMIC); if( str.accError!=SQLITE_OK ){ sqlite3_result_null(context); sqlite3_result_error_code(context, str.accError); @@ -2734,11 +2837,13 @@ VFUNCTION(randomblob, 1, 0, 0, randomBlob ), FUNCTION(nullif, 2, 0, 1, nullifFunc ), DFUNCTION(sqlite_version, 0, 0, 0, versionFunc ), DFUNCTION(sqlite_source_id, 0, 0, 0, sourceidFunc ), FUNCTION(sqlite_log, 2, 0, 0, errlogFunc ), + FUNCTION(unistr, 1, 0, 0, unistrFunc ), FUNCTION(quote, 1, 0, 0, quoteFunc ), + FUNCTION(unistr_quote, 1, 1, 0, quoteFunc ), VFUNCTION(last_insert_rowid, 0, 0, 0, last_insert_rowid), VFUNCTION(changes, 0, 0, 0, changes ), VFUNCTION(total_changes, 0, 0, 0, total_changes ), FUNCTION(replace, 3, 0, 0, replaceFunc ), FUNCTION(zeroblob, 1, 0, 0, zeroblobFunc ), Index: src/printf.c ================================================================== --- src/printf.c +++ src/printf.c @@ -23,21 +23,21 @@ #define etSTRING 5 /* Strings. %s */ #define etDYNSTRING 6 /* Dynamically allocated strings. %z */ #define etPERCENT 7 /* Percent symbol. %% */ #define etCHARX 8 /* Characters. %c */ /* The rest are extensions, not normally found in printf() */ -#define etSQLESCAPE 9 /* Strings with '\'' doubled. %q */ -#define etSQLESCAPE2 10 /* Strings with '\'' doubled and enclosed in '', - NULL pointers replaced by SQL NULL. %Q */ -#define etTOKEN 11 /* a pointer to a Token structure */ -#define etSRCITEM 12 /* a pointer to a SrcItem */ -#define etPOINTER 13 /* The %p conversion */ -#define etSQLESCAPE3 14 /* %w -> Strings with '\"' doubled */ -#define etORDINAL 15 /* %r -> 1st, 2nd, 3rd, 4th, etc. English only */ -#define etDECIMAL 16 /* %d or %u, but not %x, %o */ - -#define etINVALID 17 /* Any unrecognized conversion type */ +#define etESCAPE_q 9 /* Strings with '\'' doubled. %q */ +#define etESCAPE_Q 10 /* Strings with '\'' doubled and enclosed in '', + NULL pointers replaced by SQL NULL. %Q */ +#define etTOKEN 11 /* a pointer to a Token structure */ +#define etSRCITEM 12 /* a pointer to a SrcItem */ +#define etPOINTER 13 /* The %p conversion */ +#define etESCAPE_w 14 /* %w -> Strings with '\"' doubled */ +#define etORDINAL 15 /* %r -> 1st, 2nd, 3rd, 4th, etc. English only */ +#define etDECIMAL 16 /* %d or %u, but not %x, %o */ + +#define etINVALID 17 /* Any unrecognized conversion type */ /* ** An "etByte" is an 8-bit unsigned value. */ @@ -72,13 +72,13 @@ static const et_info fmtinfo[] = { { 'd', 10, 1, etDECIMAL, 0, 0 }, { 's', 0, 4, etSTRING, 0, 0 }, { 'g', 0, 1, etGENERIC, 30, 0 }, { 'z', 0, 4, etDYNSTRING, 0, 0 }, - { 'q', 0, 4, etSQLESCAPE, 0, 0 }, - { 'Q', 0, 4, etSQLESCAPE2, 0, 0 }, - { 'w', 0, 4, etSQLESCAPE3, 0, 0 }, + { 'q', 0, 4, etESCAPE_q, 0, 0 }, + { 'Q', 0, 4, etESCAPE_Q, 0, 0 }, + { 'w', 0, 4, etESCAPE_w, 0, 0 }, { 'c', 0, 0, etCHARX, 0, 0 }, { 'o', 8, 0, etRADIX, 0, 2 }, { 'u', 10, 0, etDECIMAL, 0, 0 }, { 'x', 16, 0, etRADIX, 16, 1 }, { 'X', 16, 0, etRADIX, 0, 4 }, @@ -671,29 +671,11 @@ }else{ buf[0] = 0; } }else{ unsigned int ch = va_arg(ap,unsigned int); - if( ch<0x00080 ){ - buf[0] = ch & 0xff; - length = 1; - }else if( ch<0x00800 ){ - buf[0] = 0xc0 + (u8)((ch>>6)&0x1f); - buf[1] = 0x80 + (u8)(ch & 0x3f); - length = 2; - }else if( ch<0x10000 ){ - buf[0] = 0xe0 + (u8)((ch>>12)&0x0f); - buf[1] = 0x80 + (u8)((ch>>6) & 0x3f); - buf[2] = 0x80 + (u8)(ch & 0x3f); - length = 3; - }else{ - buf[0] = 0xf0 + (u8)((ch>>18) & 0x07); - buf[1] = 0x80 + (u8)((ch>>12) & 0x3f); - buf[2] = 0x80 + (u8)((ch>>6) & 0x3f); - buf[3] = 0x80 + (u8)(ch & 0x3f); - length = 4; - } + length = sqlite3AppendOneUtf8Character(buf, ch); } if( precision>1 ){ i64 nPrior = 1; width -= precision-1; if( width>1 && !flag_leftjustify ){ @@ -769,26 +751,35 @@ /* Adjust width to account for extra bytes in UTF-8 characters */ int ii = length - 1; while( ii>=0 ) if( (bufpt[ii--] & 0xc0)==0x80 ) width++; } break; - case etSQLESCAPE: /* %q: Escape ' characters */ - case etSQLESCAPE2: /* %Q: Escape ' and enclose in '...' */ - case etSQLESCAPE3: { /* %w: Escape " characters */ + case etESCAPE_q: /* %q: Escape ' characters */ + case etESCAPE_Q: /* %Q: Escape ' and enclose in '...' */ + case etESCAPE_w: { /* %w: Escape " characters */ i64 i, j, k, n; - int needQuote, isnull; + int needQuote = 0; char ch; - char q = ((xtype==etSQLESCAPE3)?'"':'\''); /* Quote character */ char *escarg; + char q; if( bArgList ){ escarg = getTextArg(pArgList); }else{ escarg = va_arg(ap,char*); } - isnull = escarg==0; - if( isnull ) escarg = (xtype==etSQLESCAPE2 ? "NULL" : "(NULL)"); + if( escarg==0 ){ + escarg = (xtype==etESCAPE_Q ? "NULL" : "(NULL)"); + }else if( xtype==etESCAPE_Q ){ + needQuote = 1; + } + if( xtype==etESCAPE_w ){ + q = '"'; + flag_alternateform = 0; + }else{ + q = '\''; + } /* For %q, %Q, and %w, the precision is the number of bytes (or ** characters if the ! flags is present) to use from the input. ** Because of the extra quoting characters inserted, the number ** of output characters may be larger than the precision. */ @@ -797,26 +788,77 @@ if( ch==q ) n++; if( flag_altform2 && (ch&0xc0)==0xc0 ){ while( (escarg[i+1]&0xc0)==0x80 ){ i++; } } } - needQuote = !isnull && xtype==etSQLESCAPE2; + if( flag_alternateform ){ + /* For %#q, do unistr()-style backslash escapes for + ** all control characters, and for backslash itself. + ** For %#Q, do the same but only if there is at least + ** one control character. */ + u32 nBack = 0; + u32 nCtrl = 0; + for(k=0; ketBUFSIZE ){ bufpt = zExtra = printfTempBuf(pAccum, n); if( bufpt==0 ) return; }else{ bufpt = buf; } j = 0; - if( needQuote ) bufpt[j++] = q; + if( needQuote ){ + if( needQuote==2 ){ + memcpy(&bufpt[j], "unistr('", 8); + j += 8; + }else{ + bufpt[j++] = '\''; + } + } k = i; - for(i=0; i=0x10 ? '1' : '0'; + bufpt[j++] = "0123456789abcdef"[ch&0xf]; + } + } + }else{ + for(i=0; iout; sqlite3_fsetmode(out, _O_BINARY); if( z==0 ) return; - for(i=0; (c = z[i])!=0 && c!='\''; i++){} - if( c==0 ){ + for(i=0; (c = z[i])!=0; i++){ + if( c=='\'' ){ needDblQuote = 1; } + if( c>0x1f ) continue; + if( c=='\t' || c=='\n' ) continue; + if( c=='\r' && z[i+1]=='\n' ) continue; + needUnistr = 1; + break; + } + if( (needDblQuote==0 && needUnistr==0) + || (needDblQuote==0 && p->eEscMode==SHELL_ESC_OFF) + ){ sqlite3_fprintf(out, "'%s'",z); + }else if( p->eEscMode==SHELL_ESC_OFF ){ + char *zEncoded = sqlite3_mprintf("%Q", z); + sqlite3_fputs(zEncoded, out); + sqlite3_free(zEncoded); }else{ - sqlite3_fputs("'", out); + if( needUnistr ){ + sqlite3_fputs("unistr('", out); + }else{ + sqlite3_fputs("'", out); + } while( *z ){ - for(i=0; (c = z[i])!=0 && c!='\''; i++){} - if( c=='\'' ) i++; + for(i=0; (c = z[i])!=0; i++){ + if( c=='\'' ) break; + if( c>0x1f ) continue; + if( c=='\t' || c=='\n' ) continue; + if( c=='\r' && z[i+1]=='\n' ) continue; + break; + } if( i ){ sqlite3_fprintf(out, "%.*s", i, z); z += i; } + if( c==0 ) break; if( c=='\'' ){ - sqlite3_fputs("'", out); - continue; - } - if( c==0 ){ - break; + sqlite3_fputs("''", out); + }else{ + sqlite3_fprintf(out, "\\u%04x", c); } z++; } - sqlite3_fputs("'", out); + if( needUnistr ){ + sqlite3_fputs("')", out); + }else{ + sqlite3_fputs("'", out); + } } setCrlfMode(p); } /* @@ -1948,65 +1973,19 @@ ** ** This is like output_quoted_string() but with the addition of the \r\n ** escape mechanism. */ static void output_quoted_escaped_string(ShellState *p, const char *z){ - int i; - char c; - FILE *out = p->out; - sqlite3_fsetmode(out, _O_BINARY); - for(i=0; (c = z[i])!=0 && c!='\'' && c!='\n' && c!='\r'; i++){} - if( c==0 ){ - sqlite3_fprintf(out, "'%s'",z); + char *zEscaped; + sqlite3_fsetmode(p->out, _O_BINARY); + if( p->eEscMode==SHELL_ESC_OFF ){ + zEscaped = sqlite3_mprintf("%Q", z); }else{ - const char *zNL = 0; - const char *zCR = 0; - int nNL = 0; - int nCR = 0; - char zBuf1[20], zBuf2[20]; - for(i=0; z[i]; i++){ - if( z[i]=='\n' ) nNL++; - if( z[i]=='\r' ) nCR++; - } - if( nNL ){ - sqlite3_fputs("replace(", out); - zNL = unused_string(z, "\\n", "\\012", zBuf1); - } - if( nCR ){ - sqlite3_fputs("replace(", out); - zCR = unused_string(z, "\\r", "\\015", zBuf2); - } - sqlite3_fputs("'", out); - while( *z ){ - for(i=0; (c = z[i])!=0 && c!='\n' && c!='\r' && c!='\''; i++){} - if( c=='\'' ) i++; - if( i ){ - sqlite3_fprintf(out, "%.*s", i, z); - z += i; - } - if( c=='\'' ){ - sqlite3_fputs("'", out); - continue; - } - if( c==0 ){ - break; - } - z++; - if( c=='\n' ){ - sqlite3_fputs(zNL, out); - continue; - } - sqlite3_fputs(zCR, out); - } - sqlite3_fputs("'", out); - if( nCR ){ - sqlite3_fprintf(out, ",'%s',char(13))", zCR); - } - if( nNL ){ - sqlite3_fprintf(out, ",'%s',char(10))", zNL); - } - } + zEscaped = sqlite3_mprintf("%#Q", z); + } + sqlite3_fputs(zEscaped, p->out); + sqlite3_free(zEscaped); setCrlfMode(p); } /* ** Find earliest of chars within s specified in zAny. @@ -2151,10 +2130,97 @@ sqlite3_fputs(ace+1, out); } } sqlite3_fputs(zq, out); } + +/* +** Escape the input string if it is needed and in accordance with +** eEscMode. +** +** Escaping is needed if the string contains any control characters +** other than \t, \n, and \r\n +** +** If no escaping is needed (the common case) then set *ppFree to NULL +** and return the original string. If escapingn is needed, write the +** escaped string into memory obtained from sqlite3_malloc64() or the +** equivalent, and return the new string and set *ppFree to the new string +** as well. +** +** The caller is responsible for freeing *ppFree if it is non-NULL in order +** to reclaim memory. +*/ +static const char *escapeOutput( + ShellState *p, + const char *zInX, + char **ppFree +){ + i64 i, j; + i64 nCtrl = 0; + unsigned char *zIn; + unsigned char c; + unsigned char *zOut; + + + /* No escaping if disabled */ + if( p->eEscMode==SHELL_ESC_OFF ){ + *ppFree = 0; + return zInX; + } + + /* Count the number of control characters in the string. */ + zIn = (unsigned char*)zInX; + for(i=0; (c = zIn[i])!=0; i++){ + if( c<=0x1f + && c!='\t' + && c!='\n' + && (c!='\r' || zIn[i+1]!='\n') + ){ + nCtrl++; + } + } + if( nCtrl==0 ){ + *ppFree = 0; + return zInX; + } + if( p->eEscMode==SHELL_ESC_SYMBOL ) nCtrl *= 2; + zOut = sqlite3_malloc64( i + nCtrl + 1 ); + shell_check_oom(zOut); + for(i=j=0; (c = zIn[i])!=0; i++){ + if( c>0x1f + || c=='\t' + || c=='\n' + || (c=='\r' && zIn[i+1]=='\n') + ){ + continue; + } + if( i>0 ){ + memcpy(&zOut[j], zIn, i); + j += i; + } + zIn += i+1; + i = -1; + switch( p->eEscMode ){ + case SHELL_ESC_SYMBOL: + zOut[j++] = 0xe2; + zOut[j++] = 0x90; + zOut[j++] = 0x80+c; + break; + case SHELL_ESC_ASCII: + zOut[j++] = '^'; + zOut[j++] = 0x40+c; + break; + } + } + if( i>0 ){ + memcpy(&zOut[j], zIn, i); + j += i; + } + zOut[j] = 0; + *ppFree = (char*)zOut; + return (char*)zOut; +} /* ** Output the given string with characters that are special to ** HTML escaped. */ @@ -2595,12 +2661,16 @@ int len = strlen30(azCol[i] ? azCol[i] : ""); if( len>w ) w = len; } if( p->cnt++>0 ) sqlite3_fputs(p->rowSeparator, p->out); for(i=0; inullValue, &pFree); sqlite3_fprintf(p->out, "%*s = %s%s", w, azCol[i], - azArg[i] ? azArg[i] : p->nullValue, p->rowSeparator); + pDisplay, p->rowSeparator); + if( pFree ) sqlite3_free(pFree); } break; } case MODE_ScanExp: case MODE_Explain: { @@ -2728,19 +2798,27 @@ break; } case MODE_List: { if( p->cnt++==0 && p->showHeader ){ for(i=0; iout, "%s%s", azCol[i], + char *z = azCol[i]; + char *pFree; + const char *zOut = escapeOutput(p, z, &pFree); + sqlite3_fprintf(p->out, "%s%s", zOut, i==nArg-1 ? p->rowSeparator : p->colSeparator); + if( pFree ) sqlite3_free(pFree); } } if( azArg==0 ) break; for(i=0; inullValue; - sqlite3_fputs(z, p->out); + zOut = escapeOutput(p, z, &pFree); + sqlite3_fputs(zOut, p->out); + if( pFree ) sqlite3_free(pFree); sqlite3_fputs((icolSeparator : p->rowSeparator, p->out); } break; } case MODE_Www: @@ -3855,10 +3933,11 @@ ** from malloc()) of that first line, which caller should free sometime. ** Write anything to display on the next line into *pzTail. If this is ** the last line, write a NULL into *pzTail. (*pzTail is not allocated.) */ static char *translateForDisplayAndDup( + ShellState *p, /* To access current settings */ const unsigned char *z, /* Input text to be transformed */ const unsigned char **pzTail, /* OUT: Tail of the input for next line */ int mxWidth, /* Max width. 0 means no limit */ u8 bWordWrap /* If true, avoid breaking mid-word */ ){ @@ -3889,19 +3968,22 @@ n++; i++; j++; continue; } + if( c==0 || c=='\n' || (c=='\r' && z[i+1]=='\n') ) break; if( c=='\t' ){ do{ n++; j++; }while( (n&7)!=0 && n=mxWidth && bWordWrap ){ /* Perhaps try to back up to a better place to break the line */ for(k=i; k>i/2; k--){ if( isspace(z[k-1]) ) break; @@ -3944,23 +4026,48 @@ if( c>=' ' ){ n++; zOut[j++] = z[i++]; continue; } + if( c==0 ) break; if( z[i]=='\t' ){ do{ n++; zOut[j++] = ' '; }while( (n&7)!=0 && neEscMode ){ + case SHELL_ESC_SYMBOL: + zOut[j++] = 0xe2; + zOut[j++] = 0x90; + zOut[j++] = 0x80 + c; + break; + case SHELL_ESC_ASCII: + zOut[j++] = '^'; + zOut[j++] = 0x40 + c; + break; + case SHELL_ESC_OFF: + zOut[j++] = c; + break; + } + i++; } zOut[j] = 0; return (char*)zOut; } + +/* Return true if the text string z[] contains characters that need +** unistr() escaping. +*/ +static int needUnistr(const unsigned char *z){ + unsigned char c; + if( z==0 ) return 0; + while( (c = *z)>0x1f || c=='\t' || c=='\n' || (c=='\r' && z[1]=='\n') ){ z++; } + return c!=0; +} /* Extract the value of the i-th current column for pStmt as an SQL literal ** value. Memory is obtained from sqlite3_malloc64() and must be freed by ** the caller. */ @@ -3972,11 +4079,12 @@ case SQLITE_INTEGER: case SQLITE_FLOAT: { return sqlite3_mprintf("%s",sqlite3_column_text(pStmt,i)); } case SQLITE_TEXT: { - return sqlite3_mprintf("%Q",sqlite3_column_text(pStmt,i)); + const unsigned char *zText = sqlite3_column_text(pStmt,i); + return sqlite3_mprintf(needUnistr(zText)?"%#Q":"%Q",zText); } case SQLITE_BLOB: { int j; sqlite3_str *pStr = sqlite3_str_new(0); const unsigned char *a = sqlite3_column_blob(pStmt,i); @@ -4064,11 +4172,11 @@ wx = p->cmOpts.iWrap; } if( wx<0 ) wx = -wx; uz = (const unsigned char*)sqlite3_column_name(pStmt,i); if( uz==0 ) uz = (u8*)""; - azData[i] = translateForDisplayAndDup(uz, &zNotUsed, wx, bw); + azData[i] = translateForDisplayAndDup(p, uz, &zNotUsed, wx, bw); } do{ int useNextLine = bNextLine; bNextLine = 0; if( (nRow+2)*nColumn >= nAlloc ){ @@ -4088,19 +4196,20 @@ if( wx<0 ) wx = -wx; if( useNextLine ){ uz = azNextLine[i]; if( uz==0 ) uz = (u8*)zEmpty; }else if( p->cmOpts.bQuote ){ + assert( azQuoted!=0 ); sqlite3_free(azQuoted[i]); azQuoted[i] = quoted_column(pStmt,i); uz = (const unsigned char*)azQuoted[i]; }else{ uz = (const unsigned char*)sqlite3_column_text(pStmt,i); if( uz==0 ) uz = (u8*)zShowNull; } azData[nRow*nColumn + i] - = translateForDisplayAndDup(uz, &azNextLine[i], wx, bw); + = translateForDisplayAndDup(p, uz, &azNextLine[i], wx, bw); if( azNextLine[i] ){ bNextLine = 1; abRowDiv[nRow-1] = 0; bMultiLineRowExists = 1; } @@ -9777,28 +9886,56 @@ if( c=='m' && cli_strncmp(azArg[0], "mode", n)==0 ){ const char *zMode = 0; const char *zTabname = 0; int i, n2; + int chng = 0; /* 0x01: change to cmopts. 0x02: Any other change */ ColModeOpts cmOpts = ColModeOpts_default; for(i=1; ieEscMode = k; + chng |= 2; + break; + } + } + if( k>=ArraySize(shell_EscModeNames) ){ + sqlite3_fprintf(stderr, "unknown control character escape mode \"%s\"" + " - choices:", zEsc); + for(k=0; kmode==MODE_Column || (p->mode>=MODE_Markdown && p->mode<=MODE_Box) ){ sqlite3_fprintf(p->out, - "current output mode: %s --wrap %d --wordwrap %s --%squote\n", + "current output mode: %s --wrap %d --wordwrap %s " + "--%squote --escape %s\n", modeDescr[p->mode], p->cmOpts.iWrap, p->cmOpts.bWordWrap ? "on" : "off", - p->cmOpts.bQuote ? "" : "no"); + p->cmOpts.bQuote ? "" : "no", + shell_EscModeNames[p->eEscMode] + ); }else{ sqlite3_fprintf(p->out, - "current output mode: %s\n", modeDescr[p->mode]); + "current output mode: %s --escape %s\n", + modeDescr[p->mode], + shell_EscModeNames[p->eEscMode] + ); } + } + if( zMode==0 ){ zMode = modeDescr[p->mode]; + if( (chng&1)==0 ) cmOpts = p->cmOpts; } n2 = strlen30(zMode); if( cli_strncmp(zMode,"lines",n2)==0 ){ p->mode = MODE_Line; sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row); @@ -9864,10 +10011,15 @@ p->mode = MODE_List; sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Tab); }else if( cli_strncmp(zMode,"insert",n2)==0 ){ p->mode = MODE_Insert; set_table_name(p, zTabname ? zTabname : "table"); + if( p->eEscMode==SHELL_ESC_OFF ){ + ShellSetFlag(p, SHFLG_Newlines); + }else{ + ShellClearFlag(p, SHFLG_Newlines); + } }else if( cli_strncmp(zMode,"quote",n2)==0 ){ p->mode = MODE_Quote; sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Comma); sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Row); }else if( cli_strncmp(zMode,"ascii",n2)==0 ){ @@ -12584,10 +12736,11 @@ " -csv set output mode to 'csv'\n" #if !defined(SQLITE_OMIT_DESERIALIZE) " -deserialize open the database using sqlite3_deserialize()\n" #endif " -echo print inputs before execution\n" + " -escape MODE ctrl-char escape mode, one of: symbol, ascii, off\n" " -init FILENAME read/process named file\n" " -[no]header turn headers on or off\n" #if defined(SQLITE_ENABLE_MEMSYS3) || defined(SQLITE_ENABLE_MEMSYS5) " -heap SIZE Size of heap for memsys3 or memsys5\n" #endif @@ -13017,10 +13170,13 @@ data.zNonce = strdup(cmdline_option_value(argc, argv, ++i)); }else if( cli_strcmp(z,"-unsafe-testing")==0 ){ ShellSetFlag(&data,SHFLG_TestingMode); }else if( cli_strcmp(z,"-safe")==0 ){ /* no-op - catch this on the second pass */ + }else if( cli_strcmp(z,"-escape")==0 && i+1=ArraySize(shell_EscModeNames) ){ + sqlite3_fprintf(stderr, "unknown control character escape mode \"%s\"" + " - choices:", zEsc); + for(k=0; k>10)&0x003F) + (((c-0x10000)>>10)&0x00C0)); \ *zOut++ = (u8)(0x00DC + ((c>>8)&0x03)); \ *zOut++ = (u8)(c&0x00FF); \ } \ } + +/* +** Write a single UTF8 character whose value is v into the +** buffer starting at zOut. zOut must be sized to hold at +** least for bytes. Return the number of bytes needed +** to encode the new character. +*/ +int sqlite3AppendOneUtf8Character(char *zOut, u32 v){ + if( v<0x00080 ){ + zOut[0] = (u8)(v & 0xff); + return 1; + } + if( v<0x00800 ){ + zOut[0] = 0xc0 + (u8)((v>>6) & 0x1f); + zOut[1] = 0x80 + (u8)(v & 0x3f); + return 2; + } + if( v<0x10000 ){ + zOut[0] = 0xe0 + (u8)((v>>12) & 0x0f); + zOut[1] = 0x80 + (u8)((v>>6) & 0x3f); + zOut[2] = 0x80 + (u8)(v & 0x3f); + return 3; + } + zOut[0] = 0xf0 + (u8)((v>>18) & 0x07); + zOut[1] = 0x80 + (u8)((v>>12) & 0x3f); + zOut[2] = 0x80 + (u8)((v>>6) & 0x3f); + zOut[3] = 0x80 + (u8)(v & 0x3f); + return 4; +} /* ** Translate a single UTF-8 character. Return the unicode value. ** ** During translation, assume that the byte that zTerm points Index: test/shell1.test ================================================================== --- test/shell1.test +++ test/shell1.test @@ -448,11 +448,11 @@ # list Values delimited by .separator strings # tabs Tab-separated values # tcl TCL list elements do_test shell1-3.13.1 { catchcmd "test.db" ".mode" -} {0 {current output mode: list}} +} {0 {current output mode: list --escape symbol}} do_test shell1-3.13.2 { catchcmd "test.db" ".mode FOO" } {1 {Error: mode should be one of: ascii box column csv html insert json line list markdown qbox quote table tabs tcl}} do_test shell1-3.13.3 { catchcmd "test.db" ".mode csv" ADDED test/shellA.test Index: test/shellA.test ================================================================== --- /dev/null +++ test/shellA.test @@ -0,0 +1,257 @@ +# 2025-02-24 +# +# 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. +# +#*********************************************************************** +# TESTRUNNER: shell +# +# Test cases for the command-line shell - focusing on .mode and +# especially control-character escaping and the --escape option. +# +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set CLI [test_cli_invocation] + +do_execsql_test shellA-1.0 { + CREATE TABLE t1(a INT, x TEXT); + INSERT INTO t1 VALUES + (1, 'line with '' single quote'), + (2, concat(char(0x1b),'[31mVT-100 codes',char(0x1b),'[0m')), + (3, NULL), + (4, 1234), + (5, 568.25), + (6, unistr('new\u000aline')), + (7, unistr('carriage\u000dreturn')), + (8, 'last line'); +} {} + +# Initial verification that the database created correctly +# and that our calls to the CLI are working. +# +do_test shellA-1.2 { + exec {*}$CLI test.db {.mode box} {SELECT * FROM t1;} +} { +┌───┬──────────────────────────┐ +│ a │ x │ +├───┼──────────────────────────┤ +│ 1 │ line with ' single quote │ +├───┼──────────────────────────┤ +│ 2 │ ␛[31mVT-100 codes␛[0m │ +├───┼──────────────────────────┤ +│ 3 │ │ +├───┼──────────────────────────┤ +│ 4 │ 1234 │ +├───┼──────────────────────────┤ +│ 5 │ 568.25 │ +├───┼──────────────────────────┤ +│ 6 │ new │ +│ │ line │ +├───┼──────────────────────────┤ +│ 7 │ carriage␍return │ +├───┼──────────────────────────┤ +│ 8 │ last line │ +└───┴──────────────────────────┘ +} + +# ".mode list" +# +do_test shellA-1.3 { + exec {*}$CLI test.db {SELECT x FROM t1 WHERE a=2;} +} { +␛[31mVT-100 codes␛[0m +} +do_test shellA-1.4 { + exec {*}$CLI test.db --escape symbol {SELECT x FROM t1 WHERE a=2;} +} { +␛[31mVT-100 codes␛[0m +} +do_test shellA-1.5 { + exec {*}$CLI test.db --escape ascii {SELECT x FROM t1 WHERE a=2;} +} { +^[[31mVT-100 codes^[[0m +} +do_test shellA-1.6 { + exec {*}$CLI test.db {.mode list --escape symbol} {SELECT x FROM t1 WHERE a=2;} +} { +␛[31mVT-100 codes␛[0m +} +do_test shellA-1.7 { + exec {*}$CLI test.db {.mode list --escape ascii} {SELECT x FROM t1 WHERE a=2;} +} { +^[[31mVT-100 codes^[[0m +} +do_test shellA-1.8 { + file delete -force out.txt + exec {*}$CLI test.db {.mode list --escape off} {SELECT x FROM t1 WHERE a=7;} \ + >out.txt + set fd [open out.txt rb] + set res [read $fd] + close $fd + string trim $res +} "carriage\rreturn" +do_test shellA-1.9 { + set rc [catch { + exec {*}$CLI test.db {.mode test --escape xyz} + } msg] + lappend rc $msg +} {1 {unknown control character escape mode "xyz" - choices: symbol ascii off}} +do_test shellA-1.10 { + set rc [catch { + exec {*}$CLI --escape abc test.db .q + } msg] + lappend rc $msg +} {1 {unknown control character escape mode "abc" - choices: symbol ascii off}} + +# ".mode quote" +# +do_test shellA-2.1 { + exec {*}$CLI test.db --quote {SELECT a, x FROM t1 WHERE a IN (1,2,6,7,8)} +} { +1,'line with '' single quote' +2,unistr('\u001b[31mVT-100 codes\u001b[0m') +6,'new +line' +7,unistr('carriage\u000dreturn') +8,'last line' +} +do_test shellA-2.2 { + exec {*}$CLI test.db --quote {.mode} +} {current output mode: quote --escape symbol} +do_test shellA-2.3 { + exec {*}$CLI test.db --quote --escape ASCII {.mode} +} {current output mode: quote --escape ascii} +do_test shellA-2.4 { + exec {*}$CLI test.db --quote --escape OFF {.mode} +} {current output mode: quote --escape off} + + +# ".mode line" +# +do_test shellA-3.1 { + exec {*}$CLI test.db --line --escape symbol \ + {SELECT a, x FROM t1 WHERE a IN (1,2,6,7,8)} +} { + a = 1 + x = line with ' single quote + + a = 2 + x = ␛[31mVT-100 codes␛[0m + + a = 6 + x = new +line + + a = 7 + x = carriage␍return + + a = 8 + x = last line +} +do_test shellA-3.2 { + exec {*}$CLI test.db --line --escape ascii \ + {SELECT a, x FROM t1 WHERE a IN (1,2,6,7,8)} +} { + a = 1 + x = line with ' single quote + + a = 2 + x = ^[[31mVT-100 codes^[[0m + + a = 6 + x = new +line + + a = 7 + x = carriage^Mreturn + + a = 8 + x = last line +} + +# ".mode box" +# +do_test shellA-4.1 { + exec {*}$CLI test.db --box --escape ascii \ + {SELECT a, x FROM t1 WHERE a IN (1,2,6,7,8)} +} { +┌───┬──────────────────────────┐ +│ a │ x │ +├───┼──────────────────────────┤ +│ 1 │ line with ' single quote │ +├───┼──────────────────────────┤ +│ 2 │ ^[[31mVT-100 codes^[[0m │ +├───┼──────────────────────────┤ +│ 6 │ new │ +│ │ line │ +├───┼──────────────────────────┤ +│ 7 │ carriage^Mreturn │ +├───┼──────────────────────────┤ +│ 8 │ last line │ +└───┴──────────────────────────┘ +} +do_test shellA-4.2 { + exec {*}$CLI test.db {.mode qbox} {SELECT a, x FROM t1 WHERE a IN (1,2,6,7,8)} +} { +┌───┬───────────────────────────────────────────┐ +│ a │ x │ +├───┼───────────────────────────────────────────┤ +│ 1 │ 'line with '' single quote' │ +├───┼───────────────────────────────────────────┤ +│ 2 │ unistr('\u001b[31mVT-100 codes\u001b[0m') │ +├───┼───────────────────────────────────────────┤ +│ 6 │ 'new │ +│ │ line' │ +├───┼───────────────────────────────────────────┤ +│ 7 │ unistr('carriage\u000dreturn') │ +├───┼───────────────────────────────────────────┤ +│ 8 │ 'last line' │ +└───┴───────────────────────────────────────────┘ +} + +# ".mode insert" +# +do_test shellA-5.1 { + exec {*}$CLI test.db {.mode insert t1 --escape ascii} \ + {SELECT a, x FROM t1 WHERE a IN (1,2,6,7,8)} +} { +INSERT INTO t1 VALUES(1,'line with '' single quote'); +INSERT INTO t1 VALUES(2,unistr('\u001b[31mVT-100 codes\u001b[0m')); +INSERT INTO t1 VALUES(6,unistr('new\u000aline')); +INSERT INTO t1 VALUES(7,unistr('carriage\u000dreturn')); +INSERT INTO t1 VALUES(8,'last line'); +} +do_test shellA-5.2 { + exec {*}$CLI test.db {.mode insert t1 --escape symbol} \ + {SELECT a, x FROM t1 WHERE a IN (1,2,6,7,8)} +} { +INSERT INTO t1 VALUES(1,'line with '' single quote'); +INSERT INTO t1 VALUES(2,unistr('\u001b[31mVT-100 codes\u001b[0m')); +INSERT INTO t1 VALUES(6,unistr('new\u000aline')); +INSERT INTO t1 VALUES(7,unistr('carriage\u000dreturn')); +INSERT INTO t1 VALUES(8,'last line'); +} +do_test shellA-5.3 { + file delete -force out.txt + exec {*}$CLI test.db {.mode insert t1 --escape off} \ + {SELECT a, x FROM t1 WHERE a IN (1,2,6,7,8)} >out.txt + set fd [open out.txt rb] + set res [read $fd] + close $fd + string trim [string map [list \r\n \n] $res] +} " +INSERT INTO t1 VALUES(1,'line with '' single quote'); +INSERT INTO t1 VALUES(2,'\033\13331mVT-100 codes\033\1330m'); +INSERT INTO t1 VALUES(6,'new +line'); +INSERT INTO t1 VALUES(7,'carriage\rreturn'); +INSERT INTO t1 VALUES(8,'last line'); +" + +finish_test