From 70cdccb08c6d52be6add914db5844c25c9b92f0a Mon Sep 17 00:00:00 2001 From: dkf Date: Thu, 12 Jul 2001 13:15:09 +0000 Subject: TIP#36 implementation. Also includes cleanup for subst option handling and improved documentation for the subst command (in my capacity as maintainer of the Commands M-Z functional area.) --- ChangeLog | 17 ++++ doc/SubstObj.3 | 71 +++++++++++++++++ doc/subst.n | 44 +++++++++-- generic/tcl.decls | 6 +- generic/tcl.h | 11 ++- generic/tclCmdMZ.c | 213 +++++++++++++++++++++++++++++++++----------------- generic/tclDecls.h | 10 ++- generic/tclStubInit.c | 3 +- tests/subst.test | 16 ++-- unix/mkLinks | 8 ++ 10 files changed, 309 insertions(+), 90 deletions(-) create mode 100644 doc/SubstObj.3 diff --git a/ChangeLog b/ChangeLog index d48e39c..140e139 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,20 @@ +2001-07-12 Donal K. Fellows + + * tests/subst.test (subst-10.*): Updated tests to check new + behaviour for 'break' in command substitutions. + (subst-1.2,subst-7.1): Error messages changed. + * doc/SubstObj.3: New file, to document Tcl_SubstObj. + * doc/subst.n: Improved and updated documentation for 'subst' to + help support the changed behaviour. + * generic/tcl.decls (generic-437): Declaration for Tcl_SubstObj + * generic/tcl.h (TCL_SUBST_*): Added flags for Tcl_SubstObj. + * generic/tclCmdMZ.c (Tcl_SubstObj,Tcl_SubstObjCmd): Divided into + two parts to allow people to access the innards of 'subst' and + changed the behaviour when command substitutions do a 'break' to + be different from 'continue'. Also now works with objects, which + allows for some nifty optimisations with variable substitutions + and a slight improvement with command substitutions. [TIP#36] + 2001-07-10 Mo DeJong * unix/Makefile.in: Add AR variable for use in STLIB_LD. diff --git a/doc/SubstObj.3 b/doc/SubstObj.3 new file mode 100644 index 0000000..c90cbca --- /dev/null +++ b/doc/SubstObj.3 @@ -0,0 +1,71 @@ +'\" +'\" Copyright (c) 2001 Donal K. Fellows +'\" +'\" See the file "license.terms" for information on usage and redistribution +'\" of this file, and for a DISCLAIMER OF ALL WARRANTIES. +'\" +'\" RCS: @(#) $Id: SubstObj.3,v 1.1 2001/07/12 13:15:09 dkf Exp $ +'\" +.so man.macros +.TH Tcl_SubstObj 3 8.4 Tcl "Tcl Library Procedures" +.BS +.SH NAME +Tcl_SubstObj \- perform substitutions on Tcl objects +.SH SYNOPSIS +.nf +\fB#include \fR +.sp +Tcl_Obj * +\fBTcl_SubstObj\fR(\fIinterp, objPtr, flags\fR) +.SH ARGUMENTS +.AS Tcl_Interp **termPtr; +.AP Tcl_Interp *interp in +Interpreter in which to execute Tcl scripts and lookup variables. If +an error occurs, the interpreter's result is modified to hold an error +message. +.AP Tcl_Obj *objPtr in +A Tcl object containing the string to perform substitutions on. +.AP int flags in +ORed combination of flag bits that specify which substitutions to +perform. The flags \fBTCL_SUBST_COMMANDS\fR, +\fBTCL_SUBST_VARIABLES\fR and \fBTCL_SUBST_BACKSLASHES\fR are +currently supported, and \fBTCL_SUBST_ALL\fR is provided as a +convenience for the common case where all substitutions are desired. +.BE + +.SH DESCRIPTION +.PP +The \fBTcl_SubstObj\fR function is used to perform substitutions on +strings in the fashion of the \fBsubst\fR command. It gets the value +of the string contained in \fIobjPtr\fR and scans it, copying +characters and performing the chosen substitutions as it goes to an +output object which is returned as the result of the function. In the +event of an error occurring during the execution of a command or +variable substitution, the function returns NULL and an error message +is left in \fIinterp\fR's result. +.PP +Three kinds of substitutions are supported. When the +\fBTCL_SUBST_BACKSLASHES\fR bit is set in \fIflags\fR, sequences that +look like backslash substitutions for Tcl commands are replaced by +their corresponding character. +.PP +When the \fBTCL_SUBST_VARIABLES\fR bit is set in \fIflags\fR, +sequences that look like variable substitutions for Tcl commands are +replaced by the contents of the named variable. +.PP +When th \fBTCL_SUBST_COMMANDS\fR bit is set in \fIflags\fR, sequences +that look like command substitutions for Tcl commands are replaced by +the result of evaluating that script. Where an uncaught continue +exception occurs during the evaluation of a command substitution, an +empty string is substituted for the command. Where an uncaught break +exception occurs during the evaluation of a command substitution, the +result of the whole substitution on \fIobjPtr\fR will be truncated at +the point immediately before the start of the command substitution, +and no characters will be added to the result or substitutions +performed after that point. + +.SH "SEE ALSO" +subst(n) + +.SH KEYWORDS +backslash substitution, command substitution, variable substitution diff --git a/doc/subst.n b/doc/subst.n index 36884eb..b76a70c 100644 --- a/doc/subst.n +++ b/doc/subst.n @@ -1,11 +1,12 @@ '\" '\" Copyright (c) 1994 The Regents of the University of California. '\" Copyright (c) 1994-1996 Sun Microsystems, Inc. +'\" Copyright (c) 2001 Donal K. Fellows '\" '\" See the file "license.terms" for information on usage and redistribution '\" of this file, and for a DISCLAIMER OF ALL WARRANTIES. '\" -'\" RCS: @(#) $Id: subst.n,v 1.3 2000/09/07 14:27:51 poenitz Exp $ +'\" RCS: @(#) $Id: subst.n,v 1.4 2001/07/12 13:15:09 dkf Exp $ '\" .so man.macros .TH subst n 7.4 Tcl "Tcl Built-In Commands" @@ -35,17 +36,48 @@ For example, if \fB\-nocommands\fR is specified, no command substitution is performed: open and close brackets are treated as ordinary characters with no special interpretation. .PP -Note: when it performs its substitutions, \fIsubst\fR does not -give any special treatment to double quotes or curly braces. For -example, the script +.VS 8.4 +If a break exception occurs during the evaluation of a command +substitution, the result of the substitution will be the string (as +substituted) up to the start of the command substitution. If a +continue exception occurs during the evaluation of a command +substitution, an empty string will be substituted for that entire +command substitution (as long as it is well-formed Tcl.) +.VE +.SH EXAMPLES +.PP +When it performs its substitutions, \fIsubst\fR does not give any +special treatment to double quotes or curly braces (except within +command substitutions) so the script .CS \fBset a 44 subst {xyz {$a}}\fR .CE -returns ``\fBxyz {44}\fR'', not ``\fBxyz {$a}\fR''. +returns ``\fBxyz {44}\fR'', not ``\fBxyz {$a}\fR'' +.VS 8.4 +and the script +.CS +\fBset a "p\\} q \\{r" +subst {xyz {$a}}\fR +.CE +return ``\fBxyz {p} q {r}\fR'', not ``\fBxyz {p\\} q \\{r}\fR''. +.PP +The continue and break exceptions allow command substitutions to +prevent substitution of the rest of the command substitution and the +rest of \fIstring\fR respectively, giving script authors more options +when processing text using \fIsubst\fR. For example, the script +.CS +\fBsubst {abc,[break],def}\fR +.CE +returns ``\fBabc,\fR'', not ``\fBabc,,def\fR'' and the script +.CS +\fBsubst {abc,[continue;expr 1+2],def}\fR +.CE +returns ``\fBabc,,def\fR'', not ``\fBabc,3,def\fR''. +.VE .SH "SEE ALSO" -eval(n) +Tcl(n), eval(n), break(n), continue(n) .SH KEYWORDS backslash substitution, command substitution, variable substitution diff --git a/generic/tcl.decls b/generic/tcl.decls index f60a719..679cff8 100644 --- a/generic/tcl.decls +++ b/generic/tcl.decls @@ -10,7 +10,7 @@ # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # -# RCS: @(#) $Id: tcl.decls,v 1.49 2001/06/08 20:06:11 dgp Exp $ +# RCS: @(#) $Id: tcl.decls,v 1.50 2001/07/12 13:15:09 dkf Exp $ library tcl @@ -1527,7 +1527,9 @@ declare 435 generic { declare 436 generic { Tcl_Obj * Tcl_ListMathFuncs(Tcl_Interp *interp, CONST char *pattern) } - +declare 437 generic { + Tcl_Obj * Tcl_SubstObj(Tcl_Interp *interp, Tcl_Obj *objPtr, int flags) +} ############################################################################## diff --git a/generic/tcl.h b/generic/tcl.h index 3e8a71a..a08cbc8 100644 --- a/generic/tcl.h +++ b/generic/tcl.h @@ -12,7 +12,7 @@ * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. * - * RCS: @(#) $Id: tcl.h,v 1.91 2001/06/08 20:06:11 dgp Exp $ + * RCS: @(#) $Id: tcl.h,v 1.92 2001/07/12 13:15:09 dkf Exp $ */ #ifndef _TCL @@ -535,6 +535,15 @@ typedef struct stat *Tcl_Stat_; #define TCL_RESULT_SIZE 200 /* + * Flags to control what substitutions are performed by Tcl_SubstObj(): + */ + +#define TCL_SUBST_COMMANDS 001 +#define TCL_SUBST_VARIABLES 002 +#define TCL_SUBST_BACKSLASHES 004 +#define TCL_SUBST_ALL 007 + +/* * Argument descriptors for math function callbacks in expressions: */ diff --git a/generic/tclCmdMZ.c b/generic/tclCmdMZ.c index 5984ed0..1024430 100644 --- a/generic/tclCmdMZ.c +++ b/generic/tclCmdMZ.c @@ -13,7 +13,7 @@ * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. * - * RCS: @(#) $Id: tclCmdMZ.c,v 1.39 2001/06/12 08:07:37 dkf Exp $ + * RCS: @(#) $Id: tclCmdMZ.c,v 1.40 2001/07/12 13:15:09 dkf Exp $ */ #include "tclInt.h" @@ -2247,8 +2247,7 @@ Tcl_StringObjCmd(dummy, interp, objc, objv) * * This procedure is invoked to process the "subst" Tcl command. * See the user documentation for details on what it does. This - * command is an almost direct copy of an implementation by - * Andrew Payne. + * command relies on Tcl_SubstObj() for its implementation. * * Results: * A standard Tcl result. @@ -2273,21 +2272,15 @@ Tcl_SubstObjCmd(dummy, interp, objc, objv) enum substOptions { SUBST_NOBACKSLASHES, SUBST_NOCOMMANDS, SUBST_NOVARS }; - Interp *iPtr = (Interp *) interp; - Tcl_DString result; - char *p, *old, *value; - int optionIndex, code, count, doVars, doCmds, doBackslashes, i; + Tcl_Obj *resultPtr; + int optionIndex, flags, i; /* * Parse command-line options. */ - doVars = doCmds = doBackslashes = 1; + flags = TCL_SUBST_ALL; for (i = 1; i < (objc-1); i++) { - p = Tcl_GetString(objv[i]); - if (*p != '-') { - break; - } if (Tcl_GetIndexFromObj(interp, objv[i], substOptions, "switch", 0, &optionIndex) != TCL_OK) { @@ -2295,15 +2288,15 @@ Tcl_SubstObjCmd(dummy, interp, objc, objv) } switch (optionIndex) { case SUBST_NOBACKSLASHES: { - doBackslashes = 0; + flags &= ~TCL_SUBST_BACKSLASHES; break; } case SUBST_NOCOMMANDS: { - doCmds = 0; + flags &= ~TCL_SUBST_COMMANDS; break; } case SUBST_NOVARS: { - doVars = 0; + flags &= ~TCL_SUBST_VARIABLES; break; } default: { @@ -2318,76 +2311,154 @@ Tcl_SubstObjCmd(dummy, interp, objc, objv) } /* - * Scan through the string one character at a time, performing - * command, variable, and backslash substitutions. + * Perform the substitution. */ + resultPtr = Tcl_SubstObj(interp, objv[i], flags); - Tcl_DStringInit(&result); - old = p = Tcl_GetString(objv[i]); - while (*p != 0) { + if (resultPtr == NULL) { + return TCL_ERROR; + } + Tcl_SetObjResult(interp, resultPtr); + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * Tcl_SubstObjCmd -- + * + * This function performs the substitutions specificed on the + * given string as described in the user documentation for the + * "subst" Tcl command. This code is heavily based on an + * implementation by Andrew Payne. Note that if a command + * substitution returns TCL_CONTINUE or TCL_RETURN from its + * evaluation and is not completely well-formed, the results are + * not defined. + * + * Results: + * A Tcl_Obj* containing the substituted string, or NULL to + * indicate that an error occurred. + * + * Side effects: + * See the user documentation. + * + *---------------------------------------------------------------------- + */ + +Tcl_Obj * +Tcl_SubstObj(interp, objPtr, flags) + Tcl_Interp *interp; + Tcl_Obj *objPtr; + int flags; +{ + Tcl_Obj *resultObj; + char *p, *old; + + old = p = Tcl_GetString(objPtr); + resultObj = Tcl_NewStringObj("", 0); + while (1) { switch (*p) { - case '\\': - if (doBackslashes) { - char buf[TCL_UTF_MAX]; + case 0: + if (p != old) { + Tcl_AppendToObj(resultObj, old, p-old); + } + return resultObj; - if (p != old) { - Tcl_DStringAppend(&result, old, p-old); - } - Tcl_DStringAppend(&result, buf, - Tcl_UtfBackslash(p, &count, buf)); - p += count; - old = p; - } else { - p++; + case '\\': + if (flags & TCL_SUBST_BACKSLASHES) { + char buf[TCL_UTF_MAX]; + int count; + + if (p != old) { + Tcl_AppendToObj(resultObj, old, p-old); } - break; + Tcl_AppendToObj(resultObj, buf, + Tcl_UtfBackslash(p, &count, buf)); + p += count; + old = p; + } else { + p++; + } + break; - case '$': - if (doVars) { - if (p != old) { - Tcl_DStringAppend(&result, old, p-old); - } - value = Tcl_ParseVar(interp, p, &p); - if (value == NULL) { - Tcl_DStringFree(&result); - return TCL_ERROR; - } - Tcl_DStringAppend(&result, value, -1); - old = p; - } else { + case '$': + if (flags & TCL_SUBST_VARIABLES) { + Tcl_Parse parse; + Tcl_Obj *tempObj; + + /* + * Code is simpler overall if we (effectively) inline + * Tcl_ParseVar, particularly as that allows us to use + * a non-string interface when we come to appending + * the variable contents to the result object. There + * are a few other optimisations that doing this + * enables (like being able to continue the run of + * unsubstituted characters straight through if a '$' + * does not precede a variable name.) + */ + if (Tcl_ParseVarName(interp, p, -1, &parse, 0) != TCL_OK) { + goto errorResult; + } + if (parse.numTokens == 1) { + /* + * There isn't a variable name after all: the $ is + * just a $. + */ p++; + break; } - break; + if (p != old) { + Tcl_AppendToObj(resultObj, old, p-old); + } + p += parse.tokenPtr->size; + tempObj = Tcl_EvalTokens(interp, parse.tokenPtr, + parse.numTokens); + if (tempObj == NULL) { + goto errorResult; + } + Tcl_AppendObjToObj(resultObj, tempObj); + Tcl_DecrRefCount(tempObj); + old = p; + } else { + p++; + } + break; - case '[': - if (doCmds) { - if (p != old) { - Tcl_DStringAppend(&result, old, p-old); - } - iPtr->evalFlags = TCL_BRACKET_TERM; - code = Tcl_Eval(interp, p+1); - if (code == TCL_ERROR) { - Tcl_DStringFree(&result); - return code; - } - old = p = (p+1 + iPtr->termOffset+1); - Tcl_DStringAppend(&result, iPtr->result, -1); + case '[': + if (flags & TCL_SUBST_COMMANDS) { + Interp *iPtr = (Interp *) interp; + int code; + + if (p != old) { + Tcl_AppendToObj(resultObj, old, p-old); + } + iPtr->evalFlags = TCL_BRACKET_TERM; + code = Tcl_EvalEx(interp, p+1, -1, 0); + switch (code) { + case TCL_ERROR: + goto errorResult; + case TCL_BREAK: Tcl_ResetResult(interp); - } else { - p++; + return resultObj; + default: + Tcl_AppendObjToObj(resultObj, Tcl_GetObjResult(interp)); + case TCL_CONTINUE: + Tcl_ResetResult(interp); + old = p = (p+1 + iPtr->termOffset + 1); } - break; - - default: + } else { p++; - break; + } + break; + default: + p++; + break; } } - if (p != old) { - Tcl_DStringAppend(&result, old, p-old); - } - Tcl_DStringResult(interp, &result); - return TCL_OK; + + errorResult: + Tcl_DecrRefCount(resultObj); + return NULL; } /* diff --git a/generic/tclDecls.h b/generic/tclDecls.h index bb600ed..2e94d6a 100644 --- a/generic/tclDecls.h +++ b/generic/tclDecls.h @@ -8,7 +8,7 @@ * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. * - * RCS: @(#) $Id: tclDecls.h,v 1.51 2001/06/08 20:06:11 dgp Exp $ + * RCS: @(#) $Id: tclDecls.h,v 1.52 2001/07/12 13:15:09 dkf Exp $ */ #ifndef _TCLDECLS @@ -1368,6 +1368,9 @@ EXTERN int Tcl_GetMathFuncInfo _ANSI_ARGS_((Tcl_Interp * interp, /* 436 */ EXTERN Tcl_Obj * Tcl_ListMathFuncs _ANSI_ARGS_((Tcl_Interp * interp, CONST char * pattern)); +/* 437 */ +EXTERN Tcl_Obj * Tcl_SubstObj _ANSI_ARGS_((Tcl_Interp * interp, + Tcl_Obj * objPtr, int flags)); typedef struct TclStubHooks { struct TclPlatStubs *tclPlatStubs; @@ -1864,6 +1867,7 @@ typedef struct TclStubs { Tcl_UniChar * (*tcl_GetUnicodeFromObj) _ANSI_ARGS_((Tcl_Obj * objPtr, int * lengthPtr)); /* 434 */ int (*tcl_GetMathFuncInfo) _ANSI_ARGS_((Tcl_Interp * interp, CONST char * name, int * numArgsPtr, Tcl_ValueType ** argTypesPtr, Tcl_MathProc ** procPtr, ClientData * clientDataPtr)); /* 435 */ Tcl_Obj * (*tcl_ListMathFuncs) _ANSI_ARGS_((Tcl_Interp * interp, CONST char * pattern)); /* 436 */ + Tcl_Obj * (*tcl_SubstObj) _ANSI_ARGS_((Tcl_Interp * interp, Tcl_Obj * objPtr, int flags)); /* 437 */ } TclStubs; #ifdef __cplusplus @@ -3652,6 +3656,10 @@ extern TclStubs *tclStubsPtr; #define Tcl_ListMathFuncs \ (tclStubsPtr->tcl_ListMathFuncs) /* 436 */ #endif +#ifndef Tcl_SubstObj +#define Tcl_SubstObj \ + (tclStubsPtr->tcl_SubstObj) /* 437 */ +#endif #endif /* defined(USE_TCL_STUBS) && !defined(USE_TCL_STUB_PROCS) */ diff --git a/generic/tclStubInit.c b/generic/tclStubInit.c index cf8b8b9..1fe3582 100644 --- a/generic/tclStubInit.c +++ b/generic/tclStubInit.c @@ -8,7 +8,7 @@ * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. * - * RCS: @(#) $Id: tclStubInit.c,v 1.52 2001/06/17 03:48:19 dgp Exp $ + * RCS: @(#) $Id: tclStubInit.c,v 1.53 2001/07/12 13:15:09 dkf Exp $ */ #include "tclInt.h" @@ -832,6 +832,7 @@ TclStubs tclStubs = { Tcl_GetUnicodeFromObj, /* 434 */ Tcl_GetMathFuncInfo, /* 435 */ Tcl_ListMathFuncs, /* 436 */ + Tcl_SubstObj, /* 437 */ }; /* !END!: Do not edit above this line. */ diff --git a/tests/subst.test b/tests/subst.test index 21b0d7e..662db99 100644 --- a/tests/subst.test +++ b/tests/subst.test @@ -11,7 +11,7 @@ # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # -# RCS: @(#) $Id: subst.test,v 1.8 2001/06/28 01:10:15 hobbs Exp $ +# RCS: @(#) $Id: subst.test,v 1.9 2001/07/12 13:15:09 dkf Exp $ if {[lsearch [namespace children] ::tcltest] == -1} { package require tcltest @@ -23,7 +23,7 @@ test subst-1.1 {basics} { } {1 {wrong # args: should be "subst ?-nobackslashes? ?-nocommands? ?-novariables? string"}} test subst-1.2 {basics} { list [catch {subst a b c} msg] $msg -} {1 {wrong # args: should be "subst ?-nobackslashes? ?-nocommands? ?-novariables? string"}} +} {1 {bad switch "a": must be -nobackslashes, -nocommands, or -novariables}} test subst-2.1 {simple strings} { subst {} @@ -90,7 +90,7 @@ test subst-6.1 {clear the result after command substitution} { test subst-7.1 {switches} { list [catch {subst foo bar} msg] $msg -} {1 {wrong # args: should be "subst ?-nobackslashes? ?-nocommands? ?-novariables? string"}} +} {1 {bad switch "foo": must be -nobackslashes, -nocommands, or -novariables}} test subst-7.2 {switches} { list [catch {subst -no bar} msg] $msg } {1 {ambiguous switch "-no": must be -nobackslashes, -nocommands, or -novariables}} @@ -148,19 +148,19 @@ test subst-9.2 {error in a subst} { test subst-10.1 {break in a subst} { subst {foo [break; bogus code] bar} -} {foo bar} +} {foo } test subst-10.2 {break in a subst} { subst {foo [break; return x; bogus code] bar} -} {foo bar} +} {foo } test subst-10.3 {break in a subst} { subst {foo [if 1 { break; bogus code}] bar} -} {foo bar} +} {foo } test subst-10.4 {break in a subst, parse error} { subst {foo [break ; set a {}{} ; stuff] bar} -} {foo set a {}{} ; stuff] bar} +} {foo } test subst-10.5 {break in a subst, parse error} { subst {foo [break ;set bar baz ;set a {}{} ; stuff] bar} -} {foo set bar baz ;set a {}{} ; stuff] bar} +} {foo } test subst-11.1 {continue in a subst} { subst {foo [continue; bogus code] bar} diff --git a/unix/mkLinks b/unix/mkLinks index fde23e4..1b57e15 100644 --- a/unix/mkLinks +++ b/unix/mkLinks @@ -234,7 +234,11 @@ if test -r CrtInterp.3; then fi if test -r CrtMathFnc.3; then rm -f Tcl_CreateMathFunc.3 + rm -f Tcl_GetMathFuncInfo.3 + rm -f Tcl_ListMathFuncs.3 ln CrtMathFnc.3 Tcl_CreateMathFunc.3 + ln CrtMathFnc.3 Tcl_GetMathFuncInfo.3 + ln CrtMathFnc.3 Tcl_ListMathFuncs.3 fi if test -r CrtObjCmd.3; then rm -f Tcl_CreateObjCommand.3 @@ -928,6 +932,10 @@ if test -r StringObj.3; then ln StringObj.3 Tcl_ConcatObj.3 ln StringObj.3 Tcl_AttemptSetObjLength.3 fi +if test -r SubstObj.3; then + rm -f Tcl_SubstObj.3 + ln SubstObj.3 Tcl_SubstObj.3 +fi if test -r Tcl_Main.3; then rm -f Tcl_SetMainLoop.3 ln Tcl_Main.3 Tcl_SetMainLoop.3 -- cgit v0.12