Index: src/blob.c ================================================================== --- src/blob.c +++ src/blob.c @@ -19,12 +19,12 @@ ** or binary data. */ #include "config.h" #include <zlib.h> #include "blob.h" - #if INTERFACE + /* ** A Blob can hold a string or a binary object of arbitrary size. The ** size changes as necessary. */ struct Blob { @@ -49,11 +49,10 @@ ** Seek whence parameter values */ #define BLOB_SEEK_SET 1 #define BLOB_SEEK_CUR 2 #define BLOB_SEEK_END 3 - #endif /* INTERFACE */ /* ** Make sure a blob is initialized */ Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -1098,10 +1098,16 @@ ** argument is true. Ignore unfinalized statements when false. */ void db_close(int reportErrors){ sqlite3_stmt *pStmt; if( g.db==0 ) return; + if(g.interp){ + /* clean up up any query_prepare statements */ + Th_DeleteInterp(g.interp); + g.interp = 0; + } + if( g.fSqlStats ){ int cur, hiwtr; sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur, &hiwtr, 0); fprintf(stderr, "-- LOOKASIDE_USED %10d %10d\n", cur, hiwtr); sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_HIT, &cur, &hiwtr, 0); Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -679,13 +679,13 @@ } void fossil_free(void *p){ free(p); } void *fossil_realloc(void *p, size_t n){ - p = realloc(p, n); - if( p==0 ) fossil_panic("out of memory"); - return p; + void * re = realloc(p, n); + if( re==0 && n>0 ) fossil_panic("out of memory"); + return (n > 0) ? re : 0; } /* ** This function implements a cross-platform "system()" interface. */ @@ -787,22 +787,42 @@ /* ** Look for a command-line option. If present, return a pointer. ** Return NULL if missing. ** ** hasArg==0 means the option is a flag. It is either present or not. -** hasArg==1 means the option has an argument. Return a pointer to the -** argument. +** hasArg==1 means the option has an argument. Return a pointer to +** the argument. If hasArg is 0 and the argument is found then a +** pointer to an empty string is returned (to distinguish from +** NULL). If hasArg==1 and the option lies at the end of the argument +** list, it is treated as if it had not been found. +** +** Note that this function REMOVES any found entry from the args list, +** so calling this twice for the same var will cause NULL to be +** returned after the first time. +** +** zLong may not be NULL but zShort may be. +** +** Options are accepted in these forms, depending on the value of +** hasArg: +** +** hasArg=true: +** -long (hasArg==0) +** -long VALUE (hasArg==1) +** -long=VALUE (hasArg==1) +** -short (hasArg==0) +** -short VALUE (hasArg==1) +** -short=VALUE (hasArg==1) */ const char *find_option(const char *zLong, const char *zShort, int hasArg){ int i; - int nLong; + int nLong, nShort; const char *zReturn = 0; assert( hasArg==0 || hasArg==1 ); nLong = strlen(zLong); + nShort = zShort ? strlen(zShort) : 0; for(i=1; i<g.argc; i++){ char *z; - if (i+hasArg >= g.argc) break; z = g.argv[i]; if( z[0]!='-' ) continue; z++; if( z[0]=='-' ){ if( z[1]==0 ){ @@ -815,20 +835,37 @@ if( hasArg && z[nLong]=='=' ){ zReturn = &z[nLong+1]; remove_from_argv(i, 1); break; }else if( z[nLong]==0 ){ - zReturn = g.argv[i+hasArg]; + if( hasArg ){ + if (i+hasArg >= g.argc) break; + zReturn = g.argv[i+hasArg]; + }else{ + zReturn = ""; + } + remove_from_argv(i, 1+hasArg); + break; + } + }else if( strncmp(z,zShort,nShort)==0 ){ + if( hasArg && z[nShort]=='=' ){ + zReturn = &z[nShort+1]; + remove_from_argv(i, 1); + break; + }else if( z[nShort]==0 ){ + if( hasArg ){ + if (i+hasArg >= g.argc) break; + zReturn = g.argv[i+hasArg]; + }else{ + zReturn = ""; + } remove_from_argv(i, 1+hasArg); break; } - }else if( fossil_strcmp(z,zShort)==0 ){ - zReturn = g.argv[i+hasArg]; - remove_from_argv(i, 1+hasArg); - break; } } + return zReturn; } /* ** Verify that there are no unprocessed command-line options. If Index: src/report.c ================================================================== --- src/report.c +++ src/report.c @@ -81,11 +81,11 @@ blob_appendf(&ril, "</li>\n"); } Th_Store("report_items", blob_str(&ril)); - Th_Render(zScript); + Th_Render(zScript, Th_Render_Flags_DEFAULT); blob_reset(&ril); if( g.thTrace ) Th_Trace("END_REPORTLIST<br />\n", -1); style_footer(); Index: src/style.c ================================================================== --- src/style.c +++ src/style.c @@ -19,11 +19,10 @@ ** */ #include "config.h" #include "style.h" - /* ** Elements of the submenu are collected into the following ** structure and displayed below the main menu by style_header(). ** ** Populate this structure with calls to style_submenu_element() @@ -194,11 +193,11 @@ Th_Store("compiler_name", COMPILER_NAME); if( g.zLogin ){ Th_Store("login", g.zLogin); } if( g.thTrace ) Th_Trace("BEGIN_HEADER_SCRIPT<br />\n", -1); - Th_Render(zHeader); + Th_Render(zHeader, Th_Render_Flags_DEFAULT); if( g.thTrace ) Th_Trace("END_HEADER<br />\n", -1); Th_Unstore("title"); /* Avoid collisions with ticket field names */ cgi_destination(CGI_BODY); g.cgiOutput = 1; headerHasBeenGenerated = 1; @@ -281,11 +280,11 @@ ** the footer will be generating </html> */ style_resolve_href(); zFooter = db_get("footer", (char*)zDefaultFooter); if( g.thTrace ) Th_Trace("BEGIN_FOOTER<br />\n", -1); - Th_Render(zFooter); + Th_Render(zFooter, Th_Render_Flags_DEFAULT); if( g.thTrace ) Th_Trace("END_FOOTER<br />\n", -1); /* Render trace log if TH1 tracing is enabled. */ if( g.thTrace ){ cgi_append_content("<span class=\"thTrace\"><hr />\n", -1); @@ -960,11 +959,11 @@ /* Process through TH1 in order to give an opportunity to substitute ** variables such as $baseurl. */ Th_Store("baseurl", g.zBaseURL); Th_Store("home", g.zTop); - Th_Render(blob_str(&css)); + Th_Render(blob_str(&css), Th_Render_Flags_DEFAULT); /* Tell CGI that the content returned by this page is considered cacheable */ g.isConst = 1; } Index: src/th.c ================================================================== --- src/th.c +++ src/th.c @@ -5,25 +5,52 @@ */ #include "th.h" #include <string.h> #include <assert.h> +#include <stdio.h> /* FILE class */ typedef struct Th_Command Th_Command; typedef struct Th_Frame Th_Frame; typedef struct Th_Variable Th_Variable; +/* +** Shared instance. See th.h for the docs. +*/ +const Th_Vtab_OutputMethods Th_Vtab_OutputMethods_FILE = { + Th_Output_f_FILE /* xWrite() */, + Th_Output_dispose_FILE /* dispose() */, + NULL /*pState*/, + 1/*enabled*/ +}; + +/* +** Holds client-provided "garbage collected" data for +** a Th_Interp instance. +*/ +struct Th_GcEntry { + void * pData; /* arbitrary data */ + void (*xDel)( Th_Interp *, void * ); /* finalizer for pData */ +}; +typedef struct Th_GcEntry Th_GcEntry; + /* ** Interpreter structure. */ struct Th_Interp { - Th_Vtab *pVtab; /* Copy of the argument passed to Th_CreateInterp() */ + Th_Vtab *pVtab; /* Copy of the argument passed to Th_CreateInterp() */ char *zResult; /* Current interpreter result (Th_Malloc()ed) */ - int nResult; /* number of bytes in zResult */ - Th_Hash *paCmd; /* Table of registered commands */ - Th_Frame *pFrame; /* Current execution frame */ - int isListMode; /* True if thSplitList() should operate in "list" mode */ + int nResult; /* number of bytes in zResult */ + Th_Hash *paCmd; /* Table of registered commands */ + Th_Frame *pFrame; /* Current execution frame */ + int isListMode; /* True if thSplitList() should operate in "list" mode */ + Th_Hash * paGc; /* Holds client-provided data owned by this + object. It would be more efficient to store + these in a list (because we don't expect many + entries), but Th_Hash has the strong advantage + of being here and working. + */ }; /* ** Each TH command registered using Th_CreateCommand() is represented ** by an instance of the following structure stored in the Th_Interp.paCmd @@ -106,10 +133,11 @@ static int thPushFrame(Th_Interp*, Th_Frame*); static void thPopFrame(Th_Interp*); static void thFreeVariable(Th_HashEntry*, void*); static void thFreeCommand(Th_HashEntry*, void*); +static void thFreeGc(Th_HashEntry*, void*); /* ** The following are used by both the expression and language parsers. ** Given that the start of the input string (z, n) is a language ** construct of the relevant type (a command enclosed in [], an escape @@ -288,10 +316,26 @@ pCommand->xDel((Th_Interp *)pContext, pCommand->pContext); } Th_Free((Th_Interp *)pContext, pEntry->pData); pEntry->pData = 0; } + +/* +** Th_Hash visitor/destructor for Th_Interp::paGc entries. Frees +** pEntry->pData but not pEntry. +*/ +static void thFreeGc(Th_HashEntry *pEntry, void *pContext){ + Th_GcEntry *gc = (Th_GcEntry *)pEntry->pData; + if(gc){ + if( gc->xDel ){ + gc->xDel( (Th_Interp*)pContext, gc->pData ); + } + Th_Free((Th_Interp *)pContext, pEntry->pData); + pEntry->pData = 0; + } +} + /* ** Push a new frame onto the stack. */ static int thPushFrame(Th_Interp *interp, Th_Frame *pFrame){ @@ -1369,19 +1413,67 @@ /* ** Wrappers around the supplied malloc() and free() */ void *Th_Malloc(Th_Interp *pInterp, int nByte){ - void *p = pInterp->pVtab->xMalloc(nByte); + void * p = pInterp->pVtab->xRealloc(NULL, nByte); if( p ){ memset(p, 0, nByte); + }else{ + assert( 0 == nByte ); } return p; } void Th_Free(Th_Interp *pInterp, void *z){ if( z ){ - pInterp->pVtab->xFree(z); + pInterp->pVtab->xRealloc(z, 0); + } +} +void *Th_Realloc(Th_Interp *pInterp, void *z, int nByte){ + void *p = pInterp->pVtab->xRealloc(z, nByte); + return p; +} + + +int Th_Vtab_Output( Th_Vtab *vTab, char const * zData, int nData ){ + if(!vTab->out.xWrite){ + return -1; + }else if(!vTab->out.enabled){ + return 0; + }else{ + return vTab->out.xWrite( zData, nData, vTab->out.pState ); + } +} + + +int Th_Output( Th_Interp *pInterp, char const * zData, int nData ){ + return Th_Vtab_Output( pInterp->pVtab, zData, nData ); +} + +void Th_OutputEnable( Th_Interp *pInterp, char flag ){ + pInterp->pVtab->out.enabled = flag; +} + +char Th_OutputEnabled( Th_Interp *pInterp ){ + return pInterp->pVtab->out.enabled ? 1 : 0; +} + +int Th_Output_f_FILE( char const * zData, int nData, void * pState ){ + FILE * dest = pState ? (FILE*)pState : stdout; + int rc = (int)fwrite(zData, 1, nData, dest); + fflush(dest); + return rc; +} + +void Th_Output_dispose_FILE( void * pState ){ + FILE * f = pState ? (FILE*)pState : NULL; + if(f + && (f != stdout) + && (f != stderr) + && (f != stdin)){ + fflush(f); + fclose(f); } } /* ** Install a new th1 command. @@ -1638,10 +1730,22 @@ */ void Th_DeleteInterp(Th_Interp *interp){ assert(interp->pFrame); assert(0==interp->pFrame->pCaller); + /* Delete any client-side gc entries first. */ + if( interp->paGc ){ + Th_HashIterate(interp, interp->paGc, thFreeGc, (void *)interp); + Th_HashDelete(interp, interp->paGc); + interp->paGc = NULL; + } + + /* Clean up the output abstraction. */ + if( interp->pVtab && interp->pVtab->out.xDispose ){ + interp->pVtab->out.xDispose( interp->pVtab->out.pState ); + } + /* Delete the contents of the global frame. */ thPopFrame(interp); /* Delete any result currently stored in the interpreter. */ Th_SetResult(interp, 0, 0); @@ -1648,10 +1752,11 @@ /* Delete all registered commands and the command hash-table itself. */ Th_HashIterate(interp, interp->paCmd, thFreeCommand, (void *)interp); Th_HashDelete(interp, interp->paCmd); + /* Delete the interpreter structure itself. */ Th_Free(interp, (void *)interp); } /* @@ -1659,11 +1764,11 @@ */ Th_Interp * Th_CreateInterp(Th_Vtab *pVtab){ Th_Interp *p; /* Allocate and initialise the interpreter and the global frame */ - p = pVtab->xMalloc(sizeof(Th_Interp) + sizeof(Th_Frame)); + p = pVtab->xRealloc(NULL, sizeof(Th_Interp) + sizeof(Th_Frame)); memset(p, 0, sizeof(Th_Interp)); p->pVtab = pVtab; p->paCmd = Th_HashNew(p); thPushFrame(p, (Th_Frame *)&p[1]); @@ -2271,11 +2376,11 @@ ** a null pointer. */ int th_strlen(const char *zStr){ int n = 0; if( zStr ){ - while( zStr[n] ) n++; + while( zStr[n] ) ++n; } return n; } /* Whitespace characters: @@ -2335,11 +2440,10 @@ } #ifndef LONGDOUBLE_TYPE # define LONGDOUBLE_TYPE long double #endif -typedef char u8; /* ** Return TRUE if z is a pure numeric string. Return FALSE if the ** string contains any character which is not part of a number. If @@ -2373,19 +2477,22 @@ return *z==0; } /* ** The string z[] is an ascii representation of a real number. -** Convert this string to a double. +** Convert this string to a double and assigns its value to +** pResult. ** ** This routine assumes that z[] really is a valid number. If it ** is not, the result is undefined. ** ** This routine is used instead of the library atof() function because ** the library atof() might want to use "," as the decimal point instead ** of "." depending on how locale is set. But that would cause problems ** for SQL. So this routine always uses "." regardless of locale. +** +** Returns the number of bytes of z consumed in parsing the value. */ static int sqlite3AtoF(const char *z, double *pResult){ int sign = 1; const char *zBegin = z; LONGDOUBLE_TYPE v1 = 0.0; @@ -2438,18 +2545,16 @@ *pResult = sign<0 ? -v1 : v1; return z - zBegin; } /* -** Try to convert the string passed as arguments (z, n) to an integer. -** If successful, store the result in *piOut and return TH_OK. -** -** If the string cannot be converted to an integer, return TH_ERROR. -** If the interp argument is not NULL, leave an error message in the -** interpreter result too. +** Attempts to convert (zArg,nArg) to an integer. On success *piOut is +** assigned to its value and TH_OK is returned, else piOut is not +** modified and TH_ERROR is returned. Conversion errors are considered +** non-fatal here, so interp's error state is not set. */ -int Th_ToInt(Th_Interp *interp, const char *z, int n, int *piOut){ +int Th_TryInt(Th_Interp *interp, const char *z, int n, int *piOut){ int i = 0; int iOut = 0; if( n<0 ){ n = th_strlen(z); @@ -2458,11 +2563,10 @@ if( n>0 && (z[0]=='-' || z[0]=='+') ){ i = 1; } for(; i<n; i++){ if( !th_isdigit(z[i]) ){ - Th_ErrorMessage(interp, "expected integer, got: \"", z, n); return TH_ERROR; } iOut = iOut * 10 + (z[i] - 48); } @@ -2471,10 +2575,45 @@ } *piOut = iOut; return TH_OK; } + +/* +** Try to convert the string passed as arguments (z, n) to an integer. +** If successful, store the result in *piOut and return TH_OK. +** +** If the string cannot be converted to an integer, return TH_ERROR. +** If the interp argument is not NULL, leave an error message in the +** interpreter result too. +*/ +int Th_ToInt(Th_Interp *interp, const char *z, int n, int *piOut){ + const int rc = Th_TryInt(interp, z, n, piOut); + if( TH_OK != rc ){ + Th_ErrorMessage(interp, "expected integer, got: \"", z, n); + } + return rc; +} + + +/* +** Functionally/semantically identical to Th_TryInt() but works on +** doubles. +*/ +int Th_TryDouble( + Th_Interp *interp, + const char *z, + int n, + double *pfOut +){ + if( !sqlite3IsNumber((const char *)z, 0) ){ + return TH_ERROR; + }else{ + sqlite3AtoF((const char *)z, pfOut); + return TH_OK; + } +} /* ** Try to convert the string passed as arguments (z, n) to a double. ** If successful, store the result in *pfOut and return TH_OK. ** @@ -2486,17 +2625,15 @@ Th_Interp *interp, const char *z, int n, double *pfOut ){ - if( !sqlite3IsNumber((const char *)z, 0) ){ - Th_ErrorMessage(interp, "expected number, got: \"", z, n); - return TH_ERROR; + const int rc = Th_TryDouble(interp, z, n, pfOut); + if( TH_OK != rc ){ + Th_ErrorMessage(interp, "expected number, got: \"", z, n); } - - sqlite3AtoF((const char *)z, pfOut); - return TH_OK; + return rc; } /* ** Set the result of the interpreter to the th1 representation of ** the integer iVal and return TH_OK. @@ -2605,5 +2742,509 @@ } *z = '\0'; return Th_SetResult(interp, zBuf, -1); } + + +int Th_SetData( Th_Interp * interp, char const * key, + void * pData, + void (*finalizer)( Th_Interp *, void * ) ){ + Th_HashEntry * pEnt; + Th_GcEntry * pGc; + if(NULL == interp->paGc){ + interp->paGc = Th_HashNew(interp); + assert(NULL != interp->paGc); + if(!interp->paGc){ + return TH_ERROR; + } + } + pEnt = Th_HashFind(interp, interp->paGc, key, th_strlen(key), 1); + if( pEnt->pData ){ + thFreeGc( pEnt, interp ); + } + assert( NULL == pEnt->pData ); + pEnt->pData = pGc = (Th_GcEntry*)Th_Malloc(interp, sizeof(Th_GcEntry)); + pGc->pData = pData; + pGc->xDel = finalizer; + return 0; + +} +void * Th_GetData( Th_Interp * interp, char const * key ){ + Th_HashEntry * e = interp->paGc + ? Th_HashFind(interp, interp->paGc, key, th_strlen(key), 0) + : NULL; + return e ? ((Th_GcEntry*)e->pData)->pData : NULL; +} + +int Th_RegisterCommands( Th_Interp * interp, + Th_Command_Reg const * aCommand ){ + int i; + int rc = TH_OK; + for(i=0; (TH_OK==rc) && aCommand[i].zName; ++i){ + if ( !aCommand[i].zName ) break; + else if( !aCommand[i].xProc ) continue; + else{ + rc = Th_CreateCommand(interp, aCommand[i].zName, aCommand[i].xProc, + aCommand[i].pContext, 0); + } + } + return rc; +} + + + +#ifdef TH_ENABLE_OB +/* Reminder: the ob code "really" belongs in th_lang.c or th_main.c, + but it needs access to Th_Interp::pVtab in order to swap out + Th_Vtab_OutputMethods parts for purposes of stacking layers of + buffers. We could add access to it via the public interface, + but that didn't seem appropriate. +*/ + +/* +** Th_Output_f() impl which redirects output to a Th_Ob_Manager. +** Requires that pState be a (Th_Ob_Man*). +*/ +static int Th_Output_f_ob( char const * zData, int len, void * pState ); + +/* +** Th_Output::dispose() impl which requires pState to be-a Th_Ob_Manager. +*/ +static void Th_Output_dispose_ob( void * pState ); + +/* Empty-initialized Th_Ob_Manager instance. */ +#define Th_Ob_Man_empty_m { \ + NULL/*aBuf*/, \ + 0/*nBuf*/, \ + -1/*cursor*/, \ + NULL/*interp*/, \ + NULL/*aOutput*/ \ +} + +/* Empty-initialized Th_Vtab_OutputMethods instance. */ +#define Th_Vtab_OutputMethods_empty_m { \ + NULL /* write() */, \ + NULL /* dispose() */, \ + NULL /*pState*/,\ + 1/*enabled*/\ +} +/* Vtab_OutputMethods instance initialized for OB support. */ +#define Th_Vtab_OutputMethods_ob_m { \ + Th_Output_f_ob /*write()*/, \ + Th_Output_dispose_ob /* dispose() */, \ + NULL /*pState*/,\ + 1/*enabled*/\ +} +/* Empty-initialized Th_Vtab_Man instance. */ +static const Th_Ob_Manager Th_Ob_Man_empty = Th_Ob_Man_empty_m; +/* Empty-initialized Th_Vtab_OutputMethods instance. */ +static Th_Vtab_OutputMethods Th_Vtab_OutputMethods_empty = Th_Vtab_OutputMethods_empty_m; +/* Th_Vtab_OutputMethods instance initialized for OB support. */ +static Th_Vtab_OutputMethods Th_Vtab_OutputMethods_ob = Th_Vtab_OutputMethods_ob_m; + +/* +** Internal key for Th_Set/GetData(), for storing a Th_Ob_Manager instance. +*/ +#define Th_Ob_Man_KEY "Th_Ob_Manager" + +Th_Ob_Manager * Th_Ob_GetManager(Th_Interp *interp){ + return (Th_Ob_Manager*) Th_GetData(interp, Th_Ob_Man_KEY ); +} + +Blob * Th_Ob_GetCurrentBuffer( Th_Ob_Manager * pMan ){ + return pMan->nBuf>0 ? pMan->aBuf[pMan->cursor] : 0; +} + + +/* +** Th_Output_f() impl which expects pState to be (Th_Ob_Manager*). +** (zData,len) are appended to pState's current output buffer. +*/ +int Th_Output_f_ob( char const * zData, int len, void * pState ){ + Th_Ob_Manager * pMan = (Th_Ob_Manager*)pState; + Blob * b = Th_Ob_GetCurrentBuffer( pMan ); + assert( NULL != pMan ); + assert( b ); + blob_append( b, zData, len ); + return len; +} + +void Th_Output_dispose_ob( void * pState ){ + /* possible todo: move the cleanup logic from + Th_Ob_Pop() to here? */ +#if 0 + Th_Ob_Manager * pMan = (Th_Ob_Manager*)pState; + Blob * b = Th_Ob_GetCurrentBuffer( pMan ); + assert( NULL != pMan ); + assert( b ); +#endif +} + + + +int Th_Ob_Push( Th_Ob_Manager * pMan, + Th_Vtab_OutputMethods const * pWriter, + Blob ** pOut ){ + Blob * pBlob; + int x, i; + if( NULL == pWriter ){ + pWriter = &Th_Vtab_OutputMethods_ob; + } + assert( NULL != pMan->interp ); + pBlob = (Blob *)Th_Malloc(pMan->interp, sizeof(Blob)); + *pBlob = empty_blob; + + if( pMan->cursor >= pMan->nBuf-2 ){ + /* expand if needed */ + void * re; + x = pMan->nBuf + 5; + if( pMan->cursor >= x ) { + assert( 0 && "This really should not happen." ); + x = pMan->cursor + 5; + } + re = Th_Realloc( pMan->interp, pMan->aBuf, x * sizeof(Blob*) ); + if(NULL==re){ + goto error; + } + pMan->aBuf = (Blob **)re; + re = Th_Realloc( pMan->interp, pMan->aOutput, x * sizeof(Th_Vtab_OutputMethods) ); + if(NULL==re){ + goto error; + } + pMan->aOutput = (Th_Vtab_OutputMethods*)re; + for( i = pMan->nBuf; i < x; ++i ){ + pMan->aOutput[i] = Th_Vtab_OutputMethods_empty; + pMan->aBuf[i] = NULL; + } + pMan->nBuf = x; + } + assert( pMan->nBuf > pMan->cursor ); + assert( pMan->cursor >= -1 ); + ++pMan->cursor; + pMan->aBuf[pMan->cursor] = pBlob; + pMan->aOutput[pMan->cursor] = pMan->interp->pVtab->out; + pMan->interp->pVtab->out = *pWriter; + pMan->interp->pVtab->out.pState = pMan; + if( pOut ){ + *pOut = pBlob; + } + return TH_OK; + error: + if( pBlob ){ + Th_Free( pMan->interp, pBlob ); + } + return TH_ERROR; +} + +Blob * Th_Ob_Pop( Th_Ob_Manager * pMan ){ + if( pMan->cursor < 0 ){ + return NULL; + }else{ + Blob * rc; + Th_Vtab_OutputMethods * theOut; + assert( pMan->nBuf > pMan->cursor ); + rc = pMan->aBuf[pMan->cursor]; + pMan->aBuf[pMan->cursor] = NULL; + theOut = &pMan->aOutput[pMan->cursor]; +#if 0 + /* We need something like this (but not this!) if we extend the + support to use other (non-Blob) proxies. We will likely need + another callback function or two for that case, e.g. xStart() + and xEnd(), which would be called when they are pushed/popped + to/from the stack. + */ + if( theOut->xDispose ){ + theOut->xDispose( theOut->pState ); + } +#endif + pMan->interp->pVtab->out = *theOut; + pMan->aOutput[pMan->cursor] = Th_Vtab_OutputMethods_empty; + if(-1 == --pMan->cursor){ + Th_Interp * interp = pMan->interp; + Th_Free( pMan->interp, pMan->aBuf ); + Th_Free( pMan->interp, pMan->aOutput ); + *pMan = Th_Ob_Man_empty; + pMan->interp = interp; + assert(-1 == pMan->cursor); + } + return rc; + } +} + +int Th_Ob_PopAndFree( Th_Ob_Manager * pMan ){ + Blob * b = Th_Ob_Pop( pMan ); + if(!b) return 1; + else { + blob_reset(b); + Th_Free( pMan->interp, b ); + return 0; + } +} + + +void Th_ob_cleanup( Th_Ob_Manager * man ){ + while( 0 == Th_Ob_PopAndFree(man) ){} +} + + +/* +** TH command: +** +** ob clean +** +** Erases any currently buffered contents but does not modify +** the buffering level. +*/ +static int ob_clean_command( Th_Interp *interp, void *ctx, + int argc, const char **argv, int *argl +){ + const char doRc = ctx ? 1 : 0; + Th_Ob_Manager * pMan = ctx ? (Th_Ob_Manager *)ctx : Th_Ob_GetManager(interp); + Blob * b; + assert( pMan && (interp == pMan->interp) ); + b = pMan ? Th_Ob_GetCurrentBuffer(pMan) : NULL; + if(!b){ + Th_ErrorMessage( interp, "Not currently buffering.", NULL, 0 ); + return TH_ERROR; + }else{ + blob_reset(b); + if( doRc ) { + Th_SetResultInt( interp, 0 ); + } + return TH_OK; + } +} + +/* +** TH command: +** +** ob end +** +** Erases any currently buffered contents and pops the current buffer +** from the stack. +*/ +static int ob_end_command( Th_Interp *interp, void *ctx, + int argc, const char **argv, int *argl ){ + const char doRc = ctx ? 1 : 0; + Th_Ob_Manager * pMan = ctx ? (Th_Ob_Manager *)ctx : Th_Ob_GetManager(interp); + Blob * b; + assert( pMan && (interp == pMan->interp) ); + b = Th_Ob_Pop(pMan); + if(!b){ + Th_ErrorMessage( interp, "Not currently buffering.", NULL, 0 ); + return TH_ERROR; + }else{ + blob_reset(b); + Th_Free( interp, b ); + if(doRc){ + Th_SetResultInt( interp, 0 ); + } + return TH_OK; + } +} + +/* +** TH command: +** +** ob flush ?pop|end? +** +** Briefly reverts the output layer to the next-lower +** level, flushes the current buffer to that output layer, +** and clears out the current buffer. Does not change the +** buffering level unless "end" is specified, in which case +** it behaves as if "ob end" had been called (after flushing +** the buffer). +*/ +static int ob_flush_command( Th_Interp *interp, void *ctx, + int argc, const char **argv, int *argl ){ + Th_Ob_Manager * pMan = (Th_Ob_Manager *)ctx; + Blob * b = NULL; + Th_Vtab * oldVtab; + int rc = TH_OK; + assert( pMan && (interp == pMan->interp) ); + b = Th_Ob_GetCurrentBuffer(pMan); + if( NULL == b ){ + Th_ErrorMessage( interp, "Not currently buffering.", NULL, 0 ); + return TH_ERROR; + } + oldVtab = interp->pVtab; + interp->pVtab->out = pMan->aOutput[pMan->cursor]; + Th_Output( interp, blob_str(b), b->nUsed ); + interp->pVtab = oldVtab; + blob_reset(b); + + if(!rc && argc>2){ + int argPos = 2; + char const * sub = argv[argPos]; + int subL = argl[argPos]; + /* "flush end" */ + if(th_strlen(sub)==3 && + ((0==memcmp("end", sub, subL) + || (0==memcmp("pop", sub, subL))))){ + rc |= ob_end_command(interp, NULL, argc-1, argv+1, argl+1); + } + } + Th_SetResultInt( interp, 0 ); + return rc; +} + +/* +** TH command: +** +** ob get ?clean|end|pop? +** +** Fetches the contents of the current buffer level. If either +** 'clean' or 'end' are specified then the effect is as if "ob clean" +** or "ob end", respectively, are called after fetching the +** value. Calling "ob get end" is functionality equivalent to "ob get" +** followed by "ob end". +*/ +static int ob_get_command( Th_Interp *interp, void *ctx, + int argc, const char **argv, int *argl){ + Th_Ob_Manager * pMan = (Th_Ob_Manager *)ctx; + Blob * b = NULL; + assert( pMan && (interp == pMan->interp) ); + b = Th_Ob_GetCurrentBuffer(pMan); + if( NULL == b ){ + Th_ErrorMessage( interp, "Not currently buffering.", NULL, 0 ); + return TH_ERROR; + }else{ + int argPos = 2; + char const * sub; + int subL; + int rc = TH_OK; + Th_SetResult( interp, blob_str(b), b->nUsed ); + if(argc>argPos){ + sub = argv[argPos]; + subL = argl[argPos]; + /* "ob get clean" */ + if(!rc && th_strlen(sub)==5 && 0==memcmp("clean", sub, subL)){ + rc |= ob_clean_command(interp, NULL, argc-1, argv+1, argl+1); + }/* "ob get end" */ + else if(!rc && th_strlen(sub)==3 && + ((0==memcmp("end", sub, subL)) + || (0==memcmp("pop", sub, subL)))){ + rc |= ob_end_command(interp, NULL, argc-1, argv+1, argl+1); + } + } + return rc; + } +} + +/* +** TH command: +** +** ob level +** +** Returns the buffering level, where 0 means no buffering is +** active, 1 means 1 level is active, etc. +*/ +static int ob_level_command( Th_Interp *interp, void *ctx, + int argc, const char **argv, int *argl +){ + Th_Ob_Manager * pMan = (Th_Ob_Manager *)ctx; + Th_SetResultInt( interp, 1 + pMan->cursor ); + return TH_OK; +} + +/* +** TH command: +** +** ob start|push +** +** Pushes a new level of buffering onto the buffer stack. +** Returns the new buffering level (1-based). +** +** TODO: take an optional final argument naming the output handler. +** e.g. "stdout" or "cgi" or "default" +** +*/ +static int ob_start_command( Th_Interp *interp, void *ctx, + int argc, const char **argv, int *argl +){ + Th_Ob_Manager * pMan = (Th_Ob_Manager *)ctx; + Blob * b = NULL; + int rc; + Th_Vtab_OutputMethods const * pWriter = &Th_Vtab_OutputMethods_ob; + assert( pMan && (interp == pMan->interp) ); + rc = Th_Ob_Push(pMan, NULL, &b); + if( TH_OK != rc ){ + assert( NULL == b ); + return rc; + } + assert( NULL != b ); + Th_SetResultInt( interp, 1 + pMan->cursor ); + return TH_OK; +} + +static void finalizerObMan( Th_Interp * interp, void * p ){ + Th_Ob_Manager * man = (Th_Ob_Manager*)p; + if(man){ + assert( interp == man->interp ); + Th_ob_cleanup( man ); + Th_Free( interp, p ); + } +} + +/* +** TH command: +** +** ob clean|(end|pop)|flush|get|level|(start|push) +** +** Runs the given subcommand. Some subcommands have other subcommands +** (see their docs for details). +** +*/ +static int ob_cmd( + Th_Interp *interp, + void *ignored, + int argc, + const char **argv, + int *argl +){ + Th_Ob_Manager * pMan = Th_Ob_GetManager(interp); + Th_SubCommand aSub[] = { + { "clean", ob_clean_command }, + { "end", ob_end_command }, + { "flush", ob_flush_command }, + { "get", ob_get_command }, + { "level", ob_level_command }, + { "pop", ob_end_command }, + { "push", ob_start_command }, + { "start", ob_start_command }, + /* TODO: enable/disable commands which call Th_OutputEnable(). */ + { 0, 0 } + }; + assert(NULL != pMan && pMan->interp==interp); + return Th_CallSubCommand(interp, pMan, argc, argv, argl, aSub); +} + +int th_register_ob(Th_Interp * interp){ + int rc; + static Th_Command_Reg aCommand[] = { + {"ob", ob_cmd, 0}, + {0,0,0} + }; + rc = Th_RegisterCommands( interp, aCommand ); + if(NULL == Th_Ob_GetManager(interp)){ + Th_Ob_Manager * pMan; + pMan = Th_Malloc(interp, sizeof(Th_Ob_Manager)); + if(!pMan){ + rc = TH_ERROR; + }else{ + *pMan = Th_Ob_Man_empty; + pMan->interp = interp; + assert( -1 == pMan->cursor ); + Th_SetData( interp, Th_Ob_Man_KEY, pMan, finalizerObMan ); + assert( NULL != Th_Ob_GetManager(interp) ); + } + } + return rc; +} + +#undef Th_Ob_Man_empty_m +#undef Th_Vtab_OutputMethods_empty_m +#undef Th_Vtab_OutputMethods_ob_m +#undef Th_Ob_Man_KEY +#endif +/* end TH_ENABLE_OB */ Index: src/th.h ================================================================== --- src/th.h +++ src/th.h @@ -1,32 +1,120 @@ +#include "config.h" + +/* +** TH_ENABLE_QUERY, if defined, enables the "query" family of functions. +** They provide SELECT-only access to the repository db. +*/ +#define TH_ENABLE_QUERY + +/* +** TH_ENABLE_OB, if defined, enables the "ob" family of functions. +** They are functionally similar to PHP's ob_start(), ob_end(), etc. +** family of functions, providing output capturing/buffering. +*/ +#define TH_ENABLE_OB + +/* +** TH_ENABLE_ARGV, if defined, enables the "argv" family of functions. +** They provide access to CLI arguments as well as GET/POST arguments. +** They do not provide access to POST data submitted in JSON mode. +*/ +#define TH_ENABLE_ARGV + +#ifdef TH_ENABLE_OB +#ifndef INTERFACE +#include "blob.h" /* maintenance reminder: also pulls in fossil_realloc() and friends */ +#endif +#endif /* This header file defines the external interface to the custom Scripting ** Language (TH) interpreter. TH is very similar to TCL but is not an ** exact clone. */ +/* +** Th_Output_f() specifies a generic output routine for use by +** Th_Vtab_OutputMethods and friends. Its first argument is the data to +** write, the second is the number of bytes to write, and the 3rd is +** an implementation-specific state pointer (may be NULL, depending on +** the implementation). The return value is the number of bytes output +** (which may differ from len due to encoding and whatnot). On error +** a negative value must be returned. +*/ +typedef int (*Th_Output_f)( char const * zData, int len, void * pState ); + +/* +** This structure defines the output state associated with a +** Th_Vtab. It is intended that a given Vtab be able to swap out +** output back-ends during its lifetime, e.g. to form a stack of +** buffers. +*/ +struct Th_Vtab_OutputMethods { + Th_Output_f xWrite; /* output handler */ + void (*xDispose)( void * pState ); /* Called when the framework is done with + this output handler,passed this object's + pState pointer.. */ + void * pState; /* final argument for xWrite() and xDispose()*/ + char enabled; /* if 0, Th_Output() does nothing. */ +}; +typedef struct Th_Vtab_OutputMethods Th_Vtab_OutputMethods; + +/* +** Shared Th_Vtab_OutputMethods instance used for copy-initialization. This +** implementation uses Th_Output_f_FILE as its write() impl and +** Th_Output_dispose_FILE() for cleanup. If its pState member is NULL +** it outputs to stdout, else pState must be a (FILE*) which it will +** output to. +*/ +extern const Th_Vtab_OutputMethods Th_Vtab_OutputMethods_FILE; + /* ** Before creating an interpreter, the application must allocate and ** populate an instance of the following structure. It must remain valid ** for the lifetime of the interpreter. */ struct Th_Vtab { - void *(*xMalloc)(unsigned int); - void (*xFree)(void *); + void *(*xRealloc)(void *, unsigned int); /** + Re/deallocation routine. Must behave like + realloc(3), with the minor extension that + realloc(anything,positiveValue) _must_ return + NULL on allocation error. The Standard's wording + allows realloc() to return "some value suitable for + passing to free()" on error, but because client code + has no way of knowing if any non-NULL value is an error + value, no sane realloc() implementation would/should + return anything _but_ NULL on allocation error. + */ + Th_Vtab_OutputMethods out; /** Output handler. TH functions which generate + output should send it here (via Th_Output()). + */ }; typedef struct Th_Vtab Th_Vtab; + /* ** Opaque handle for interpeter. */ typedef struct Th_Interp Th_Interp; + /* -** Create and delete interpreters. +** Creates a new interpreter instance using the given v-table. pVtab +** must outlive the returned object, and pVtab->out.xDispose() will be +** called when the interpreter is cleaned up. The optional "ob" API +** swaps out Vtab::out instances, so pVtab->out might not be active +** for the entire lifetime of the interpreter. +** +** Potential TODO: we "should probably" add a dispose() method to the +** Th_Vtab interface. */ Th_Interp * Th_CreateInterp(Th_Vtab *pVtab); -void Th_DeleteInterp(Th_Interp *); + +/* +** Frees up all resources associated with interp then frees interp. +*/ +void Th_DeleteInterp(Th_Interp *interp); /* ** Evaluate an TH program in the stack frame identified by parameter ** iFrame, according to the following rules: ** @@ -54,19 +142,37 @@ int Th_GetVar(Th_Interp *, const char *, int); int Th_SetVar(Th_Interp *, const char *, int, const char *, int); int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int); int Th_UnsetVar(Th_Interp *, const char *, int); -typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *); +/* +** Typedef for Th interpreter callbacks, i.e. script-bound native C +** functions. +** +** The interp argument is the interpreter running the function. pState +** is arbitrary state which is passed to Th_CreateCommand(). arg +** contains the number of arguments (argument #0 is the command's +** name, in the same way that main()'s argv[0] is the binary's +** name). argv is the list of arguments. argl is an array argc items +** long which contains the length of each argument in the +** list. e.g. argv[0] is argl[0] bytes long. +*/ +typedef int (*Th_CommandProc)(Th_Interp * interp, void * pState, int argc, const char ** argv, int * argl); /* -** Register new commands. +** Registers a new command with interp. zName must be a NUL-terminated +** name for the function. xProc is the native implementation of the +** function. pContext is arbitrary data to pass as xProc()'s 2nd +** argument. xDel is an optional finalizer which should be called when +** interpreter is finalized. If xDel is not NULL then it is passed +** (interp,pContext) when interp is finalized. +** +** Return TH_OK on success. */ int Th_CreateCommand( Th_Interp *interp, const char *zName, - /* int (*xProc)(Th_Interp *, void *, int, const char **, int *), */ Th_CommandProc xProc, void *pContext, void (*xDel)(Th_Interp *, void *) ); @@ -120,11 +226,11 @@ ** Access the memory management functions associated with the specified ** interpreter. */ void *Th_Malloc(Th_Interp *, int); void Th_Free(Th_Interp *, void *); - +void *Th_Realloc(Th_Interp *, void *, int); /* ** Functions for handling TH lists. */ int Th_ListAppend(Th_Interp *, char **, int *, const char *, int); int Th_SplitList(Th_Interp *, const char *, int, char ***, int **, int *); @@ -136,10 +242,12 @@ */ int Th_ToInt(Th_Interp *, const char *, int, int *); int Th_ToDouble(Th_Interp *, const char *, int, double *); int Th_SetResultInt(Th_Interp *, int); int Th_SetResultDouble(Th_Interp *, double); +int Th_TryInt(Th_Interp *, const char * zArg, int nArg, int * piOut); +int Th_TryDouble(Th_Interp *, const char * zArg, int nArg, double * pfOut); /* ** Drop in replacements for the corresponding standard library functions. */ int th_strlen(const char *); @@ -151,14 +259,38 @@ /* ** Interfaces to register the language extensions. */ int th_register_language(Th_Interp *interp); /* th_lang.c */ -int th_register_sqlite(Th_Interp *interp); /* th_sqlite.c */ int th_register_vfs(Th_Interp *interp); /* th_vfs.c */ int th_register_testvfs(Th_Interp *interp); /* th_testvfs.c */ + +/* +** Registers the TCL extensions. Only available if FOSSIL_ENABLE_TCL +** is enabled at compile-time. +*/ int th_register_tcl(Th_Interp *interp, void *pContext); /* th_tcl.c */ + +#ifdef TH_ENABLE_ARGV +/* +** Registers the "argv" API. See www/th1_argv.wiki. +*/ +int th_register_argv(Th_Interp *interp); /* th_main.c */ +#endif + +#ifdef TH_ENABLE_QUERY +/* +** Registers the "query" API. See www/th1_query.wiki. +*/ +int th_register_query(Th_Interp *interp); /* th_main.c */ +#endif +#ifdef TH_ENABLE_OB +/* +** Registers the "ob" API. See www/th1_ob.wiki. +*/ +int th_register_ob(Th_Interp * interp); /* th.c */ +#endif /* ** General purpose hash table from th_lang.c. */ typedef struct Th_Hash Th_Hash; @@ -175,9 +307,223 @@ Th_HashEntry *Th_HashFind(Th_Interp*, Th_Hash*, const char*, int, int); /* ** Useful functions from th_lang.c. */ + +/* +** Generic "wrong number of arguments" helper which sets the error +** state of interp to the given message plus a generic prefix. +*/ int Th_WrongNumArgs(Th_Interp *interp, const char *zMsg); +/* +** Works like Th_WrongNumArgs() but expects (zCmdName,zCmdLen) to be +** the current command's (name,length), i.e. (argv[0],argl[0]). +*/ +int Th_WrongNumArgs2(Th_Interp *interp, const char *zCmdName, + int zCmdLen, const char *zMsg); + typedef struct Th_SubCommand {char *zName; Th_CommandProc xProc;} Th_SubCommand; int Th_CallSubCommand(Th_Interp*,void*,int,const char**,int*,Th_SubCommand*); + +/* +** Works similarly to Th_CallSubCommand() but adjusts argc/argv/argl +** by 1 before passing on the call to the subcommand. This allows them +** to function the same whether they are called as top-level commands +** or as sub-sub-commands. +*/ +int Th_CallSubCommand2(Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl, Th_SubCommand *aSub); + +/* +** Sends the given data through vTab->out.f() if vTab->out.enabled is +** true, otherwise this is a no-op. Returns 0 or higher on success, * +** a negative value if vTab->out.f is NULL. +*/ +int Th_Vtab_Output( Th_Vtab *vTab, char const * zData, int len ); + +/* +** Sends the given output through pInterp's vtab's output +** implementation. See Th_Vtab_OutputMethods() for the argument and +** return value semantics. +*/ +int Th_Output( Th_Interp *pInterp, char const * zData, int len ); + +/* +** Enables or disables output of the current Vtab API, depending on +** whether flag is true (non-0) or false (0). Note that when output +** buffering/stacking is enabled (e.g. via the "ob" API) this modifies +** only the current output mechanism, and not any further down the +** stack. +*/ +void Th_OutputEnable( Th_Interp *pInterp, char flag ); + +/* +** Returns true if output is enabled for the current output mechanism +** of pInterp, else false. See Th_OutputEnable(). +*/ +char Th_OutputEnabled( Th_Interp *pInterp ); + + + +/* +** A Th_Output_f() implementation which sends its output to either +** pState (which must be NULL or a (FILE*)) or stdout (if pState is +** NULL). +*/ +int Th_Output_f_FILE( char const * zData, int len, void * pState ); + +/* +** A Th_Vtab_OutputMethods::xDispose() impl for FILE handles. If pState is not +** one of the standard streams (stdin, stdout, stderr) then it is +** fclose()d by this call. +*/ +void Th_Output_dispose_FILE( void * pState ); + +/* +** A helper type for holding lists of function registration information. +** For use with Th_RegisterCommands(). +*/ +struct Th_Command_Reg { + const char *zName; /* Function name. */ + Th_CommandProc xProc; /* Callback function */ + void *pContext; /* Arbitrary data for the callback. */ +}; +typedef struct Th_Command_Reg Th_Command_Reg; + +/* +** Th_Render_Flags_XXX are flags for Th_Render(). +*/ +/* makeheaders cannot do enums: enum Th_Render_Flags {...};*/ +/* +** Default flags ("compatibility mode"). +*/ +#define Th_Render_Flags_DEFAULT 0 +/* +** If set, Th_Render() will not process $var and $<var> +** variable references outside of TH1 blocks. +*/ +#define Th_Render_Flags_NO_DOLLAR_DEREF (1 << 1) + +/* +** Runs the given th1 program through Fossil's th1 interpreter. Flags +** may contain a bitmask made up of any of the Th_Render_Flags_XXX +** values. +*/ +int Th_Render(const char *zTh1Program, int Th_Render_Flags); + +/* +** Adds a piece of memory to the given interpreter, such that: +** +** a) it will be cleaned up when the interpreter is destroyed, by +** calling finalizer(interp, pData). The finalizer may be NULL. +** Cleanup happens in an unspecified/unpredictable order. +** +** b) it can be fetched via Th_GetData(). +** +** If a given key is added more than once then any previous +** entry is cleaned up before adding it. +** +** Returns 0 on success, non-0 on allocation error. +*/ +int Th_SetData( Th_Interp * interp, char const * key, + void * pData, + void (*finalizer)( Th_Interp *, void * ) ); + +/* +** Fetches data added via Th_SetData(), or NULL if no data +** has been associated with the given key. +*/ +void * Th_GetData( Th_Interp * interp, char const * key ); + + +/* +** Registers a list of commands with the interpreter. pList must be a non-NULL +** pointer to an array of Th_Command_Reg objects, the last one of which MUST +** have a NULL zName field (that is the end-of-list marker). +** Returns TH_OK on success, "something else" on error. +*/ +int Th_RegisterCommands( Th_Interp * interp, Th_Command_Reg const * pList ); + +#ifdef TH_ENABLE_OB +/* +** Output buffer stack manager for TH. Used/managed by the Th_ob_xxx() +** functions. This class manages Th_Interp::pVtab->out for a specific +** interpreter, swapping it in and out in order to redirect output +** generated via Th_Output() to internal buffers. The buffers can be +** pushed and popped from the stack, allowing clients to selectively +** capture output for a given block of TH1 code. +*/ +struct Th_Ob_Manager { + Blob ** aBuf; /* Stack of Blobs */ + int nBuf; /* Number of blobs */ + int cursor; /* Current level (-1=not active) */ + Th_Interp * interp; /* The associated interpreter */ + Th_Vtab_OutputMethods * aOutput + /* Stack of output routines corresponding + to the current buffering level. + Has nBuf entries. + */; +}; + +/* +** Manager of a stack of Th_Vtab_Output objects for output buffering. +** It gets its name ("ob") from the similarly-named PHP functionality. +** +** See Th_Ob_GetManager(). +** +** Potential TODO: remove the Blob from the interface and replace it +** with a Th_Output_f (or similar) which clients can pass in to have +** the data transfered from Th_Ob_Manager to them. We would also need to +** add APIs for clearing the buffer. +*/ +typedef struct Th_Ob_Manager Th_Ob_Manager; + +/* +** Returns the ob manager for the given interpreter. The manager gets +** installed by the th_register_ob(). In Fossil ob support is +** installed automatically if it is available at built time. +*/ +Th_Ob_Manager * Th_Ob_GetManager(Th_Interp *ignored); + +/* +** Returns the top-most Blob in pMan's stack, or NULL if buffering is +** not active or if the current buffering level does not refer to a +** blob. (Note: the latter will never currently be the case, but may +** be if the API is expanded to offer other output direction options, +** e.g. (ob start file /tmp/foo.out).) +*/ +Blob * Th_Ob_GetCurrentBuffer( Th_Ob_Manager * pMan ); + +/* +** Pushes a new blob onto pMan's stack. On success returns TH_OK and +** assigns *pOut (if pOut is not NULL) to the new blob (which is owned +** by pMan). On error pOut is not modified and non-0 is returned. The +** new blob can be cleaned up via Th_Ob_Pop() or Th_Ob_PopAndFree() +** (please read both to understand the difference!). +*/ +int Th_Ob_Push( Th_Ob_Manager * pMan, Th_Vtab_OutputMethods const * pWriter, Blob ** pOut ); + +/* +** Pops the top-most output buffer off the stack and returns +** it. Returns NULL if there is no current buffer. When the last +** buffer is popped, pMan's internals are cleaned up (but pMan is not +** freed). +** +** The caller owns the returned object and must eventually clean it up +** by first passing it to blob_reset() and then Th_Free() it. +** +** See also: Th_Ob_PopAndFree(). +*/ +Blob * Th_Ob_Pop( Th_Ob_Manager * pMan ); + +/* +** Convenience form of Th_Ob_Pop() which pops and frees the +** top-most buffer. Returns 0 on success, non-0 if there is no +** stack to pop. Thus is can be used in a loop like: +** +** while( !Th_Ob_PopAndFree(theManager) ) {} +*/ +int Th_Ob_PopAndFree( Th_Ob_Manager * pMan ); + +#endif +/* end TH_ENABLE_OB */ Index: src/th_lang.c ================================================================== --- src/th_lang.c +++ src/th_lang.c @@ -16,10 +16,23 @@ int Th_WrongNumArgs(Th_Interp *interp, const char *zMsg){ Th_ErrorMessage(interp, "wrong # args: should be \"", zMsg, -1); return TH_ERROR; } + +int Th_WrongNumArgs2(Th_Interp *interp, const char *zCmdName, + int zCmdLen, const char *zMsg){ + char * zBuf = 0; + int nBuf = 0; + Th_StringAppend(interp, &zBuf, &nBuf, zCmdName, zCmdLen); + Th_StringAppend(interp, &zBuf, &nBuf, ": wrong # args: expecting: ", -1); + Th_StringAppend(interp, &zBuf, &nBuf, zMsg, -1); + Th_StringAppend(interp, &zBuf, &nBuf, "", 1); + Th_ErrorMessage(interp, zBuf, NULL, 0); + Th_Free(interp, zBuf); + return TH_ERROR; +} /* ** Syntax: ** ** catch script ?varname? @@ -458,13 +471,15 @@ /* If the last parameter in the parameter list is "args", then set the ** ProcDefn.hasArgs flag. The "args" parameter does not require an ** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays. */ - if( anParam[nParam-1]==4 && 0==memcmp(azParam[nParam-1], "args", 4) ){ - p->hasArgs = 1; - nParam--; + if(nParam>0){ + if( anParam[nParam-1]==4 && 0==memcmp(azParam[nParam-1], "args", 4) ){ + p->hasArgs = 1; + nParam--; + } } p->nParam = nParam; p->azParam = (char **)&p[1]; p->anParam = (int *)&p->azParam[nParam]; @@ -870,10 +885,30 @@ } Th_ErrorMessage(interp, "Expected sub-command, got:", argv[1], argl[1]); return TH_ERROR; } + +int Th_CallSubCommand2( + Th_Interp *interp, + void *ctx, + int argc, + const char **argv, + int *argl, + Th_SubCommand *aSub +){ + int i; + for(i=0; aSub[i].zName; i++){ + char const *zName = aSub[i].zName; + if( th_strlen(zName)==argl[1] && 0==memcmp(zName, argv[1], argl[1]) ){ + return aSub[i].xProc(interp, ctx, argc-1, argv+1, argl+1); + } + } + Th_ErrorMessage(interp, "Expected sub-command, got:", argv[1], argl[1]); + return TH_ERROR; +} + /* ** TH Syntax: ** ** string compare STR1 STR2 @@ -1028,16 +1063,13 @@ /* ** Register the built-in th1 language commands with interpreter interp. ** Usually this is called soon after interpreter creation. */ int th_register_language(Th_Interp *interp){ + int rc; /* Array of built-in commands. */ - struct _Command { - const char *zName; - Th_CommandProc xProc; - void *pContext; - } aCommand[] = { + struct Th_Command_Reg aCommand[] = { {"catch", catch_command, 0}, {"expr", expr_command, 0}, {"for", for_command, 0}, {"if", if_command, 0}, {"info", info_command, 0}, @@ -1059,17 +1091,8 @@ {"continue", simple_command, (void *)TH_CONTINUE}, {"error", simple_command, (void *)TH_ERROR}, {0, 0, 0} }; - int i; - - /* Add the language commands. */ - for(i=0; i<(sizeof(aCommand)/sizeof(aCommand[0])); i++){ - void *ctx; - if ( !aCommand[i].zName || !aCommand[i].xProc ) continue; - ctx = aCommand[i].pContext; - Th_CreateCommand(interp, aCommand[i].zName, aCommand[i].xProc, ctx, 0); - } - - return TH_OK; + rc = Th_RegisterCommands(interp, aCommand); + return rc; } Index: src/th_main.c ================================================================== --- src/th_main.c +++ src/th_main.c @@ -19,10 +19,16 @@ ** (an independent project) and fossil. */ #include "config.h" #include "th_main.h" +#ifdef TH_ENABLE_QUERY +#ifndef INTERFACE +#include "sqlite3.h" +#endif +#endif + /* ** Global variable counting the number of outstanding calls to malloc() ** made by the th1 implementation. This is used to catch memory leaks ** in the interpreter. Obviously, it also means th1 is not threadsafe. */ @@ -40,13 +46,30 @@ } static void xFree(void *p){ if( p ){ nOutstandingMalloc--; } - free(p); + fossil_free(p); } -static Th_Vtab vtab = { xMalloc, xFree }; + +/* +** Default Th_Vtab::xRealloc() implementation. +*/ +static void *xRealloc(void * p, unsigned int n){ + assert(n>=0 && "Invalid memory (re/de)allocation size."); + if(0 == n){ + xFree(p); + return NULL; + }else if(NULL == p){ + return xMalloc(n); + }else{ + return fossil_realloc(p, n) + /* In theory nOutstandingMalloc doesn't need to be updated here + unless xRealloc() is sorely misused. + */; + } +} /* ** Generate a TH1 trace message if debugging is enabled. */ void Th_Trace(const char *zFormat, ...){ @@ -57,10 +80,18 @@ } /* ** True if output is enabled. False if disabled. +** +** We "could" replace this with Th_OutputEnable() and friends, but +** there is a functional difference: this particular flag prohibits +** some extra escaping which would happen (but be discared, unused) if +** relied solely on that API. Also, because that API only works on the +** current Vtab_Output handler, relying soly on that handling would +** introduce incompatible behaviour with the historical enable_output +** command. */ static int enableOutput = 1; /* ** TH command: enable_output BOOLEAN @@ -73,53 +104,97 @@ int argc, const char **argv, int *argl ){ if( argc!=2 ){ - return Th_WrongNumArgs(interp, "enable_output BOOLEAN"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "BOOLEAN"); + }else{ + int rc = Th_ToInt(interp, argv[1], argl[1], &enableOutput); + return rc; } - return Th_ToInt(interp, argv[1], argl[1], &enableOutput); } + +/* +** Th_Output_f() impl which sends all output to cgi_append_content(). +*/ +static int Th_Output_f_cgi_content( char const * zData, int nData, void * pState ){ + cgi_append_content(zData, nData); + return nData; +} + /* ** Send text to the appropriate output: Either to the console ** or to the CGI reply buffer. */ -static void sendText(const char *z, int n, int encode){ +static void sendText(Th_Interp *pInterp, const char *z, int n, int encode){ + if(NULL == pInterp){ + pInterp = g.interp; + } + assert( NULL != pInterp ); if( enableOutput && n ){ if( n<0 ) n = strlen(z); if( encode ){ z = htmlize(z, n); n = strlen(z); } - if( g.cgiOutput ){ - cgi_append_content(z, n); - }else{ - fwrite(z, 1, n, stdout); - fflush(stdout); - } - if( encode ) free((char*)z); + Th_Output( pInterp, z, n ); + if( encode ) fossil_free((char*)z); } } +/* +** Internal state for the putsCmd() function, allowing it to be used +** as the basis for multiple implementations with slightly different +** behaviours based on the context. An instance of this type must be +** set as the Context parameter for any putsCmd()-based script command +** binding. +*/ +struct PutsCmdData { + char escapeHtml; /* If true, htmlize all output. */ + char const * sep; /* Optional NUL-terminated separator to output + between arguments. May be NULL. */ + char const * eol; /* Optional NUL-terminated end-of-line separator, + output after the final argument. May be NULL. */ +}; +typedef struct PutsCmdData PutsCmdData; + /* ** TH command: puts STRING ** TH command: html STRING ** -** Output STRING as HTML (html) or unchanged (puts). +** Output STRING as HTML (html) or unchanged (puts). +** +** pConvert MUST be a (PutsCmdData [const]*). It is not modified by +** this function. */ static int putsCmd( Th_Interp *interp, void *pConvert, int argc, const char **argv, int *argl ){ - if( argc!=2 ){ - return Th_WrongNumArgs(interp, "puts STRING"); + PutsCmdData const * fmt = (PutsCmdData const *)pConvert; + const int sepLen = fmt->sep ? strlen(fmt->sep) : 0; + int i; + if( argc<2 ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING ...STRING_N"); } - sendText((char*)argv[1], argl[1], pConvert!=0); + for( i = 1; i < argc; ++i ){ + if(sepLen && (i>1)){ + sendText(interp, fmt->sep, sepLen, 0); + } + sendText(interp, (char const*)argv[i], argl[i], fmt->escapeHtml); + } + if(fmt->eol){ + sendText(interp, fmt->eol, strlen(fmt->eol), 0); + } return TH_OK; } /* ** TH command: wiki STRING @@ -132,11 +207,13 @@ int argc, const char **argv, int *argl ){ if( argc!=2 ){ - return Th_WrongNumArgs(interp, "wiki STRING"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING"); } if( enableOutput ){ Blob src; blob_init(&src, (char*)argv[1], argl[1]); wiki_convert(&src, 0, WIKI_INLINE); @@ -158,18 +235,74 @@ const char **argv, int *argl ){ char *zOut; if( argc!=2 ){ - return Th_WrongNumArgs(interp, "htmlize STRING"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING"); } zOut = htmlize((char*)argv[1], argl[1]); Th_SetResult(interp, zOut, -1); free(zOut); return TH_OK; } +#if 0 +/* This is not yet needed, but something like it may become useful for + custom page/command support, for rendering snippets/templates. */ +/* +** TH command: render STRING +** +** Render the input string as TH1. +*/ +static int renderCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc<2 ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING ?STRING...?"); + }else{ + Th_Ob_Manager * man = Th_Ob_GetManager(interp); + Blob * b = NULL; + Blob buf = empty_blob; + int rc, i; + /*FIXME: assert(NULL != man && man->interp==interp);*/ + man->interp = interp; + /* Combine all inputs into one buffer so that we can use that to + embed TH1 tags across argument boundaries. + + FIX:E optimize away buf for the 1-arg case. + */ + for( i = 1; TH_OK==rc && i < argc; ++i ){ + char const * str = argv[i]; + blob_append( &buf, str, argl[i] ); + /*rc = Th_Render( str, Th_Render_Flags_NO_DOLLAR_DEREF );*/ + } + rc = Th_Ob_Push( man, &b ); + if(rc){ + blob_reset( &buf ); + return rc; + } + rc = Th_Render( buf.aData, Th_Render_Flags_DEFAULT ); + blob_reset(&buf); + b = Th_Ob_Pop( man ); + if(TH_OK==rc){ + Th_SetResult( interp, b->aData, b->nUsed ); + } + blob_reset( b ); + Th_Free( interp, b ); + return rc; + } +}/* renderCmd() */ +#endif + /* ** TH command: date ** ** Return a string which is the current time and date. If the ** -local option is used, the date appears using localtime instead @@ -205,11 +338,13 @@ const char **argv, int *argl ){ int rc; if( argc!=2 ){ - return Th_WrongNumArgs(interp, "hascap STRING"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING"); } rc = login_has_capability((char*)argv[1],argl[1]); if( g.thTrace ){ Th_Trace("[hascap %#h] => %d<br />\n", argl[1], argv[1], rc); } @@ -236,11 +371,13 @@ int *argl ){ int rc = 0; char const * zArg; if( argc!=2 ){ - return Th_WrongNumArgs(interp, "hasfeature STRING"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING"); } zArg = (char const*)argv[1]; if(NULL==zArg){ /* placeholder for following ifdefs... */ } @@ -280,11 +417,13 @@ int *argl ){ int rc = 0; int i; if( argc!=2 ){ - return Th_WrongNumArgs(interp, "anycap STRING"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING"); } for(i=0; rc==0 && i<argl[1]; i++){ rc = login_has_capability((char*)&argv[1][i],1); } if( g.thTrace ){ @@ -310,11 +449,13 @@ int argc, const char **argv, int *argl ){ if( argc!=4 ){ - return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "NAME TEXT-LIST NUMLINES"); } if( enableOutput ){ int height; Blob name; int nValue; @@ -329,11 +470,11 @@ Th_SplitList(interp, argv[2], argl[2], &azElem, &aszElem, &nElem); blob_init(&name, (char*)argv[1], argl[1]); zValue = Th_Fetch(blob_str(&name), &nValue); z = mprintf("<select name=\"%z\" size=\"%d\">", htmlize(blob_buffer(&name), blob_size(&name)), height); - sendText(z, -1, 0); + sendText(interp, z, -1, 0); free(z); blob_reset(&name); for(i=0; i<nElem; i++){ zH = htmlize((char*)azElem[i], aszElem[i]); if( zValue && aszElem[i]==nValue @@ -342,14 +483,14 @@ zH, zH); }else{ z = mprintf("<option value=\"%s\">%s</option>", zH, zH); } free(zH); - sendText(z, -1, 0); + sendText(interp, z, -1, 0); free(z); } - sendText("</select>", -1, 0); + sendText(interp, "</select>", -1, 0); Th_Free(interp, azElem); } return TH_OK; } @@ -368,11 +509,13 @@ ){ const char *z; int size, n, i; int iMin, iMax; if( argc!=4 ){ - return Th_WrongNumArgs(interp, "linecount STRING MAX MIN"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING MAX MIN"); } if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR; if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR; z = argv[1]; size = argl[1]; @@ -403,11 +546,13 @@ int *argl ){ int openRepository; if( argc!=1 && argc!=2 ){ - return Th_WrongNumArgs(interp, "repository ?BOOLEAN?"); + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "?BOOLEAN?"); } if( argc==2 ){ if( Th_ToInt(interp, argv[1], argl[1], &openRepository) ){ return TH_ERROR; } @@ -415,50 +560,1572 @@ } Th_SetResult(interp, g.zRepositoryName, -1); return TH_OK; } + +#ifdef TH_ENABLE_ARGV +/* +** TH command: +** +** argv len +** +** Returns the number of command-line arguments. +*/ +static int argvArgcCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_SetResultInt( interp, g.argc ); + return TH_OK; +} + + + +/* +** TH command: +** +** argv at Index +** +** Returns the raw argument at the given index, throwing if +** out of bounds. +*/ +static int argvGetAtCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + char const * zVal; + int pos = 0; + if( argc != 2 ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "Index"); + } + if( TH_OK != Th_ToInt(interp, argv[1], argl[1], &pos) ){ + return TH_ERROR; + } + if( pos < 0 || pos >= g.argc ){ + Th_ErrorMessage(interp, "Argument out of range:", argv[1], argl[1]); + return TH_ERROR; + } + if( 0 == pos ){/*special case*/ + zVal = fossil_nameofexe(); + }else{ + zVal = (pos>0 && pos<g.argc) ? g.argv[pos] : 0; + } + Th_SetResult( interp, zVal, zVal ? strlen(zVal) : 0 ); + return TH_OK; +} + + +/* +** TH command: +** +** argv getstr longName ??shortName? ?defaultValue?? +** +** Functions more or less like Fossil's find_option(). +** If the given argument is found then its value is returned, +** else defaultValue is returned. If that is not set +** and the option is not found, an error is thrown. +** If defaultValue is provided, shortName must also be provided +** but it may be empty. For example: +** +** set foo [argv getstr foo "" "hi, world"] +** +** ACHTUNG: find_option() removes any entries it finds from +** g.argv, such that future calls to find_option() will not +** find the same option. +*/ +static int argvFindOptionStringCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + enum { BufLen = 100 }; + char zLong[BufLen] = {0}; + char zShort[BufLen] = {0}; + char aBuf[BufLen] = {0}; + int hasArg; + char const * zVal = NULL; + char const * zDefault = NULL; + int check; + if( 1 < argc ){ + assert( argl[1] < BufLen ); + check = snprintf( zLong, BufLen, "%s", argv[1] ); + assert( check <= BufLen ); + } + if( (2 < argc) && (0 < argl[2]) ){ + assert( argl[2] < BufLen ); + check = snprintf( zShort, BufLen, "%s", argv[2] ); + assert( check <= BufLen ); + } + if( 3 < argc){ + zDefault = argv[3]; + } + + if(0 == zLong[0]){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "longName ?shortName? ?defaultVal?"); + } + if(g.cgiOutput){ + zVal = cgi_parameter( zLong, NULL ); + if( !zVal && zShort[0] ){ + zVal = cgi_parameter( zShort, NULL ); + } + }else{ + zVal = find_option( zLong, zShort[0] ? zShort : NULL, 1 ); + } + if(!zVal){ + zVal = zDefault; + if(!zVal){ + Th_ErrorMessage(interp, "Option not found and no default provided:", zLong, -1); + return TH_ERROR; + } + } + Th_SetResult( interp, zVal, zVal ? strlen(zVal) : 0 ); + return TH_OK; +} + +/* +** TH command: +** +** argv getbool longName ??shortName? ?defaultValue?? +** +** Works just like argv getstr but treats any empty value or one +** starting with the digit '0' as a boolean false. +** +** Returns the result as an integer 0 (false) or 1 (true). +*/ +static int argvFindOptionBoolCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + /* FIXME: refactor to re-use the code from getstr */ + enum { BufLen = 100 }; + char zLong[BufLen] = {0}; + char zShort[BufLen] = {0}; + char aBuf[BufLen] = {0}; + int hasArg; + char const * zVal = NULL; + char const * zDefault = NULL; + int val; + int rc; + int check; + if( 1 < argc ){ + assert( argl[1] < BufLen ); + check = snprintf( zLong, BufLen, "%s", argv[1] ); + assert( check <= BufLen ); + } + if( (2 < argc) && (0 < argl[2]) ){ + assert( argl[2] < BufLen ); + check = snprintf( zShort, BufLen, "%s", argv[2] ); + assert( check <= BufLen ); + } + if( 3 < argc){ + zDefault = argv[3]; + } + + if(0 == zLong[0]){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "longName ?shortName? ?defaultVal?"); + } + if(g.cgiOutput){ + zVal = cgi_parameter( zLong, NULL ); + if( !zVal && zShort[0] ){ + zVal = cgi_parameter( zShort, NULL ); + } + }else{ + zVal = find_option( zLong, zShort[0] ? zShort : NULL, 0 ); + } + if(zVal && !*zVal){ + zVal = "1"; + } + if(!zVal){ + zVal = zDefault; + if(!zVal){ + Th_ErrorMessage(interp, "Option not found and no default provided:", zLong, -1); + return TH_ERROR; + } + } + if( !*zVal ){ + zVal = "0"; + } + zVal = (zVal && *zVal && (*zVal!='0')) ? zVal : 0; + Th_SetResultInt( interp, zVal ? 1 : 0 ); + return TH_OK; +} + +/* +** TH command: +** +** argv getint longName ?shortName? ?defaultValue? +** +** Works like argv getstr but returns the value as an integer +** (throwing an error if the argument cannot be converted). +*/ +static int argvFindOptionIntCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + /* FIXME: refactor to re-use the code from getstr */ + enum { BufLen = 100 }; + char zLong[BufLen] = {0}; + char zShort[BufLen] = {0}; + char aBuf[BufLen] = {0}; + int hasArg; + char const * zVal = NULL; + char const * zDefault = NULL; + int val = 0; + int check; + if( 1 < argc ){ + assert( argl[1] < BufLen ); + check = snprintf( zLong, BufLen, "%s", argv[1] ); + assert( check <= BufLen ); + } + if( (2 < argc) && (0 < argl[2]) ){ + assert( argl[2] < BufLen ); + check = snprintf( zShort, BufLen, "%s", argv[2] ); + assert( check <= BufLen ); + } + if( 3 < argc){ + zDefault = argv[3]; + } + + if(0 == zLong[0]){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "longName ?shortName? ?defaultVal?"); + } + if(g.cgiOutput){ + zVal = cgi_parameter( zLong, NULL ); + if( !zVal && zShort[0] ){ + zVal = cgi_parameter( zShort, NULL ); + } + }else{ + zVal = find_option( zLong, zShort[0] ? zShort : NULL, 1 ); + } + if(!zVal){ + zVal = zDefault; + if(!zVal){ + Th_ErrorMessage(interp, "Option not found and no default provided:", zLong, -1); + return TH_ERROR; + } + } + Th_ToInt(interp, zVal, strlen(zVal), &val); + Th_SetResultInt( interp, val ); + return TH_OK; +} + +/* +** TH command: +** +** argv subcommand +** +** This is the top-level dispatching function. +*/ +static int argvTopLevelCmd( + Th_Interp *interp, + void *ctx, + int argc, + const char **argv, + int *argl +){ + static Th_SubCommand aSub[] = { + {"len", argvArgcCmd}, + {"at", argvGetAtCmd}, + {"getstr", argvFindOptionStringCmd}, + {"string", argvFindOptionStringCmd}, + {"getbool", argvFindOptionBoolCmd}, + {"bool", argvFindOptionBoolCmd}, + {"getint", argvFindOptionIntCmd}, + {"int", argvFindOptionIntCmd}, + {0, 0} + }; + Th_CallSubCommand2( interp, ctx, argc, argv, argl, aSub ); +} + +int th_register_argv(Th_Interp *interp){ + static Th_Command_Reg aCommand[] = { + {"argv", argvTopLevelCmd, 0 }, + {0, 0, 0} + }; + Th_RegisterCommands( interp, aCommand ); +} + +#endif +/* end TH_ENABLE_ARGV */ + +#ifdef TH_ENABLE_QUERY + +/* +** Adds the given prepared statement to the interpreter. Returns the +** statement's opaque identifier (a positive value). Ownerships of +** pStmt is transfered to interp and it must be cleaned up by the +** client by calling Th_query_FinalizeStmt(), passing it the value returned +** by this function. +** +** If interp is destroyed before all statements are finalized, +** it will finalize them but may emit a warning message. +*/ +static int Th_query_AddStmt(Th_Interp *interp, sqlite3_stmt * pStmt); + + +/* +** Internal state for the "query" API. +*/ +struct Th_Query { + sqlite3_stmt ** aStmt; /* Array of statement handles. */ + int nStmt; /* number of entries in aStmt. */ + int colCmdIndex; /* column index argument. Set by some top-level dispatchers + for their subcommands. + */ +}; +/* +** Internal key for use with Th_Data_Add(). +*/ +#define Th_Query_KEY "Th_Query" +typedef struct Th_Query Th_Query; + +/* +** Returns the Th_Query object associated with the given interpreter, +** or 0 if there is not one. +*/ +static Th_Query * Th_query_manager( Th_Interp * interp ){ + void * p = Th_GetData( interp, Th_Query_KEY ); + return p ? (Th_Query*)p : NULL; +} + +static int Th_query_AddStmt(Th_Interp *interp, sqlite3_stmt * pStmt){ + Th_Query * sq = Th_query_manager(interp); + int i, x; + sqlite3_stmt * s; + sqlite3_stmt ** list = sq->aStmt; + for( i = 0; i < sq->nStmt; ++i ){ + s = list[i]; + if(NULL==s){ + list[i] = pStmt; + return i+1; + } + } + x = (sq->nStmt + 1) * 2; + list = (sqlite3_stmt**)fossil_realloc( list, sizeof(sqlite3_stmt*)*x ); + for( i = sq->nStmt; i < x; ++i ){ + list[i] = NULL; + } + list[sq->nStmt] = pStmt; + x = sq->nStmt; + sq->nStmt = i; + sq->aStmt = list; + return x + 1; +} + + +/* +** Expects stmtId to be a statement identifier returned by +** Th_query_AddStmt(). On success, finalizes the statement and returns 0. +** On error (statement not found) non-0 is returned. After this +** call, some subsequent call to Th_query_AddStmt() may return the +** same statement ID. +*/ +static int Th_query_FinalizeStmt(Th_Interp *interp, int stmtId){ + Th_Query * sq = Th_query_manager(interp); + sqlite3_stmt * st; + int rc = 0; + assert( stmtId>0 && stmtId<=sq->nStmt ); + st = sq->aStmt[stmtId-1]; + if(NULL != st){ + sq->aStmt[stmtId-1] = NULL; + sqlite3_finalize(st); + return 0; + }else{ + return 1; + } +} + +/* +** Works like Th_query_FinalizeStmt() but takes a statement pointer, which +** must have been Th_query_AddStmt()'d to the given interpreter. +*/ +static int Th_query_FinalizeStmt2(Th_Interp *interp, sqlite3_stmt * pSt){ + Th_Query * sq = Th_query_manager(interp); + int i = 0; + sqlite3_stmt * st = NULL; + int rc = 0; + for( ; i < sq->nStmt; ++i ){ + st = sq->aStmt[i]; + if(st == pSt) break; + } + if( st == pSt ){ + assert( i>=0 && i<sq->nStmt ); + sq->aStmt[i] = NULL; + sqlite3_finalize(st); + return 0; + }else{ + return 1; + } +} + + +/* +** Fetches the statement with the given ID, as returned by +** Th_query_AddStmt(). Returns NULL if stmtId does not refer (or no longer +** refers) to a statement added via Th_query_AddStmt(). +*/ +static sqlite3_stmt * Th_query_GetStmt(Th_Interp *interp, int stmtId){ + Th_Query * sq = Th_query_manager(interp); + return (!sq || (stmtId<1) || (stmtId > sq->nStmt)) + ? NULL + : sq->aStmt[stmtId-1]; +} + + +/* +** Th_GCEntry finalizer which requires that p be a (Th_Query*). +*/ +static void finalizerSqlite( Th_Interp * interp, void * p ){ + Th_Query * sq = (Th_Query *)p; + int i; + sqlite3_stmt * st = NULL; + if(!sq) { + fossil_warning("Got a finalizer call for a NULL Th_Query."); + return; + } + for( i = 0; i < sq->nStmt; ++i ){ + st = sq->aStmt[i]; + if(NULL != st){ + fossil_warning("Auto-finalizing unfinalized " + "statement id #%d: %s", + i+1, sqlite3_sql(st)); + Th_query_FinalizeStmt( interp, i+1 ); + } + } + Th_Free(interp, sq->aStmt); + Th_Free(interp, sq); +} + + +/* +** TH command: +** +** query prepare SQL +** +** Returns an opaque statement identifier. +*/ +static int queryPrepareCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + char const * zSql; + sqlite3_stmt * pStmt = NULL; + int rc; + char const * errMsg = NULL; + if( argc!=2 ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "STRING"); + } + zSql = argv[1]; + rc = sqlite3_prepare( g.db, zSql, strlen(zSql), &pStmt, NULL ); + if(SQLITE_OK==rc){ + if(sqlite3_column_count( pStmt ) < 1){ + errMsg = "Only SELECT-like queries are supported."; + rc = SQLITE_ERROR; + sqlite3_finalize( pStmt ); + pStmt = NULL; + } + }else{ + errMsg = sqlite3_errmsg( g.db ); + } + if(SQLITE_OK!=rc){ + assert(NULL != errMsg); + assert(NULL == pStmt); + Th_ErrorMessage(interp, "error preparing SQL:", errMsg, -1); + return TH_ERROR; + } + rc = Th_query_AddStmt( interp, pStmt ); + assert( rc >= 0 && "AddStmt failed."); + Th_SetResultInt( interp, rc ); + return TH_OK; +} + +/* +** Tries to convert arg, which must be argLen bytes long, to a +** statement handle id and, in turn, to a sqlite3_stmt. On success +** (the argument references a prepared statement) it returns the +** handle and stmtId (if not NULL) is assigned to the integer value of +** arg. On error NULL is returned and stmtId might be modified (if not +** NULL). If stmtId is unmodified after an error then it is not a +** number, else it is a number but does not reference an opened +** statement. +*/ +static sqlite3_stmt * queryStmtHandle(Th_Interp *interp, char const * arg, int argLen, int * stmtId ){ + int rc = 0; + sqlite3_stmt * pStmt = NULL; + if( 0 == Th_ToInt( interp, arg, argLen, &rc ) ){ + if(stmtId){ + *stmtId = rc; + } + pStmt = Th_query_GetStmt( interp, rc ); + if(NULL==pStmt){ + Th_ErrorMessage(interp, "no such statement handle:", arg, -1); + } + } + return pStmt; + +} + +/* +** TH command: +** +** query finalize stmtId +** query stmtId finalize +** +** sqlite3_finalize()s the given statement. +*/ +static int queryFinalizeCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 1 : 2; + char * zSql; + int stId = 0; + char const * arg; + int rc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle"); + } + if(!pStmt){ + arg = argv[1]; + pStmt = queryStmtHandle(interp, arg, argl[1], &stId); + if(!pStmt){ + Th_ErrorMessage(interp, "Not a valid statement handle argument.", NULL, 0); + return TH_ERROR; + } + } + assert( NULL != pStmt ); + rc = Th_query_FinalizeStmt2( interp, pStmt ); + Th_SetResultInt( interp, rc ); + return TH_OK; +} + +/* +** Reports the current sqlite3_errmsg() via TH and returns TH_ERROR. +*/ +static int queryReportDbErr( Th_Interp * interp ){ + char const * msg = sqlite3_errmsg( g.db ); + Th_ErrorMessage(interp, "db error:", msg, -1); + return TH_ERROR; +} + +/* +** Internal helper for fetching statement handle and index parameters. +** The first 4 args should be the args passed to the TH1 callback. +** pStmt must be a pointer to a NULL pointer. pIndex may be NULL or +** a pointer to store the statement index argument in. If pIndex is +** NULL then argc is asserted to be at least 2, else it must be at +** least 3. +** +** On success it returns 0, sets *pStmt to the referenced statement +** handle, and pIndex (if not NULL) to the integer value of argv[2] +** argument. On error it reports the error via TH, returns non-0, and +** modifies neither pStmt nor pIndex. +*/ +static int queryStmtIndexArgs( + Th_Interp * interp, + int argc, + char const ** argv, + int *argl, + sqlite3_stmt ** pStmt, + int * pIndex ){ + int index = 0; + sqlite3_stmt * stmt; + if( !pIndex ){ + if(argc<2){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle"); + } + }else{ + if( argc<3 ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index"); + } + if( 0 != Th_ToInt( interp, argv[2], argl[2], &index ) ){ + return TH_ERROR; + } + } + stmt = *pStmt ? *pStmt : queryStmtHandle(interp, argv[1], argl[1], NULL); + if( NULL == stmt ){ + return TH_ERROR; + }else{ + *pStmt = stmt; + if( pIndex ){ + *pIndex = index; + } + return 0; + } +} + +/* +** TH command: +** +** query step stmtId +** query stmtId step +** +** Steps the given statement handle. Returns 0 at the end of the set, +** a positive value if it fetches a row, and throws on error. +*/ +static int queryStepCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 1 : 2; + int rc = 0; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle"); + } + if(!pStmt && 0 != queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, NULL)){ + return TH_ERROR; + } + assert(NULL != pStmt); + rc = sqlite3_step( pStmt ); + switch(rc){ + case SQLITE_ROW: + rc = 1; + break; + case SQLITE_DONE: + rc = 0; + break; + default: + return queryReportDbErr( interp ); + } + Th_SetResultInt( interp, rc ); + return TH_OK; +} + +/* +** TH command: +** +** query StmtId reset +** query reset StmtId +** +** Equivalent to sqlite3_reset(). +*/ +static int queryResetCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int const rc = sqlite3_reset(pStmt); + if(rc){ + Th_ErrorMessage(interp, "Reset of statement failed.", NULL, 0); + return TH_ERROR; + }else{ + return TH_OK; + } +} + + +/* +** TH command: +** +** query col string stmtId Index +** query stmtId col string Index +** query stmtId col Index string +** +** Returns the result column value at the given 0-based index. +*/ +static int queryColStringCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 2 : 3; + char const * val; + int valLen; + if( index >= 0 ) --requireArgc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + }else if(index<0){ + Th_ToInt(interp, argv[1], argl[1], &index); + } + if(index < 0){ + return TH_ERROR; + } + val = sqlite3_column_text( pStmt, index ); + valLen = val ? sqlite3_column_bytes( pStmt, index ) : 0; + Th_SetResult( interp, val, valLen ); + return TH_OK; +} + +/* +** TH command: +** +** query col int stmtId Index +** query stmtId col int Index +** query stmtId col Index int +** +** Returns the result column value at the given 0-based index. +*/ +static int queryColIntCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 2 : 3; + int rc = 0; + if( index >= 0 ) --requireArgc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + }else if(index<0){ + Th_ToInt(interp, argv[1], argl[1], &index); + } + if(index < 0){ + return TH_ERROR; + } + Th_SetResultInt( interp, sqlite3_column_int( pStmt, index ) ); + return TH_OK; +} + +/* +** TH command: +** +** query col double stmtId Index +** query stmtId col double Index +** query stmtId col Index double +** +** Returns the result column value at the given 0-based index. +*/ +static int queryColDoubleCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 2 : 3; + double rc = 0; + if( index >= 0 ) --requireArgc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + }else if(index<0){ + Th_ToInt(interp, argv[1], argl[1], &index); + } + if(index < 0){ + return TH_ERROR; + } + Th_SetResultDouble( interp, sqlite3_column_double( pStmt, index ) ); + return TH_OK; +} + +/* +** TH command: +** +** query col isnull stmtId Index +** query stmtId col isnull Index +** query stmtId col Index isnull +** +** Returns non-0 if the given 0-based result column index contains +** an SQL NULL value, else returns 0. +*/ +static int queryColIsNullCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 2 : 3; + if( index >= 0 ) --requireArgc; + double rc = 0; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + }else if(index<0){ + Th_ToInt(interp, argv[1], argl[1], &index); + } + if(index < 0){ + return TH_ERROR; + } + Th_SetResultInt( interp, + SQLITE_NULL==sqlite3_column_type( pStmt, index ) + ? 1 : 0); + return TH_OK; +} + +/* +** TH command: +** +** query col type stmtId Index +** query stmtId col type Index +** query stmtId col Index type +** +** Returns the sqlite type identifier for the given 0-based result +** column index. The values are available in TH as $SQLITE_NULL, +** $SQLITE_INTEGER, etc. +*/ +static int queryColTypeCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 2 : 3; + if( index >= 0 ) --requireArgc; + double rc = 0; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + }else if(index<0){ + Th_ToInt( interp, argv[1], argl[1], &index ); + } + if(index < 0){ + return TH_ERROR; + } + Th_SetResultInt( interp, sqlite3_column_type( pStmt, index ) ); + return TH_OK; +} + +/* +** TH command: +** +** query col count stmtId +** query stmtId col count +** +** Returns the number of result columns in the query. +*/ +static int queryColCountCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 1 : 2; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle"); + } + if(!pStmt){ + pStmt = queryStmtHandle(interp, argv[1], argl[1], NULL); + if( NULL == pStmt ){ + return TH_ERROR; + } + } + rc = sqlite3_column_count( pStmt ); + Th_SetResultInt( interp, rc ); + return TH_OK; +} + +/* +** TH command: +** +** query col name stmtId Index +** query stmtId col name Index +** query stmtId col Index name +** +** Returns the result column name at the given 0-based index. +*/ +static int queryColNameCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 2 : 3; + char const * val; + int rc = 0; + if( index >= 0 ) --requireArgc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + }else if(index<0){ + Th_ToInt( interp, argv[1], argl[1], &index ); + } + if(index < 0){ + return TH_ERROR; + } + assert(NULL!=pStmt); + val = sqlite3_column_name( pStmt, index ); + if(NULL==val){ + Th_ErrorMessage(interp, "Column index out of bounds(?):", argv[2], -1); + return TH_ERROR; + }else{ + Th_SetResult( interp, val, strlen( val ) ); + return TH_OK; + } +} + +/* +** TH command: +** +** query col time stmtId Index format +** query stmtId col name Index format +** query stmtId col Index name format +** +** Returns the result column name at the given 0-based index. +*/ +static int queryColTimeCmd( + Th_Interp *interp, + void *ctx, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)ctx; + int minArgs = pStmt ? 3 : 4; + int argPos; + char const * val; + char * fval; + int i, rc = 0; + char const * fmt; + Blob sql = empty_blob; + if( index >= 0 ) --minArgs; + if( argc<minArgs ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index Format"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + argPos = 3; + }else if(index<0){ + Th_ToInt( interp, argv[1], argl[1], &index ); + argPos = 2; + }else{ + argPos = 1; + } + if(index < 0){ + return TH_ERROR; + } + val = sqlite3_column_text( pStmt, index ); + fmt = argv[argPos++]; + assert(NULL!=pStmt); + blob_appendf(&sql,"SELECT strftime(%Q,%Q", + fmt, val); + if(argc>argPos){ + for(i = argPos; i < argc; ++i ){ + blob_appendf(&sql, ",%Q", argv[i]); + } + } + blob_append(&sql, ")", 1); + fval = db_text(NULL,"%s", sql.aData); + + blob_reset(&sql); + Th_SetResult( interp, fval, fval ? strlen(fval) : 0 ); + fossil_free(fval); + return 0; +} + +/* +** TH command: +** +** query strftime TimeVal ?Modifiers...? +** +** Acts as a proxy to sqlite3's strftime() SQL function. +*/ +static int queryStrftimeCmd( + Th_Interp *interp, + void *ctx, + int argc, + const char **argv, + int *argl +){ + char const * val; + char * fval; + int i, rc = 0; + int index = -1; + char const * fmt; + Blob sql = empty_blob; + if( argc<3 ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "Format Value ?Modifiers...?"); + } + fmt = argv[1]; + val = argv[2]; + blob_appendf(&sql,"SELECT strftime(%Q,%Q", + fmt, val); + if(argc>3){ + for(i = 3; i < argc; ++i ){ + blob_appendf(&sql, ",%Q", argv[i]); + } + } + blob_append(&sql, ")", 1); + fval = db_text(NULL,"%s", sql.aData); + blob_reset(&sql); + Th_SetResult( interp, fval, fval ? strlen(fval) : 0 ); + fossil_free(fval); + return 0; +} + + +/* +** TH command: +** +** query bind null stmtId Index +** query stmtId bind null Index +** +** Binds a value to the given 1-based parameter index. +*/ +static int queryBindNullCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 2 : 3; + if( index > 0 ) --requireArgc; + int rc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + }else if(index<1){ + Th_ToInt( interp, argv[1], argl[1], &index ); + } + if(index < 1){ + return TH_ERROR; + } + rc = sqlite3_bind_null( pStmt, index ); + if(rc){ + return queryReportDbErr( interp ); + } + Th_SetResultInt( interp, 0 ); + return TH_OK; +} + + +/* +** TH command: +** +** query bind string stmtId Index Value +** query stmtId bind string Index Value +** +** Binds a value to the given 1-based parameter index. +*/ +static int queryBindStringCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 3 : 4; + int rc; + int argPos; + if( index > 0 ) --requireArgc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index Value"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + argPos = 3; + }else if(index<1){ + Th_ToInt( interp, argv[1], argl[1], &index ); + argPos = 2; + }else{ + argPos = 1; + } + if(index < 1){ + return TH_ERROR; + } + rc = sqlite3_bind_text( pStmt, index, argv[argPos], argl[argPos], SQLITE_TRANSIENT ); + if(rc){ + return queryReportDbErr( interp ); + } + Th_SetResultInt( interp, 0 ); + return TH_OK; +} + +/* +** TH command: +** +** query bind int stmtId Index Value +** query stmtId bind int Index Value +** +** Binds a value to the given 1-based parameter index. +*/ +static int queryBindIntCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 3 : 4; + int rc; + int argPos; + int val; + if( index > 0 ) --requireArgc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index Value"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + argPos = 3; + }else if(index<1){ + Th_ToInt( interp, argv[1], argl[1], &index ); + argPos = 2; + }else{ + argPos = 1; + } + if(index < 1){ + return TH_ERROR; + } + if( 0 != Th_ToInt( interp, argv[argPos], argl[argPos], &val ) ){ + return TH_ERROR; + } + + rc = sqlite3_bind_int( pStmt, index, val ); + if(rc){ + return queryReportDbErr( interp ); + } + Th_SetResultInt( interp, 0 ); + return TH_OK; +} + +/* +** TH command: +** +** query bind double stmtId Index Value +** query stmtId bind double Index Value +** +** Binds a value to the given 1-based parameter index. +*/ +static int queryBindDoubleCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Th_Query * sq = Th_query_manager(interp); + int index = sq->colCmdIndex; + sqlite3_stmt * pStmt = (sqlite3_stmt*)p; + int requireArgc = pStmt ? 3 : 4; + int rc; + int argPos; + double val; + if( index > 0 ) --requireArgc; + if( argc!=requireArgc ){ + return Th_WrongNumArgs2(interp, + argv[0], argl[0], + "StmtHandle Index Value"); + } + if(!pStmt){ + queryStmtIndexArgs(interp, argc, argv, argl, &pStmt, &index); + argPos = 3; + }else if(index<1){ + Th_ToInt( interp, argv[1], argl[1], &index ); + argPos = 2; + }else{ + argPos = 1; + } + if(index < 1){ + return TH_ERROR; + } + if( 0 != Th_ToDouble( interp, argv[argPos], argl[argPos], &val ) ){ + return TH_ERROR; + } + + rc = sqlite3_bind_double( pStmt, index, val ); + if(rc){ + return queryReportDbErr( interp ); + } + Th_SetResultInt( interp, 0 ); + return TH_OK; +} + +/* +** TH command: +** +** bind subcommand StmtId... +** bind StmtId subcommand... +** +** This is the top-level dispatcher for the "bind" family of commands. +*/ +static int queryBindTopLevelCmd( + Th_Interp *interp, + void *ctx, + int argc, + const char **argv, + int *argl +){ + int colIndex = -1; + static Th_SubCommand aSub[] = { + {"int", queryBindIntCmd}, + {"double", queryBindDoubleCmd}, + {"null", queryBindNullCmd}, + {"string", queryBindStringCmd}, + {0, 0} + }; + Th_Query * sq = Th_query_manager(interp); + assert(NULL != sq); + if( 1 == argc ){ + Th_WrongNumArgs2( interp, argv[0], argl[0], + "subcommand: int|double|null|string"); + return TH_ERROR; + }else if( 0 == Th_TryInt(interp,argv[1], argl[1], &colIndex) ){ + if(colIndex <0){ + Th_ErrorMessage( interp, "Invalid column index.", NULL, 0); + return TH_ERROR; + } + ++argv; + ++argl; + --argc; + } + sq->colCmdIndex = colIndex; + Th_CallSubCommand2( interp, ctx, argc, argv, argl, aSub ); + +} + +/* +** TH command: +** +** query col subcommand ... +** query StmtId col subcommand ... +** +** This is the top-level dispatcher for the col subcommands. +*/ +static int queryColTopLevelCmd( + Th_Interp *interp, + void *ctx, + int argc, + const char **argv, + int *argl +){ + int colIndex = -1; + static Th_SubCommand aSub[] = { + {"count", queryColCountCmd}, + {"is_null", queryColIsNullCmd}, + {"isnull", queryColIsNullCmd}, + {"name", queryColNameCmd}, + {"double", queryColDoubleCmd}, + {"int", queryColIntCmd}, + {"string", queryColStringCmd}, + {"time", queryColTimeCmd}, + {"type", queryColTypeCmd}, + {0, 0} + }; + static Th_SubCommand aSubWithIndex[] = { + /* + This subset is coded to accept the column index + either before the subcommand name or after it. + If called like (bind StmtId subcommand) then + only these commands will be checked. + */ + {"is_null", queryColIsNullCmd}, + {"isnull", queryColIsNullCmd}, + {"name", queryColNameCmd}, + {"double", queryColDoubleCmd}, + {"int", queryColIntCmd}, + {"string", queryColStringCmd}, + {"time", queryColTimeCmd}, + {"type", queryColTypeCmd}, + {0, 0} + }; + Th_Query * sq = Th_query_manager(interp); + assert(NULL != sq); + if( 1 == argc ){ + Th_WrongNumArgs2( interp, argv[0], argl[0], + "subcommand: " + "count|is_null|isnull|name|" + "double|int|string|time|type"); + return TH_ERROR; + }else if( 0 == Th_TryInt(interp,argv[1], argl[1], &colIndex) ){ + if(colIndex <0){ + Th_ErrorMessage( interp, "Invalid column index.", NULL, 0); + return TH_ERROR; + } + ++argv; + ++argl; + --argc; + } + sq->colCmdIndex = colIndex; + Th_CallSubCommand2( interp, ctx, argc, argv, argl, + (colIndex<0) ? aSub : aSubWithIndex ); +} + + +/* +** TH command: +** +** query subcommand ... +** query StmtId subcommand ... +** +** This is the top-level dispatcher for the query subcommand. +*/ +static int queryTopLevelCmd( + Th_Interp *interp, + void *ctx, + int argc, + const char **argv, + int *argl +){ + int stmtId = 0; + sqlite3_stmt * pStmt = NULL; + static Th_SubCommand aSubAll[] = { + {"bind", queryBindTopLevelCmd}, + {"col", queryColTopLevelCmd}, + {"finalize", queryFinalizeCmd}, + {"prepare", queryPrepareCmd}, + {"reset", queryResetCmd}, + {"step", queryStepCmd}, + {"strftime", queryStrftimeCmd}, + {0, 0} + }; + static Th_SubCommand aSubWithStmt[] = { + /* This subset is coded to deal with being supplied a statement + via pStmt or via one of their args. When called like (query + StmtId ...) only these subcommands will be checked.*/ + {"bind", queryBindTopLevelCmd}, + {"col", queryColTopLevelCmd}, + {"step", queryStepCmd}, + {"finalize", queryFinalizeCmd}, + {"reset", queryResetCmd}, + {0, 0} + }; + + + assert( NULL != Th_query_manager(interp) ); + if( 1 == argc ){ + Th_WrongNumArgs2( interp, argv[0], argl[0], + "subcommand: bind|col|finalize|prepare|reset|step|strftime"); + return TH_ERROR; + }else if( 0 == Th_TryInt(interp,argv[1], argl[1], &stmtId) ){ + ++argv; + ++argl; + --argc; + pStmt = Th_query_GetStmt( interp, stmtId ); + } + + Th_CallSubCommand2( interp, pStmt, argc, argv, argl, + pStmt ? aSubWithStmt : aSubAll ); +} + +/* +** Registers the "query" API with the given interpreter. Returns TH_OK +** on success, TH_ERROR on error. +*/ +int th_register_query(Th_Interp *interp){ + enum { BufLen = 100 }; + char buf[BufLen]; + int i, l; +#define SET(K) l = snprintf(buf, BufLen, "%d", K); \ + Th_SetVar( interp, #K, strlen(#K), buf, l ); + SET(SQLITE_BLOB); + SET(SQLITE_FLOAT); + SET(SQLITE_INTEGER); + SET(SQLITE_NULL); + SET(SQLITE_TEXT); +#if 0 + /* so far we don't need these in script code */ + SET(SQLITE_ERROR); + SET(SQLITE_DONE); + SET(SQLITE_OK); + SET(SQLITE_ROW); +#endif +#undef SET + int rc = TH_OK; + static Th_Command_Reg aCommand[] = { + {"query", queryTopLevelCmd, 0}, + {0, 0, 0} + }; + rc = Th_RegisterCommands( interp, aCommand ); + if(TH_OK==rc){ + Th_Query * sq = Th_Malloc(interp, sizeof(Th_Query)); + if(!sq){ + rc = TH_ERROR; + }else{ + assert( NULL == sq->aStmt ); + assert( 0 == sq->nStmt ); + Th_SetData( interp, Th_Query_KEY, sq, finalizerSqlite ); + assert( sq == Th_query_manager(interp) ); + } + } + return rc; +} + +#endif +/* end TH_ENABLE_QUERY */ + /* ** Make sure the interpreter has been initialized. Initialize it if ** it has not been already. ** ** The interpreter is stored in the g.interp global variable. */ void Th_FossilInit(void){ - static struct _Command { - const char *zName; - Th_CommandProc xProc; - void *pContext; - } aCommand[] = { + /* The fossil-internal Th_Vtab instance. */ + static Th_Vtab vtab = { xRealloc, {/*out*/ + NULL /*write()*/, + NULL/*dispose()*/, + NULL/*pState*/, + 1/*enabled*/ + } + }; + + static PutsCmdData puts_Html = {0, 0, 0}; + static PutsCmdData puts_Normal = {1, 0, 0}; + static Th_Command_Reg aCommand[] = { {"anycap", anycapCmd, 0}, {"combobox", comboboxCmd, 0}, + {"date", dateCmd, 0}, {"enable_output", enableOutputCmd, 0}, - {"linecount", linecntCmd, 0}, {"hascap", hascapCmd, 0}, {"hasfeature", hasfeatureCmd, 0}, + {"html", putsCmd, &puts_Html}, {"htmlize", htmlizeCmd, 0}, - {"date", dateCmd, 0}, - {"html", putsCmd, 0}, - {"puts", putsCmd, (void*)1}, - {"wiki", wikiCmd, 0}, + {"linecount", linecntCmd, 0}, + {"puts", putsCmd, &puts_Normal}, +#if 0 + {"render", renderCmd, 0}, +#endif {"repository", repositoryCmd, 0}, + {"wiki", wikiCmd, 0}, + {0, 0, 0} }; if( g.interp==0 ){ int i; + if(g.cgiOutput){ + vtab.out.xWrite = Th_Output_f_cgi_content; + }else{ + vtab.out = Th_Vtab_OutputMethods_FILE; + vtab.out.pState = stdout; + } + vtab.out.enabled = enableOutput; g.interp = Th_CreateInterp(&vtab); th_register_language(g.interp); /* Basic scripting commands. */ #ifdef FOSSIL_ENABLE_TCL if( getenv("TH1_ENABLE_TCL")!=0 || db_get_boolean("tcl", 0) ){ th_register_tcl(g.interp, &g.tcl); /* Tcl integration commands. */ } #endif - for(i=0; i<sizeof(aCommand)/sizeof(aCommand[0]); i++){ - if ( !aCommand[i].zName || !aCommand[i].xProc ) continue; - Th_CreateCommand(g.interp, aCommand[i].zName, aCommand[i].xProc, - aCommand[i].pContext, 0); - } +#ifdef TH_ENABLE_OB + th_register_ob(g.interp); +#endif +#ifdef TH_ENABLE_QUERY + th_register_query(g.interp); +#endif +#ifdef TH_ENABLE_ARGV + th_register_argv(g.interp); +#endif + Th_RegisterCommands( g.interp, aCommand ); + Th_Eval( g.interp, 0, "proc incr {name {step 1}} {\n" + "upvar $name x\n" + "set x [expr $x+$step]\n" + "}", -1 ); } } /* ** Store a string value in a variable in the interpreter. @@ -553,30 +2220,33 @@ return i; } /* ** The z[] input contains text mixed with TH1 scripts. -** The TH1 scripts are contained within <th1>...</th1>. -** TH1 variables are $aaa or $<aaa>. The first form of -** variable is literal. The second is run through htmlize -** before being inserted. +** The TH1 scripts are contained within <th1>...</th1>. +** +** If flags does NOT contain the Th_Render_Flags_NO_DOLLAR_DEREF bit +** then TH1 variables are $aaa or $<aaa>. The first form of variable +** is literal. The second is run through htmlize before being +** inserted. ** ** This routine processes the template and writes the results -** on either stdout or into CGI. +** via Th_Output(). */ -int Th_Render(const char *z){ +int Th_Render(const char *z, int flags){ int i = 0; int n; int rc = TH_OK; - char *zResult; + char const *zResult; + char doDollar = !(flags & Th_Render_Flags_NO_DOLLAR_DEREF); Th_FossilInit(); while( z[i] ){ - if( z[i]=='$' && (n = validVarName(&z[i+1]))>0 ){ + if( doDollar && z[i]=='$' && (n = validVarName(&z[i+1]))>0 ){ const char *zVar; int nVar; int encode = 1; - sendText(z, i, 0); + sendText(g.interp, z, i, 0); if( z[i+1]=='<' ){ /* Variables of the form $<aaa> are html escaped */ zVar = &z[i+2]; nVar = n-2; }else{ @@ -583,17 +2253,17 @@ /* Variables of the form $aaa are output raw */ zVar = &z[i+1]; nVar = n; encode = 0; } - rc = Th_GetVar(g.interp, (char*)zVar, nVar); + rc = Th_GetVar(g.interp, zVar, nVar); z += i+1+n; i = 0; - zResult = (char*)Th_GetResult(g.interp, &n); - sendText((char*)zResult, n, encode); + zResult = Th_GetResult(g.interp, &n); + sendText(g.interp, zResult, n, encode); }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){ - sendText(z, i, 0); + sendText(g.interp, z, i, 0); z += i+5; for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){} rc = Th_Eval(g.interp, 0, (const char*)z, i); if( rc!=TH_OK ) break; z += i; @@ -602,28 +2272,45 @@ }else{ i++; } } if( rc==TH_ERROR ){ - sendText("<hr><p class=\"thmainError\">ERROR: ", -1, 0); - zResult = (char*)Th_GetResult(g.interp, &n); - sendText((char*)zResult, n, 1); - sendText("</p>", -1, 0); + sendText(g.interp, "<hr><p class=\"thmainError\">ERROR: ", -1, 0); + zResult = Th_GetResult(g.interp, &n); + sendText(g.interp, zResult, n, 1); + sendText(g.interp, "</p>", -1, 0); }else{ - sendText(z, i, 0); + sendText(g.interp, z, i, 0); } return rc; } /* ** COMMAND: test-th-render +** COMMAND: th1 +** +** Processes a file provided on the command line as a TH1-capable +** script/page. Output is sent to stdout or the CGI output buffer, as +** appropriate. The input file is assumed to be text/wiki/HTML content +** which may contain TH1 tag blocks and variables in the form $var or +** $<var>. Each block is executed in the same TH1 interpreter +** instance. +** +** ACHTUNG: not all of the $variables which are set in CGI mode +** are available via this (CLI) command. +** */ void test_th_render(void){ Blob in; if( g.argc<3 ){ usage("FILE"); + assert(0 && "usage() does not return"); } - db_open_config(0); /* Needed for global "tcl" setting. */ blob_zero(&in); + db_open_config(0); /* Needed for global "tcl" setting. */ +#ifdef TH_ENABLE_QUERY + db_find_and_open_repository(OPEN_ANY_SCHEMA,0) + /* required for th1 query API. */; +#endif blob_read_from_file(&in, g.argv[2]); - Th_Render(blob_str(&in)); + Th_Render(blob_str(&in), Th_Render_Flags_DEFAULT); } Index: src/tkt.c ================================================================== --- src/tkt.c +++ src/tkt.c @@ -328,11 +328,11 @@ if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br />\n", -1); ticket_init(); initializeVariablesFromDb(); zScript = ticket_viewpage_code(); if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW_SCRIPT<br />\n", -1); - Th_Render(zScript); + Th_Render(zScript, Th_Render_Flags_DEFAULT ); if( g.thTrace ) Th_Trace("END_TKTVIEW<br />\n", -1); zFullName = db_text(0, "SELECT tkt_uuid FROM ticket" " WHERE tkt_uuid GLOB '%q*'", zUuid); @@ -540,11 +540,11 @@ Th_Store("login", g.zLogin); Th_Store("date", db_text(0, "SELECT datetime('now')")); Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zNewUuid, 0); if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br />\n", -1); - if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){ + if( Th_Render(zScript, Th_Render_Flags_DEFAULT)==TH_RETURN && !g.thTrace && zNewUuid ){ cgi_redirect(mprintf("%s/tktview/%s", g.zTop, zNewUuid)); return; } @ </form> if( g.thTrace ) Th_Trace("END_TKTVIEW<br />\n", -1); @@ -607,11 +607,11 @@ Th_Store("login", g.zLogin); Th_Store("date", db_text(0, "SELECT datetime('now')")); Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0); Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0); if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br />\n", -1); - if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){ + if( Th_Render(zScript, Th_Render_Flags_DEFAULT)==TH_RETURN && !g.thTrace && zName ){ cgi_redirect(mprintf("%s/tktview/%s", g.zTop, zName)); return; } @ </form> if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT<br />\n", -1); ADDED test/th1-ob-1.th1 Index: test/th1-ob-1.th1 ================================================================== --- test/th1-ob-1.th1 +++ test/th1-ob-1.th1 @@ -0,0 +1,17 @@ +<th1> +set i 0 +set max 6 +for {} {$i < $max} {incr i} { + ob start + puts "this is level " [ob level] + set buf($i) [ob get] +} + +for {set i [expr $max-1]} {$i >= 0} {incr i -1} { + ob pop +} +for {set i [expr $max-1]} {$i >= 0} {incr i -1} { + puts buf($i) = $buf($i) "\n" +} +puts "buffering level = " [ob level] \n +</th1> ADDED test/th1-query-api-1.th1 Index: test/th1-query-api-1.th1 ================================================================== --- test/th1-query-api-1.th1 +++ test/th1-query-api-1.th1 @@ -0,0 +1,157 @@ +This is not a formal test suite, but a tinkering ground. +Run it through "fossil test-th-render THIS_FILE". +<th1> +set stmt [query prepare {SELECT login, cap FROM user}] +set colCount [query $stmt col count] +puts "query column count: ${colCount}\n" +puts "stmt id=${stmt}\n" + +proc noop {} {} +proc incr {name {step 1}} { + upvar $name x + set x [expr $x+$step] +} + + +set sep " " +set i 0 +set colNames(0) 0 +for {set i 0} {$i < $colCount} {incr i} { + set colNames($i) [query $stmt col name $i] + puts "colNames($i)=" $colNames($i) "\n" +} + +for {set row 0} {[query $stmt step]} {incr row} { + for {set i 0} {$i < $colCount} {incr i} { + if {$i > 0} { + puts $sep + } else { + puts "#$row: $sep" + } + puts $colNames($i) = [query $stmt col string $i] + } + puts "\n" +} +unset row + +query $stmt finalize +#query finalize $stmt + + +proc query_step_each {{stmt} {callback}} { + set colNames(0) 0 + set colCount [query $stmt col count] + for {set i 0} {$i < $colCount} {incr i} { + set colNames($i) [query $stmt col name $i] + } + upvar cb $callback + for {set row 0} {[query $stmt step]} {incr row} { + #puts "Calling callback: $stmt $colCount colNames\n" + $callback $stmt $colCount + } +} + +set sql {SELECT uid, login FROM user WHERE uid!=?} +#set sql {SELECT uid, login FROM user WHERE login=?} +#set sql {SELECT tagid, value, null FROM tagxref WHERE value IS ? LIMIT 3} +set stmt [query prepare $sql] +puts "stmt ID=" $stmt "\n" +#query bind int $stmt 1 3 +#query $stmt bind 1 int 3 +query $stmt bind 1 string 3 +#query $stmt bind string 1 3 +#set stmt [query prepare $sql] +#query $stmt bind 1 string 1 +#set stmt [query prepare $sql] +#query $stmt bind null 1 +puts "USER LIST:\n" +catch { + proc my_each {stmt colCount} { + upvar 2 sep sep + puts [query $stmt col int 0] " (type=" [query $stmt col type 0] ")" $sep + puts [query $stmt col double 0] $sep + puts [query $stmt col string 1] " (type=" [query $stmt col type 1] ")" $sep + puts "isnull 0 ?= " [query $stmt col is_null 0] $sep + puts "isnull 2 ?= " [query col is_null $stmt 2] +# for {set i 0} {$i < $colCount} {incr i} { +# if {$i > 0} { puts $sep } +# } + puts "\n" +# error "hi!" + } + query_step_each $stmt my_each +# query reset $stmt +# query $stmt reset +# query_step_each $stmt { +# proc each {stmt cc} { puts hi "\n" } +# } + return 0 +} rc +query finalize $stmt +if { 0 != $rc } { + puts "ERROR: $rc\n" +} + +set consts [list SQLITE_BLOB SQLITE_FLOAT SQLITE_INTEGER SQLITE_NULL SQLITE_TEXT] +#set consts $SQLITE_CONSTANTS +puts consts = $consts "\n" +for {set i 0} {$i < [llength $consts]} {incr i} { + set x [lindex $consts $i] + puts \$$x = [expr \$$x] "\n" +} + +set ARGC [argv len] +puts ARGC = $ARGC "\n" +for {set i 0} {$i < $ARGC} {incr i} { + puts "argv at $i = " [argv at $i] \n +} + +set magicDefault hi +set optA [argv string AA a $magicDefault] +puts "argv string AA = " $optA \n + +set optA [argv bool BB b 0] +puts "argv bool BB = " $optA \n + +set exception 0 +catch { + argv int noSuchOptionAndNoDefault +} exception +puts exception = $exception "\n" + +proc multiStmt {} { + set max 5 + set i 0 + set s(0) 0 + for {set i 0} {$i < $max} {incr i} { + set s($i) [query prepare "SELECT $i"] + puts "s($i) = $s($i)\n" + } + for {set i 0} {$i < $max} {incr i} { + query $s($i) step + } + for {set i 0} {$i < $max} {incr i} { + puts "closing stmt $s($i)\n" + query $s($i) finalize + } + + puts "Preparing again\n" + + for {set i 0} {$i < $max} {incr i} { + set s($i) [query prepare "SELECT $i"] + puts "s($i) = $s($i)\n" + } + for {set i 0} {$i < $max} {incr i} { + query $s($i) step + } + puts "Closing again\n" + + for {set i 0} {$i < $max} {incr i} { + puts "closing stmt $s($i)\n" + query $s($i) finalize + } +} +multiStmt + +puts "If you got this far, you win!\n" +</th1> ADDED test/th1-query-api-2.th1 Index: test/th1-query-api-2.th1 ================================================================== --- test/th1-query-api-2.th1 +++ test/th1-query-api-2.th1 @@ -0,0 +1,70 @@ +<th1> +catch { + set stmt [query prepare { + SELECT login, cap, cexpire, mtime, NULL FROM user + WHERE uid<? AND cexpire IS NOT NULL + AND mtime IS NOT NULL + }] + puts "stmt ID=$stmt\n" +# query bind int $stmt 1 2 +# query $stmt bind int 1 2 + query $stmt bind 1 int 5 +# segfault: query bind 1 int $stmt 2 + set sep "\n" + for {} {[query $stmt step]} {} { + puts [query $stmt col string 0] $sep + puts [query $stmt col time 2 {%Y%m%d @ %H:%M:%S}] $sep + puts [query $stmt col time 2 {%Y%m%d @ %H:%M:%S} {+10 years}] $sep + puts [query $stmt col 2 time {%Y%m%d @ %H:%M:%S} {+10 years}] $sep +# puts [query col time $stmt 2 %s] $sep + puts [query col time $stmt 3 %s unixepoch] $sep +# puts [query strftime %s [query col string $stmt 3] unixepoch] + puts [query strftime %s [query col string $stmt 3] unixepoch] $sep + puts [query strftime {%Y%m%d @ %H:%M:%S} [query col string $stmt 2] {+10 years}] $sep + puts "\n" + puts "old isnull: " [query col isnull $stmt 4] "\n" + + puts "new old isnull: " [query col 4 isnull $stmt] "\n" + puts "new isnull: " [query $stmt col isnull 4] "\n" + puts "new new isnull: " [query $stmt col 4 isnull] "\n" + + puts "old col type: " [query col type $stmt 1] "\n" + puts "new col type: " [query $stmt col type 1] "\n" + puts "new new col type: " [query $stmt col 1 type] "\n" + + puts "old col name: " [query col name $stmt 1] "\n" + puts "new col name: " [query $stmt col name 1] "\n" + puts "new new col name: " [query $stmt col 1 name] "\n" + + puts "old col double: " [query col double $stmt 2] "\n" + puts "new col double: " [query $stmt col double 2] "\n" + puts "new new col double: " [query $stmt col 2 double] "\n" + + puts "old col int: " [query col int $stmt 2] "\n" + puts "new col int: " [query $stmt col int 2] "\n" + puts "new new col int: " [query $stmt col 2 int] "\n" + + puts "old col string: " [query col string $stmt 2] "\n" + puts "new col string: " [query $stmt col string 2] "\n" + puts "new new col string: " [query $stmt col 2 string] "\n" + + puts "\n" + } + + puts "alt-form col count: " [query $stmt col count] "\n" + query finalize $stmt + return 0 +} rc +if {0 != $rc} { + puts "ERROR: $rc\n" +} +puts "Done!\n" + + +ob start +puts buffered +set x [ob get pop] +puts x=$x + + +</th1> ADDED test/th1-variadic-proc.th1 Index: test/th1-variadic-proc.th1 ================================================================== --- test/th1-variadic-proc.th1 +++ test/th1-variadic-proc.th1 @@ -0,0 +1,10 @@ +<th1> +proc vfunc {args} { + set argc [llength $args] + puts "argc=$argc Check this for a memleak when args length > 0\n" +} +vfunc +vfunc 1 +vfunc 1 2 +vfunc 1 2 3 +</th1> ADDED www/th1_argv.wiki Index: www/th1_argv.wiki ================================================================== --- www/th1_argv.wiki +++ www/th1_argv.wiki @@ -0,0 +1,80 @@ +<h1>TH1 "argv" API</h1> + +The "argv" API provides features for accessing command-line arguments +and GET/POST values. They (unfortunately) do not provide access to +POST data submitted in JSON mode (which fossil internally doesn't really +know about). + +Example usage: + +<nowiki><pre> +<th1> +set argc [argv len] +set appName [argv at 0] +# Fetch --foo|-f argument: +set foo [argv getstr foo f "default value"] +<th1> +</pre></nowiki> + +(Note that fossil does not actually care if an argument starts +with 1 or 2 dashes. The convention of using 1 for "short-form" +flags and 2 for "long-form" is purely historical.) + +The various subcommands are described below... + +<h2>len</h2> + +Returns the number of arguments. + +<nowiki><pre> +set argc [argv len] +</pre></nowiki> + + +<h2>at</h2> + +Fetches the argument at the given index (0-based). + +<nowiki><pre> +set arg [argv at 3] +</pre></nowiki> + +The fossil binary's name is stored in argument #0. + +<h2>getstr|string</h2> + +Searches for a CLI/GET/POST parameter. In CLI this function has some +non-intuitive behaviour inherited from fossil's internals: once a +flag/parameter is fetched, it is removed from the internal arguments +list, meaning that this function will never see it a second time. + +<nowiki><pre> +set something [argv string "something" "S" "default"] +</pre></nowiki> + +If no default value is provided, an error is triggered if the value is +not found. + +If you do not want to search for a short-form flag, set it to an empty +string. + +NOTE: flag checking does not work in CGI mode when using <em>upper-case</em> +flags (fossil treats upper-case names as environment variables). + +<h2>getbool|bool</h2> + +Works almost like <tt>getstr</tt> but searches for boolean flags. CLI boolean flags +have no explicit value, and are "true" if the are set at all. + +<nowiki><pre> +set doSomething [argv bool "do-something" "D" 0] +</pre></nowiki> + +<h2>getint|int</h2> + +Works almost like <tt>getstr</tt> but searches for integer flags. + + +<nowiki><pre> +set limit [argv int "limit" "L" 10] +</pre></nowiki> ADDED www/th1_ob.wiki Index: www/th1_ob.wiki ================================================================== --- www/th1_ob.wiki +++ www/th1_ob.wiki @@ -0,0 +1,96 @@ +<h1>TH1 "ob" (Output Buffering) API</h1> + +The "ob" API mimics the +[http://php.net/manual/en/function.ob-start.php|PHP output buffering] fairly closely, +and provides these features: + + * Redirect output from <tt>puts</tt> and friends to a buffer. + * Fetch the buffer as a string or discard it (as you wish). + * Supports nesting buffering arbitrarily deep, but each level which gets opened must also be closed by the scripter. + +Example usage: + +<nowiki><pre> +<th1> +puts "this is unbuffered" +ob push # or: ob start (same thing) +# all output until the next ob start|end gets collected +# in a buffer... +</th1> + +this is buffered + +<th1> +puts "current buffer level = " [ob level] "\n" +puts "this part is also collected in the buffer." + +# Collect the buffer's contents: +set buf [ob get pop] +# That is equivalent to: +# set buf [ob get] +# ob pop + +puts "\nThis is now unbuffered, but we buffered: $buf\n" +</th1> +</pre></nowiki> + +The functions are summarized below... + +<h2>ob push|start</h2> + +<tt>push</tt> and <tt>start</tt> are aliases ("start" comes from the PHP API, but +"push" is probably more natural to those working with th1). + +<tt>ob start</tt> pushes a level of buffering onto the buffer stack, such that +future calls which generate output through the th1-internal mechanism will have it +transparently redirected to the current buffer. + +It is important that every call to <tt>ob start</tt> be followed up (eventually) +by either <tt>ob end</tt> or <tt>ob get end</tt>. + +<h2>ob pop|end</h2> + +<tt>pop</tt> and <tt>end</tt> are aliases ("end" comes from the PHP API, but +"pop" is probably more natural to those working with th1). + +This discards any current buffered contents and reverts the output state to +the one it had before the previous <tt>ob start</tt>. i.e. that might be another +buffering level or it might be the th1-normal output mechanism. + +The global resources associated with buffering are cleaned up when the +last buffering level is left (and re-created as needed when a new +level is started). + +<h2>ob clean</h2> + +This discards the current contents of the current buffer level but +does not change the buffer stack level. + +<h2>ob get</h2> + +This fetches the current contents as a string. It optionally accepts +either <tt>end</tt> (or its alias <tt>pop</tt>) or <tt>clean</tt>, in which cases it behaves like +either <tt>ob end|pop</tt> or <tt>ob clean</tt>, respectively, in addition +to returning the buffer contents. i.e. <tt>ob get clean</tt> will +fetch the contents and clean up the buffer, but does not change the +buffering level, whereas <tt>ob get end|pop</tt> pops the buffer off the +stack after fetching its contents. + +<h2>ob level</h2> + +Returns the current buffering level (0 if not buffering). + +<h2>ob flush</h2> + +It is not expected that this will be useful all that often, but for +the cases where it is, here's how it works: this behaves as if we +fetched the buffer state (<tt>ob get</tt>), reverted TH1 to its +previous output mechanism, push the buffer state to TH1, revert TH1 +<em>back</em> to the current buffering state, and then clear the +current buffer contents (like <tt>ob clean</tt>). This does not change +the buffering level, though it temporarily behaves as if it does. + +In other words, this function pushes the current buffer contents to the +next-lower output mechanism (which may be another ob buffering level, +fossil's internal CGI output buffer, or it might be be +<tt>fwrite(stdout)</tt>). ADDED www/th1_query.wiki Index: www/th1_query.wiki ================================================================== --- www/th1_query.wiki +++ www/th1_query.wiki @@ -0,0 +1,199 @@ +<h1>TH1 "query" API</h1> + +The "query" API provides limited access to the fossil database. +It restricts usage to queries which return result columns (i.e. +<tt>SELECT</tt> and friends). +Example usage: + +<nowiki><pre> +<th1> +catch { + set stmt [query prepare "SELECT login, cap FROM user"] + puts "stmt ID=$stmt\n" + for {} {[query $stmt step]} {} { + puts [query $stmt col string 0] " " [query $stmt col string 1] \n + } + query $stmt finalize + return 0 +} rc +if {0 != $rc} { + puts "ERROR: $rc\n" +} +<th1> +</pre></nowiki> + +The various subcommands are summarized in the following subsections, and here +are some notes regarding calling conventions: + +The (bind, col, step, finalize) functions accept their statement ID argument +either right after the "query" command or right after the final subcommand. +The following examples demonstrate this: + +<nowiki><pre> +query $stmt step +query step $stmt + +query $stmt finalize +query finalize $stmt + +query col string $stmt 1 +query $stmt col string 1 + +query bind string $stmt 1 "foo" +query $stmt bind string 1 "foo" +</pre></nowiki> + +The "prefered" form is: + +<nowiki><pre> +query StmtId command ... +</pre></nowiki> + +(Why, then, are both forms accepted? Because the "preferred" form only +evolved only after using the first form in script code.) + + +<h2>prepare</h2> + +This subcommand prepares a query for execution. It returns a statement handle +ID which must be passed to any other functions using the API. + +All prepared statements must be <tt>finalize</tt>d when they have outlived +their usefulness. + +<nowiki><pre> +set stmt [query prepare {SELECT ...}] +... +query $stmt finalize +</pre></nowiki> + + +<h2>finalize</h2> + +Releases all resources associated with the statement. Note that future +calls to <tt>prepare</tt> might re-use the same statement statement +ID. + +<nowiki><pre> +set stmt [query prepare "SELECT ..."] +... +query $stmt finalize +</pre></nowiki> + + +<h2>step</h2> + +This subcommand steps the result set by one row. It returns 0 +at the end of the set, a positive value if a new row is available, +and throws for any other condition. + +<nowiki><pre> +for {} {[query $stmt step]} {} { + puts [query $stmt col string 0] "\n" +} +</pre></nowiki> + + +<h2>reset</h2> + +Resets a query so that it can be executed again. This is only needed when +binding parameters in a loop - call it at the end of each loop iteration. + +<nowiki><pre> +query $stmt reset +query reset $stmt +</pre></nowiki> + + +<h2>bind xxx</h2> + +The <tt>bind xxx</tt> family of subcommands attach values to queries +before stepping through them. The subcommands include: + + * <tt>bind StmtId int Index Value</tt> + * <tt>bind StmtId double Index Value</tt> + * <tt>bind StmtId null Index</tt> + * <tt>bind StmtId string Index Value</tt> + +Note that all of those optionally accept the statement handle directly after +the "query" command (before the "col" subcommand). e.g. +<tt>query bind null $stmt 1</tt> and +<tt>query $stmt bind null 1</tt> are equivalent. They also accept the column index +either before or after the type name, e.g. +<tt>query $stmt bind 1 string ...</tt> and <tt>query $stmt bind string 1 ...</tt> are equivalent. + + +Achtung: the bind API uses 1-based indexes, just like SQL does. + +<nowiki><pre> +set stmt [query prepare "SELECT ... WHERE user=?"] +query $stmt bind int 1 drh +if {[query $stmt step]} { + puts [query $stmt col string 0] "\n" +} +query $stmt finalize +</pre></nowiki> + + +<h2>col xxx</h2> + +The <tt>col xxx</tt> familys of subcommands are for fetching values and metadata from result rows. + + * <tt>col StmtId count</tt> Returns the number of result columns in the statement. + * <tt>col StmtId isnull Index</tt> Returns non-0 if the given column contains an SQL NULL value. + * <tt>col StmtId (double|int|string) Index</tt> Fetches a column's value as either a number or string. + * <tt>col StmtId time Index Format Modifiers</tt> Formats a time value. See below. + * <tt>col StmtId type Index</tt> Returns the given column's type as a value from the <tt>SQLITE_TYPENAME</tt> family of constants. + +Note that all of those optionally accept the statement handle directly after +the "query" command (before the "col" subcommand). e.g. +<tt>query $stmt col count</tt> and +<tt>query col count $stmt</tt> are equivalent. They also accept the column index +either before or after the type name, e.g. +<tt>query $stmt col 1 string</tt> and <tt>query $stmt col string 1</tt> are equivalent. + +Achtung: the col API uses 0-based indexes, just like SQL does. + +<h3>col time</h3> + +This function is a proxy for sqlite3's +<tt>[http://www.sqlite.org/lang_datefunc.html|strftime()]</tt> function. It is used like this: + + +<nowiki><pre> +query $stmt col time $index {%Y%m%d @ %H:%M:%S} +</pre></nowiki> + +Any remaining arguments are treated as "modifiers" and passed as-is to strfmtime. For example: + +<nowiki><pre> +query $stmt col time $index {%Y%m%d @ %H:%M:%S} {+5 years} +query $stmt col time $index %s unixepoch +</pre></nowiki> + + + + +<h2>strftime</h2> + +This works like <tt>col time</tt> (described below) but takes its +value from an arbitrary source specified by the 3rd argument. + +<nowiki><pre> +query strftime %s 1319211587 unixepoch +query strftime {%Y%m%d @ %H:%M:%S} [query $stmt col string 2] {+10 years}] +</pre></nowiki> + +<h2>Global Variables</h2> + +This API installs the following global variables, all of which correspond to +<tt>SQLITE_xxx</tt> constant values: + + * <tt>SQLITE_BLOB</tt> + * <tt>SQLITE_FLOAT</tt> + * <tt>SQLITE_INTEGER</tt> + * <tt>SQLITE_NULL</tt> + * <tt>SQLITE_TEXT</tt> + +These values are used only by the <tt>col type</tt> function. They can be +accessed from script code via <tt>$::SQLITE_xxx</tt>.