From 30c54a01db1666c66b7c75d37cb2f287f1a2cd00 Mon Sep 17 00:00:00 2001 From: drh Date: Thu, 28 May 2020 23:49:50 +0000 Subject: [PATCH] Progress toward adding new output modes to the CLI: json, table, and markdown. FossilOrigin-Name: 14f55fafec11491e87e6526c72cf85c689d74ba18418a1ae9646586ec206767a --- manifest | 14 ++-- manifest.uuid | 2 +- src/shell.c.in | 197 +++++++++++++++++++++++++++++++++++++++++------ test/shell1.test | 12 +-- 4 files changed, 187 insertions(+), 38 deletions(-) diff --git a/manifest b/manifest index b57275c70f..68a02ab2e3 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Enhance\sthe\s".quote"\smode\sin\sthe\sshell\sso\sthat\sit\shonors\s.separator. -D 2020-05-28T20:37:17.793 +C Progress\stoward\sadding\snew\soutput\smodes\sto\sthe\sCLI:\s\sjson,\stable,\sand\nmarkdown. +D 2020-05-28T23:49:50.023 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -534,7 +534,7 @@ F src/random.c 80f5d666f23feb3e6665a6ce04c7197212a88384 F src/resolve.c c2008519a0654f1e7490e9281ed0397d0f14bb840d81f0b96946248afcbdb25d F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 F src/select.c 39a00a8bc89596dfb37c16afcbb1d33de5085b9963564b58aafe1566d08c0881 -F src/shell.c.in 2bca5f1474b43e7c0c1bcd0537b854c2e8a0fac0de2bde473b8c1e919554dcc6 +F src/shell.c.in d135e500f2c84808f86e8113fd22852af4c89f69305f122d3c529cd698ccb396 F src/sqlite.h.in 74342b41e9d68ff9e56b192009046f8dd0aa2bd76ce1a588f330de614ba61de7 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h 2d1af80082edffd71c6f96f70ad1ce6a4fb46615ad10291fc77fe0dea9ff0197 @@ -1338,7 +1338,7 @@ F test/sharedA.test 49d87ec54ab640fbbc3786ee3c01de94aaa482a3a9f834ad3fe92770eb69 F test/sharedB.test 16cc7178e20965d75278f410943109b77b2e645e F test/shared_err.test 32634e404a3317eeb94abc7a099c556a346fdb8fb3858dbe222a4cbb8926a939 F test/sharedlock.test 5ede3c37439067c43b0198f580fd374ebf15d304 -F test/shell1.test 5bd10014ec494744f5e966a1521334e9d612119a0afcfa5251684a4e1f2ffc66 +F test/shell1.test 1c4713ccec468f9300100d5e1419b414b8dcccc742978ad8942e8bd31d2adc9c F test/shell2.test e242a9912f44f4c23c3d1d802a83e934e84c853b F test/shell3.test ac8c2b744014c3e9a0e26bfd829ab65f00923dc1a91ffd044863e9423cc91494 F test/shell4.test 1c6aef11daaa2d6830acaba3ac9cbec93fbc1c3d5530743a637f39b3987d08ce @@ -1866,7 +1866,7 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 98d4262018a81a9a36dd8beb4b02ff0e75cdcbb8a121d143157ffb37b228d60d -R 6e7a8027469df73da3225b0c75827874 +P b5e33ed537e7d7dcabc9f6dc91d6838e0d1657f323440e09e2e24ffa2ba6141a +R 65dafea1aa0e6dfee5aa69ebe4f5d82f U drh -Z 824af62dceaa57c3d7f60896a599839b +Z 8f810f781bfa2ede3701aac97fce45df diff --git a/manifest.uuid b/manifest.uuid index 6cbf3738cf..7d744d62ce 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -b5e33ed537e7d7dcabc9f6dc91d6838e0d1657f323440e09e2e24ffa2ba6141a \ No newline at end of file +14f55fafec11491e87e6526c72cf85c689d74ba18418a1ae9646586ec206767a \ No newline at end of file diff --git a/src/shell.c.in b/src/shell.c.in index 9f960a745c..4795b832c5 100644 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -1035,18 +1035,6 @@ struct OpenSession { }; #endif -/* -** Shell output mode information from before ".explain on", -** saved so that it can be restored by ".explain off" -*/ -typedef struct SavedModeInfo SavedModeInfo; -struct SavedModeInfo { - int valid; /* Is there legit data in here? */ - int mode; /* Mode prior to ".explain on" */ - int showHeader; /* The ".header" setting prior to ".explain on" */ - int colWidth[100]; /* Column widths prior to ".explain on" */ -}; - typedef struct ExpertInfo ExpertInfo; struct ExpertInfo { sqlite3expert *pExpert; @@ -1202,6 +1190,9 @@ struct ShellState { #define MODE_Ascii 10 /* Use ASCII unit and record separators (0x1F/0x1E) */ #define MODE_Pretty 11 /* Pretty-print schemas */ #define MODE_EQP 12 /* Converts EXPLAIN QUERY PLAN output into a graph */ +#define MODE_Json 13 /* Output JSON */ +#define MODE_Markdown 14 /* Markdown formatting */ +#define MODE_Table 15 /* MySQL-style table formatting */ static const char *modeDescr[] = { "line", @@ -1216,7 +1207,10 @@ static const char *modeDescr[] = { "explain", "ascii", "prettyprint", - "eqp" + "eqp", + "json", + "markdown", + "table" }; /* @@ -1893,6 +1887,43 @@ static int progress_handler(void *pClientData) { } #endif /* SQLITE_OMIT_PROGRESS_CALLBACK */ +/* +** Print N dashes +*/ +static void print_dashes(FILE *out, int N){ + const char zDash[] = "--------------------------------------------------"; + const int nDash = sizeof(zDash) - 1; + while( N>nDash ){ + fputs(zDash, out); + N -= nDash; + } + raw_printf(out, "%.*s", N, zDash); +} + +/* +** Print a markdown or table-style row separator +*/ +static void print_row_separator( + ShellState *p, + int nArg, + const char *zSep +){ + int i; + for(i=0; iactualWidth) ){ + w = p->actualWidth[i]; + if( w<0 ) w = -w; + }else{ + w = 10; + } + fputs(zSep, p->out); + print_dashes(p->out, w+2); + } + fputs(zSep, p->out); + fputs("\n", p->out); +} + /* ** This is the callback routine that the shell ** invokes for each row of a query result. @@ -1923,23 +1954,38 @@ static int shell_callback( } break; } + case MODE_Table: + case MODE_Markdown: case MODE_Explain: case MODE_Column: { static const int aExplainWidths[] = {4, 13, 4, 4, 4, 13, 2, 13}; const int *colWidth; int showHdr; char *rowSep; + char *colSep; + char *rowStart; int nWidth; if( p->cMode==MODE_Column ){ colWidth = p->colWidth; nWidth = ArraySize(p->colWidth); showHdr = p->showHeader; rowSep = p->rowSeparator; - }else{ + colSep = " "; + rowStart = ""; + }else if( p->cMode==MODE_Explain ){ colWidth = aExplainWidths; nWidth = ArraySize(aExplainWidths); showHdr = 1; rowSep = SEP_Row; + colSep = " "; + rowStart = ""; + }else{ + colWidth = p->colWidth; + nWidth = ArraySize(p->colWidth); + showHdr = p->showHeader; + rowSep = " |\n"; + colSep = " | "; + rowStart = "| "; } if( p->cnt++==0 ){ for(i=0; iactualWidth) ){ p->actualWidth[i] = w; } - if( showHdr ){ - utf8_width_print(p->out, w, azCol[i]); - utf8_printf(p->out, "%s", i==nArg-1 ? rowSep : " "); - } } if( showHdr ){ + if( p->cMode==MODE_Table ){ + print_row_separator(p, nArg, "+"); + } + fputs(rowStart, p->out); for(i=0; iactualWidth) ){ @@ -1972,14 +2018,34 @@ static int shell_callback( }else{ w = 10; } - utf8_printf(p->out,"%-*.*s%s",w,w, - "----------------------------------------------------------" - "----------------------------------------------------------", - i==nArg-1 ? rowSep : " "); + utf8_width_print(p->out, w, azCol[i]); + fputs(i==nArg-1 ? rowSep : colSep, p->out); + } + for(i=0; iactualWidth) ){ + w = p->actualWidth[i]; + if( w<0 ) w = -w; + }else{ + w = 10; + } + if( p->cMode==MODE_Table || p->cMode==MODE_Markdown ){ + char *zX = p->cMode==MODE_Markdown ? "|" : "+"; + fputs(zX, p->out); + print_dashes(p->out, w+2); + if( i==nArg-1 ){ + fputs(zX, p->out); + fputs("\n", p->out); + } + }else{ + print_dashes(p->out, w); + fputs(i==nArg-1 ? rowSep : colSep, p->out); + } } } } if( azArg==0 ) break; + fputs(rowStart, p->out); for(i=0; iactualWidth) ){ @@ -1997,7 +2063,7 @@ static int shell_callback( p->iIndent++; } utf8_width_print(p->out, w, azArg[i] ? azArg[i] : p->nullValue); - utf8_printf(p->out, "%s", i==nArg-1 ? rowSep : " "); + utf8_printf(p->out, "%s", i==nArg-1 ? rowSep : colSep); } break; } @@ -2201,6 +2267,50 @@ static int shell_callback( raw_printf(p->out,");\n"); break; } + case MODE_Json: { + if( azArg==0 ) break; + if( p->cnt==0 ){ + fputs("[{", p->out); + }else{ + fputs(",\n{", p->out); + } + p->cnt++; + for(i=0; iout, azCol[i]); + putc(':', p->out); + if( (azArg[i]==0) || (aiType && aiType[i]==SQLITE_NULL) ){ + fputs("null",p->out); + }else if( aiType && aiType[i]==SQLITE_FLOAT ){ + char z[50]; + double r = sqlite3_column_double(p->pStmt, i); + sqlite3_uint64 ur; + memcpy(&ur,&r,sizeof(r)); + if( ur==0x7ff0000000000000LL ){ + raw_printf(p->out, "1e999"); + }else if( ur==0xfff0000000000000LL ){ + raw_printf(p->out, "-1e999"); + }else{ + sqlite3_snprintf(50,z,"%!.20g", r); + raw_printf(p->out, "%s", z); + } + }else if( aiType && aiType[i]==SQLITE_BLOB && p->pStmt ){ + const void *pBlob = sqlite3_column_blob(p->pStmt, i); + int nBlob = sqlite3_column_bytes(p->pStmt, i); + putc('"', p->out); + output_hex_blob(p->out, pBlob, nBlob); + putc('"', p->out); + }else if( aiType && aiType[i]==SQLITE_TEXT ){ + output_c_string(p->out, azArg[i]); + }else{ + utf8_printf(p->out,"%s", azArg[i]); + } + if( iout); + } + } + putc('}', p->out); + break; + } case MODE_Quote: { if( azArg==0 ) break; if( p->cnt==0 && p->showHeader ){ @@ -2890,6 +3000,24 @@ static void bind_prepared_stmt(ShellState *pArg, sqlite3_stmt *pStmt){ sqlite3_finalize(pQ); } +#if 0 +/* +** Run a prepared statement and output the result in one of the +** table-oriented formats, either MODE_Markdown or MODE_Table. +** +** This is different from ordinary exec_prepared_stmt() in that +** it has to run the entire query and gather the results into memory +** first, in order to determine column widths, before providing +** any output. +*/ +static void exec_prepared_stmt_tablemode( + ShellState *pArg, /* Pointer to ShellState */ + sqlite3_stmt *pStmt /* Statment to run */ +){ + +} +#endif + /* ** Run a prepared statement */ @@ -2946,6 +3074,11 @@ static void exec_prepared_stmt( } } while( SQLITE_ROW == rc ); sqlite3_free(pData); + if( pArg->cMode==MODE_Table ){ + print_row_separator(pArg, nCol, "+"); + }else if( pArg->cMode==MODE_Json ){ + fputs("]\n", pArg->out); + } } } } @@ -8224,11 +8357,18 @@ static int do_meta_command(char *zLine, ShellState *p){ p->mode = MODE_Ascii; sqlite3_snprintf(sizeof(p->colSeparator), p->colSeparator, SEP_Unit); sqlite3_snprintf(sizeof(p->rowSeparator), p->rowSeparator, SEP_Record); + }else if( c2=='m' && strncmp(azArg[1],"markdown",n2)==0 ){ + p->mode = MODE_Markdown; + }else if( c2=='t' && strncmp(azArg[1],"table",n2)==0 ){ + p->mode = MODE_Table; + }else if( c2=='j' && strncmp(azArg[1],"json",n2)==0 ){ + p->mode = MODE_Json; }else if( nArg==1 ){ raw_printf(p->out, "current output mode: %s\n", modeDescr[p->mode]); }else{ raw_printf(stderr, "Error: mode should be one of: " - "ascii column csv html insert line list quote tabs tcl\n"); + "ascii column csv html insert json line list markdown " + "quote table tabs tcl\n"); rc = 1; } p->cMode = p->mode; @@ -10238,9 +10378,11 @@ static const char zOptions[] = " -help show this message\n" " -html set output mode to HTML\n" " -interactive force interactive I/O\n" + " -json set output mode to 'json'\n" " -line set output mode to 'line'\n" " -list set output mode to 'list'\n" " -lookaside SIZE N use N entries of SZ bytes for lookaside memory\n" + " -markdown set output mode to 'markdown'\n" #if defined(SQLITE_ENABLE_DESERIALIZE) " -maxsize N maximum size for a --deserialize database\n" #endif @@ -10260,6 +10402,7 @@ static const char zOptions[] = " -sorterref SIZE sorter references threshold size\n" #endif " -stats print memory stats before each finalize\n" + " -table set output mode to 'table'\n" " -version show SQLite version\n" " -vfs NAME use NAME as the default VFS\n" #ifdef SQLITE_ENABLE_VFSTRACE @@ -10661,6 +10804,12 @@ int SQLITE_CDECL wmain(int argc, wchar_t **wargv){ data.mode = MODE_Line; }else if( strcmp(z,"-column")==0 ){ data.mode = MODE_Column; + }else if( strcmp(z,"-json")==0 ){ + data.mode = MODE_Json; + }else if( strcmp(z,"-markdown")==0 ){ + data.mode = MODE_Markdown; + }else if( strcmp(z,"-table")==0 ){ + data.mode = MODE_Table; }else if( strcmp(z,"-csv")==0 ){ data.mode = MODE_Csv; memcpy(data.colSeparator,",",2); diff --git a/test/shell1.test b/test/shell1.test index c142ea7241..a900dc3e1d 100644 --- a/test/shell1.test +++ b/test/shell1.test @@ -199,10 +199,10 @@ do_test shell1-2.2.4 { } {0 {}} do_test shell1-2.2.5 { catchcmd "test.db" ".mode \"insert FOO" -} {1 {Error: mode should be one of: ascii column csv html insert line list quote tabs tcl}} +} {1 {Error: mode should be one of: ascii column csv html insert json line list markdown quote table tabs tcl}} do_test shell1-2.2.6 { catchcmd "test.db" ".mode \'insert FOO" -} {1 {Error: mode should be one of: ascii column csv html insert line list quote tabs tcl}} +} {1 {Error: mode should be one of: ascii column csv html insert json line list markdown quote table tabs tcl}} # check multiple tokens, and quoted tokens do_test shell1-2.3.1 { @@ -230,7 +230,7 @@ do_test shell1-2.3.7 { # check quoted args are unquoted do_test shell1-2.4.1 { catchcmd "test.db" ".mode FOO" -} {1 {Error: mode should be one of: ascii column csv html insert line list quote tabs tcl}} +} {1 {Error: mode should be one of: ascii column csv html insert json line list markdown quote table tabs tcl}} do_test shell1-2.4.2 { catchcmd "test.db" ".mode csv" } {0 {}} @@ -430,7 +430,7 @@ do_test shell1-3.13.1 { } {0 {current output mode: list}} do_test shell1-3.13.2 { catchcmd "test.db" ".mode FOO" -} {1 {Error: mode should be one of: ascii column csv html insert line list quote tabs tcl}} +} {1 {Error: mode should be one of: ascii column csv html insert json line list markdown quote table tabs tcl}} do_test shell1-3.13.3 { catchcmd "test.db" ".mode csv" } {0 {}} @@ -463,10 +463,10 @@ do_test shell1-3.13.11 { # don't allow partial mode type matches do_test shell1-3.13.12 { catchcmd "test.db" ".mode l" -} {1 {Error: mode should be one of: ascii column csv html insert line list quote tabs tcl}} +} {1 {Error: mode should be one of: ascii column csv html insert json line list markdown quote table tabs tcl}} do_test shell1-3.13.13 { catchcmd "test.db" ".mode li" -} {1 {Error: mode should be one of: ascii column csv html insert line list quote tabs tcl}} +} {1 {Error: mode should be one of: ascii column csv html insert json line list markdown quote table tabs tcl}} do_test shell1-3.13.14 { catchcmd "test.db" ".mode lin" } {0 {}}